From df951facc53ff1c30332ec56846a14c7db5dac3e Mon Sep 17 00:00:00 2001 From: gruebel Date: Sun, 22 Dec 2019 11:42:15 +0100 Subject: [PATCH 1/4] Add codepipeline.list_tags_for_resource --- moto/codepipeline/models.py | 15 + moto/codepipeline/responses.py | 7 + tests/test_codepipeline/test_codepipeline.py | 469 ++++--------------- 3 files changed, 119 insertions(+), 372 deletions(-) diff --git a/moto/codepipeline/models.py b/moto/codepipeline/models.py index b3e76f838..888491296 100644 --- a/moto/codepipeline/models.py +++ b/moto/codepipeline/models.py @@ -145,6 +145,21 @@ class CodePipelineBackend(BaseBackend): def delete_pipeline(self, name): self.pipelines.pop(name, None) + def list_tags_for_resource(self, arn): + name = arn.split(":")[-1] + pipeline = self.pipelines.get(name) + + if not pipeline: + raise ResourceNotFoundException( + "The account with id '{0}' does not include a pipeline with the name '{1}'".format( + ACCOUNT_ID, name + ) + ) + + tags = [{"key": key, "value": value} for key, value in pipeline.tags.items()] + + return tags + codepipeline_backends = {} for region in Session().get_available_regions("codepipeline"): diff --git a/moto/codepipeline/responses.py b/moto/codepipeline/responses.py index f2eee4d4e..75a2ce800 100644 --- a/moto/codepipeline/responses.py +++ b/moto/codepipeline/responses.py @@ -39,3 +39,10 @@ class CodePipelineResponse(BaseResponse): self.codepipeline_backend.delete_pipeline(self._get_param("name")) return "" + + def list_tags_for_resource(self): + tags = self.codepipeline_backend.list_tags_for_resource( + self._get_param("resourceArn") + ) + + return json.dumps({"tags": tags}) diff --git a/tests/test_codepipeline/test_codepipeline.py b/tests/test_codepipeline/test_codepipeline.py index 926d7f873..b93a15fc7 100644 --- a/tests/test_codepipeline/test_codepipeline.py +++ b/tests/test_codepipeline/test_codepipeline.py @@ -13,52 +13,7 @@ from moto import mock_codepipeline, mock_iam def test_create_pipeline(): client = boto3.client("codepipeline", region_name="us-east-1") - response = client.create_pipeline( - pipeline={ - "name": "test-pipeline", - "roleArn": get_role_arn(), - "artifactStore": { - "type": "S3", - "location": "codepipeline-us-east-1-123456789012", - }, - "stages": [ - { - "name": "Stage-1", - "actions": [ - { - "name": "Action-1", - "actionTypeId": { - "category": "Source", - "owner": "AWS", - "provider": "S3", - "version": "1", - }, - "configuration": { - "S3Bucket": "test-bucket", - "S3ObjectKey": "test-object", - }, - "outputArtifacts": [{"name": "artifact"},], - }, - ], - }, - { - "name": "Stage-2", - "actions": [ - { - "name": "Action-1", - "actionTypeId": { - "category": "Approval", - "owner": "AWS", - "provider": "Manual", - "version": "1", - }, - }, - ], - }, - ], - }, - tags=[{"key": "key", "value": "value"}], - ) + response = create_basic_codepipeline(client, "test-pipeline") response["pipeline"].should.equal( { @@ -120,98 +75,10 @@ def test_create_pipeline(): def test_create_pipeline_errors(): client = boto3.client("codepipeline", region_name="us-east-1") client_iam = boto3.client("iam", region_name="us-east-1") - client.create_pipeline( - pipeline={ - "name": "test-pipeline", - "roleArn": get_role_arn(), - "artifactStore": { - "type": "S3", - "location": "codepipeline-us-east-1-123456789012", - }, - "stages": [ - { - "name": "Stage-1", - "actions": [ - { - "name": "Action-1", - "actionTypeId": { - "category": "Source", - "owner": "AWS", - "provider": "S3", - "version": "1", - }, - "configuration": { - "S3Bucket": "test-bucket", - "S3ObjectKey": "test-object", - }, - "outputArtifacts": [{"name": "artifact"},], - }, - ], - }, - { - "name": "Stage-2", - "actions": [ - { - "name": "Action-1", - "actionTypeId": { - "category": "Approval", - "owner": "AWS", - "provider": "Manual", - "version": "1", - }, - }, - ], - }, - ], - } - ) + create_basic_codepipeline(client, "test-pipeline") with assert_raises(ClientError) as e: - client.create_pipeline( - pipeline={ - "name": "test-pipeline", - "roleArn": get_role_arn(), - "artifactStore": { - "type": "S3", - "location": "codepipeline-us-east-1-123456789012", - }, - "stages": [ - { - "name": "Stage-1", - "actions": [ - { - "name": "Action-1", - "actionTypeId": { - "category": "Source", - "owner": "AWS", - "provider": "S3", - "version": "1", - }, - "configuration": { - "S3Bucket": "test-bucket", - "S3ObjectKey": "test-object", - }, - "outputArtifacts": [{"name": "artifact"},], - }, - ], - }, - { - "name": "Stage-2", - "actions": [ - { - "name": "Action-1", - "actionTypeId": { - "category": "Approval", - "owner": "AWS", - "provider": "Manual", - "version": "1", - }, - }, - ], - }, - ], - } - ) + create_basic_codepipeline(client, "test-pipeline") ex = e.exception ex.operation_name.should.equal("CreatePipeline") ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) @@ -348,52 +215,7 @@ def test_create_pipeline_errors(): @mock_codepipeline def test_get_pipeline(): client = boto3.client("codepipeline", region_name="us-east-1") - client.create_pipeline( - pipeline={ - "name": "test-pipeline", - "roleArn": get_role_arn(), - "artifactStore": { - "type": "S3", - "location": "codepipeline-us-east-1-123456789012", - }, - "stages": [ - { - "name": "Stage-1", - "actions": [ - { - "name": "Action-1", - "actionTypeId": { - "category": "Source", - "owner": "AWS", - "provider": "S3", - "version": "1", - }, - "configuration": { - "S3Bucket": "test-bucket", - "S3ObjectKey": "test-object", - }, - "outputArtifacts": [{"name": "artifact"},], - }, - ], - }, - { - "name": "Stage-2", - "actions": [ - { - "name": "Action-1", - "actionTypeId": { - "category": "Approval", - "owner": "AWS", - "provider": "Manual", - "version": "1", - }, - }, - ], - }, - ], - }, - tags=[{"key": "key", "value": "value"}], - ) + create_basic_codepipeline(client, "test-pipeline") response = client.get_pipeline(name="test-pipeline") @@ -474,53 +296,7 @@ def test_get_pipeline_errors(): @mock_codepipeline def test_update_pipeline(): client = boto3.client("codepipeline", region_name="us-east-1") - role_arn = get_role_arn() - client.create_pipeline( - pipeline={ - "name": "test-pipeline", - "roleArn": role_arn, - "artifactStore": { - "type": "S3", - "location": "codepipeline-us-east-1-123456789012", - }, - "stages": [ - { - "name": "Stage-1", - "actions": [ - { - "name": "Action-1", - "actionTypeId": { - "category": "Source", - "owner": "AWS", - "provider": "S3", - "version": "1", - }, - "configuration": { - "S3Bucket": "test-bucket", - "S3ObjectKey": "test-object", - }, - "outputArtifacts": [{"name": "artifact"},], - }, - ], - }, - { - "name": "Stage-2", - "actions": [ - { - "name": "Action-1", - "actionTypeId": { - "category": "Approval", - "owner": "AWS", - "provider": "Manual", - "version": "1", - }, - }, - ], - }, - ], - }, - tags=[{"key": "key", "value": "value"}], - ) + create_basic_codepipeline(client, "test-pipeline") response = client.get_pipeline(name="test-pipeline") created_time = response["metadata"]["created"] @@ -529,7 +305,7 @@ def test_update_pipeline(): response = client.update_pipeline( pipeline={ "name": "test-pipeline", - "roleArn": role_arn, + "roleArn": get_role_arn(), "artifactStore": { "type": "S3", "location": "codepipeline-us-east-1-123456789012", @@ -692,105 +468,19 @@ def test_update_pipeline_errors(): @mock_codepipeline def test_list_pipelines(): client = boto3.client("codepipeline", region_name="us-east-1") - client.create_pipeline( - pipeline={ - "name": "test-pipeline-1", - "roleArn": get_role_arn(), - "artifactStore": { - "type": "S3", - "location": "codepipeline-us-east-1-123456789012", - }, - "stages": [ - { - "name": "Stage-1", - "actions": [ - { - "name": "Action-1", - "actionTypeId": { - "category": "Source", - "owner": "AWS", - "provider": "S3", - "version": "1", - }, - "configuration": { - "S3Bucket": "test-bucket", - "S3ObjectKey": "test-object", - }, - "outputArtifacts": [{"name": "artifact"},], - }, - ], - }, - { - "name": "Stage-2", - "actions": [ - { - "name": "Action-1", - "actionTypeId": { - "category": "Approval", - "owner": "AWS", - "provider": "Manual", - "version": "1", - }, - }, - ], - }, - ], - }, - ) - client.create_pipeline( - pipeline={ - "name": "test-pipeline-2", - "roleArn": get_role_arn(), - "artifactStore": { - "type": "S3", - "location": "codepipeline-us-east-1-123456789012", - }, - "stages": [ - { - "name": "Stage-1", - "actions": [ - { - "name": "Action-1", - "actionTypeId": { - "category": "Source", - "owner": "AWS", - "provider": "S3", - "version": "1", - }, - "configuration": { - "S3Bucket": "test-bucket", - "S3ObjectKey": "test-object", - }, - "outputArtifacts": [{"name": "artifact"},], - }, - ], - }, - { - "name": "Stage-2", - "actions": [ - { - "name": "Action-1", - "actionTypeId": { - "category": "Approval", - "owner": "AWS", - "provider": "Manual", - "version": "1", - }, - }, - ], - }, - ], - }, - ) + name_1 = "test-pipeline-1" + create_basic_codepipeline(client, name_1) + name_2 = "test-pipeline-2" + create_basic_codepipeline(client, name_2) response = client.list_pipelines() response["pipelines"].should.have.length_of(2) - response["pipelines"][0]["name"].should.equal("test-pipeline-1") + response["pipelines"][0]["name"].should.equal(name_1) response["pipelines"][0]["version"].should.equal(1) response["pipelines"][0]["created"].should.be.a(datetime) response["pipelines"][0]["updated"].should.be.a(datetime) - response["pipelines"][1]["name"].should.equal("test-pipeline-2") + response["pipelines"][1]["name"].should.equal(name_2) response["pipelines"][1]["version"].should.equal(1) response["pipelines"][1]["created"].should.be.a(datetime) response["pipelines"][1]["updated"].should.be.a(datetime) @@ -799,68 +489,54 @@ def test_list_pipelines(): @mock_codepipeline def test_delete_pipeline(): client = boto3.client("codepipeline", region_name="us-east-1") - client.create_pipeline( - pipeline={ - "name": "test-pipeline", - "roleArn": get_role_arn(), - "artifactStore": { - "type": "S3", - "location": "codepipeline-us-east-1-123456789012", - }, - "stages": [ - { - "name": "Stage-1", - "actions": [ - { - "name": "Action-1", - "actionTypeId": { - "category": "Source", - "owner": "AWS", - "provider": "S3", - "version": "1", - }, - "configuration": { - "S3Bucket": "test-bucket", - "S3ObjectKey": "test-object", - }, - "outputArtifacts": [{"name": "artifact"},], - }, - ], - }, - { - "name": "Stage-2", - "actions": [ - { - "name": "Action-1", - "actionTypeId": { - "category": "Approval", - "owner": "AWS", - "provider": "Manual", - "version": "1", - }, - }, - ], - }, - ], - }, - ) + name = "test-pipeline" + create_basic_codepipeline(client, name) client.list_pipelines()["pipelines"].should.have.length_of(1) - client.delete_pipeline(name="test-pipeline") + client.delete_pipeline(name=name) client.list_pipelines()["pipelines"].should.have.length_of(0) # deleting a not existing pipeline, should raise no exception - client.delete_pipeline(name="test-pipeline") + client.delete_pipeline(name=name) + + +@mock_codepipeline +def test_list_tags_for_resource(): + client = boto3.client("codepipeline", region_name="us-east-1") + name = "test-pipeline" + create_basic_codepipeline(client, name) + + response = client.list_tags_for_resource( + resourceArn="arn:aws:codepipeline:us-east-1:123456789012:{}".format(name) + ) + response["tags"].should.equal([{"key": "key", "value": "value"}]) + + +@mock_codepipeline +def test_list_tags_for_resource_errors(): + client = boto3.client("codepipeline", region_name="us-east-1") + + with assert_raises(ClientError) as e: + client.list_tags_for_resource( + resourceArn="arn:aws:codepipeline:us-east-1:123456789012:not-existing" + ) + ex = e.exception + ex.operation_name.should.equal("ListTagsForResource") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("ResourceNotFoundException") + ex.response["Error"]["Message"].should.equal( + "The account with id '123456789012' does not include a pipeline with the name 'not-existing'" + ) @mock_iam def get_role_arn(): - iam = boto3.client("iam", region_name="us-east-1") + client = boto3.client("iam", region_name="us-east-1") try: - return iam.get_role(RoleName="test-role")["Role"]["Arn"] + return client.get_role(RoleName="test-role")["Role"]["Arn"] except ClientError: - return iam.create_role( + return client.create_role( RoleName="test-role", AssumeRolePolicyDocument=json.dumps( { @@ -875,3 +551,52 @@ def get_role_arn(): } ), )["Role"]["Arn"] + + +def create_basic_codepipeline(client, name): + return client.create_pipeline( + pipeline={ + "name": name, + "roleArn": get_role_arn(), + "artifactStore": { + "type": "S3", + "location": "codepipeline-us-east-1-123456789012", + }, + "stages": [ + { + "name": "Stage-1", + "actions": [ + { + "name": "Action-1", + "actionTypeId": { + "category": "Source", + "owner": "AWS", + "provider": "S3", + "version": "1", + }, + "configuration": { + "S3Bucket": "test-bucket", + "S3ObjectKey": "test-object", + }, + "outputArtifacts": [{"name": "artifact"},], + }, + ], + }, + { + "name": "Stage-2", + "actions": [ + { + "name": "Action-1", + "actionTypeId": { + "category": "Approval", + "owner": "AWS", + "provider": "Manual", + "version": "1", + }, + }, + ], + }, + ], + }, + tags=[{"key": "key", "value": "value"}], + ) From b96a46b98fb717ae9fe660999a69e426a029a4b0 Mon Sep 17 00:00:00 2001 From: gruebel Date: Mon, 23 Dec 2019 19:33:37 +0100 Subject: [PATCH 2/4] Add codepipeline.tag_resource --- moto/codepipeline/exceptions.py | 16 +++++ moto/codepipeline/models.py | 32 +++++++++ moto/codepipeline/responses.py | 7 ++ tests/test_codepipeline/test_codepipeline.py | 72 ++++++++++++++++++++ 4 files changed, 127 insertions(+) diff --git a/moto/codepipeline/exceptions.py b/moto/codepipeline/exceptions.py index e455298cd..a4db9aab1 100644 --- a/moto/codepipeline/exceptions.py +++ b/moto/codepipeline/exceptions.py @@ -26,3 +26,19 @@ class ResourceNotFoundException(JsonRESTError): super(ResourceNotFoundException, self).__init__( "ResourceNotFoundException", message ) + + +class InvalidTagsException(JsonRESTError): + code = 400 + + def __init__(self, message): + super(InvalidTagsException, self).__init__("InvalidTagsException", message) + + +class TooManyTagsException(JsonRESTError): + code = 400 + + def __init__(self, arn): + super(TooManyTagsException, self).__init__( + "TooManyTagsException", "Tag limit exceeded for resource [{}].".format(arn) + ) diff --git a/moto/codepipeline/models.py b/moto/codepipeline/models.py index 888491296..4a8b89617 100644 --- a/moto/codepipeline/models.py +++ b/moto/codepipeline/models.py @@ -12,6 +12,8 @@ from moto.codepipeline.exceptions import ( InvalidStructureException, PipelineNotFoundException, ResourceNotFoundException, + InvalidTagsException, + TooManyTagsException, ) from moto.core import BaseBackend, BaseModel @@ -54,6 +56,18 @@ class CodePipeline(BaseModel): return pipeline + def validate_tags(self, tags): + for tag in tags: + if tag["key"].startswith("aws:"): + raise InvalidTagsException( + "Not allowed to modify system tags. " + "System tags start with 'aws:'. " + "msg=[Caller is an end user and not allowed to mutate system tags]" + ) + + if (len(self.tags) + len(tags)) > 50: + raise TooManyTagsException(self._arn) + class CodePipelineBackend(BaseBackend): def __init__(self): @@ -93,6 +107,8 @@ class CodePipelineBackend(BaseBackend): self.pipelines[pipeline["name"]] = CodePipeline(region, pipeline) if tags: + self.pipelines[pipeline["name"]].validate_tags(tags) + new_tags = {tag["key"]: tag["value"] for tag in tags} self.pipelines[pipeline["name"]].tags.update(new_tags) @@ -160,6 +176,22 @@ class CodePipelineBackend(BaseBackend): return tags + def tag_resource(self, arn, tags): + name = arn.split(":")[-1] + pipeline = self.pipelines.get(name) + + if not pipeline: + raise ResourceNotFoundException( + "The account with id '{0}' does not include a pipeline with the name '{1}'".format( + ACCOUNT_ID, name + ) + ) + + pipeline.validate_tags(tags) + + for tag in tags: + pipeline.tags.update({tag["key"]: tag["value"]}) + codepipeline_backends = {} for region in Session().get_available_regions("codepipeline"): diff --git a/moto/codepipeline/responses.py b/moto/codepipeline/responses.py index 75a2ce800..df1bf220f 100644 --- a/moto/codepipeline/responses.py +++ b/moto/codepipeline/responses.py @@ -46,3 +46,10 @@ class CodePipelineResponse(BaseResponse): ) return json.dumps({"tags": tags}) + + def tag_resource(self): + self.codepipeline_backend.tag_resource( + self._get_param("resourceArn"), self._get_param("tags") + ) + + return "" diff --git a/tests/test_codepipeline/test_codepipeline.py b/tests/test_codepipeline/test_codepipeline.py index b93a15fc7..e71e24f76 100644 --- a/tests/test_codepipeline/test_codepipeline.py +++ b/tests/test_codepipeline/test_codepipeline.py @@ -530,6 +530,78 @@ def test_list_tags_for_resource_errors(): ) +@mock_codepipeline +def test_tag_resource(): + client = boto3.client("codepipeline", region_name="us-east-1") + name = "test-pipeline" + create_basic_codepipeline(client, name) + + client.tag_resource( + resourceArn="arn:aws:codepipeline:us-east-1:123456789012:{}".format(name), + tags=[{"key": "key-2", "value": "value-2"}], + ) + + response = client.list_tags_for_resource( + resourceArn="arn:aws:codepipeline:us-east-1:123456789012:{}".format(name) + ) + response["tags"].should.equal( + [{"key": "key", "value": "value"}, {"key": "key-2", "value": "value-2"}] + ) + + +@mock_codepipeline +def test_tag_resource_errors(): + client = boto3.client("codepipeline", region_name="us-east-1") + name = "test-pipeline" + create_basic_codepipeline(client, name) + + with assert_raises(ClientError) as e: + client.tag_resource( + resourceArn="arn:aws:codepipeline:us-east-1:123456789012:not-existing", + tags=[{"key": "key-2", "value": "value-2"}], + ) + ex = e.exception + ex.operation_name.should.equal("TagResource") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("ResourceNotFoundException") + ex.response["Error"]["Message"].should.equal( + "The account with id '123456789012' does not include a pipeline with the name 'not-existing'" + ) + + with assert_raises(ClientError) as e: + client.tag_resource( + resourceArn="arn:aws:codepipeline:us-east-1:123456789012:{}".format(name), + tags=[{"key": "aws:key", "value": "value"}], + ) + ex = e.exception + ex.operation_name.should.equal("TagResource") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("InvalidTagsException") + ex.response["Error"]["Message"].should.equal( + "Not allowed to modify system tags. " + "System tags start with 'aws:'. " + "msg=[Caller is an end user and not allowed to mutate system tags]" + ) + + with assert_raises(ClientError) as e: + client.tag_resource( + resourceArn="arn:aws:codepipeline:us-east-1:123456789012:{}".format(name), + tags=[ + {"key": "key-{}".format(i), "value": "value-{}".format(i)} + for i in range(50) + ], + ) + ex = e.exception + ex.operation_name.should.equal("TagResource") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("TooManyTagsException") + ex.response["Error"]["Message"].should.equal( + "Tag limit exceeded for resource [arn:aws:codepipeline:us-east-1:123456789012:{}].".format( + name + ) + ) + + @mock_iam def get_role_arn(): client = boto3.client("iam", region_name="us-east-1") From 8331d480bab8b0556718d9af0c73e19eb0a05104 Mon Sep 17 00:00:00 2001 From: gruebel Date: Mon, 23 Dec 2019 19:50:16 +0100 Subject: [PATCH 3/4] Add codepipeline.untag_resource --- IMPLEMENTATION_COVERAGE.md | 8 ++-- moto/codepipeline/models.py | 14 ++++++ moto/codepipeline/responses.py | 7 +++ tests/test_codepipeline/test_codepipeline.py | 46 ++++++++++++++++++++ 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 5e6ef1c9e..a1b0ffb5e 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -1374,7 +1374,7 @@ - [ ] update_profiling_group ## codepipeline -13% implemented +22% implemented - [ ] acknowledge_job - [ ] acknowledge_third_party_job - [ ] create_custom_action_type @@ -1394,7 +1394,7 @@ - [ ] list_action_types - [ ] list_pipeline_executions - [X] list_pipelines -- [ ] list_tags_for_resource +- [X] list_tags_for_resource - [ ] list_webhooks - [ ] poll_for_jobs - [ ] poll_for_third_party_jobs @@ -1408,8 +1408,8 @@ - [ ] register_webhook_with_third_party - [ ] retry_stage_execution - [ ] start_pipeline_execution -- [ ] tag_resource -- [ ] untag_resource +- [X] tag_resource +- [X] untag_resource - [X] update_pipeline ## codestar diff --git a/moto/codepipeline/models.py b/moto/codepipeline/models.py index 4a8b89617..556682c10 100644 --- a/moto/codepipeline/models.py +++ b/moto/codepipeline/models.py @@ -192,6 +192,20 @@ class CodePipelineBackend(BaseBackend): for tag in tags: pipeline.tags.update({tag["key"]: tag["value"]}) + def untag_resource(self, arn, tag_keys): + name = arn.split(":")[-1] + pipeline = self.pipelines.get(name) + + if not pipeline: + raise ResourceNotFoundException( + "The account with id '{0}' does not include a pipeline with the name '{1}'".format( + ACCOUNT_ID, name + ) + ) + + for key in tag_keys: + pipeline.tags.pop(key, None) + codepipeline_backends = {} for region in Session().get_available_regions("codepipeline"): diff --git a/moto/codepipeline/responses.py b/moto/codepipeline/responses.py index df1bf220f..0223dfae6 100644 --- a/moto/codepipeline/responses.py +++ b/moto/codepipeline/responses.py @@ -53,3 +53,10 @@ class CodePipelineResponse(BaseResponse): ) return "" + + def untag_resource(self): + self.codepipeline_backend.untag_resource( + self._get_param("resourceArn"), self._get_param("tagKeys") + ) + + return "" diff --git a/tests/test_codepipeline/test_codepipeline.py b/tests/test_codepipeline/test_codepipeline.py index e71e24f76..a40efa05c 100644 --- a/tests/test_codepipeline/test_codepipeline.py +++ b/tests/test_codepipeline/test_codepipeline.py @@ -602,6 +602,52 @@ def test_tag_resource_errors(): ) +@mock_codepipeline +def test_untag_resource(): + client = boto3.client("codepipeline", region_name="us-east-1") + name = "test-pipeline" + create_basic_codepipeline(client, name) + + response = client.list_tags_for_resource( + resourceArn="arn:aws:codepipeline:us-east-1:123456789012:{}".format(name) + ) + response["tags"].should.equal([{"key": "key", "value": "value"}]) + + client.untag_resource( + resourceArn="arn:aws:codepipeline:us-east-1:123456789012:{}".format(name), + tagKeys=["key"], + ) + + response = client.list_tags_for_resource( + resourceArn="arn:aws:codepipeline:us-east-1:123456789012:{}".format(name) + ) + response["tags"].should.have.length_of(0) + + # removing a not existing tag should raise no exception + client.untag_resource( + resourceArn="arn:aws:codepipeline:us-east-1:123456789012:{}".format(name), + tagKeys=["key"], + ) + + +@mock_codepipeline +def test_untag_resource_errors(): + client = boto3.client("codepipeline", region_name="us-east-1") + + with assert_raises(ClientError) as e: + client.untag_resource( + resourceArn="arn:aws:codepipeline:us-east-1:123456789012:not-existing", + tagKeys=["key"], + ) + ex = e.exception + ex.operation_name.should.equal("UntagResource") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("ResourceNotFoundException") + ex.response["Error"]["Message"].should.equal( + "The account with id '123456789012' does not include a pipeline with the name 'not-existing'" + ) + + @mock_iam def get_role_arn(): client = boto3.client("iam", region_name="us-east-1") From 9455ab0e53eb18dcd25bbd37b8fe84d58027eb49 Mon Sep 17 00:00:00 2001 From: gruebel Date: Tue, 24 Dec 2019 13:52:33 +0100 Subject: [PATCH 4/4] Fix Python 2.7 tests --- moto/codepipeline/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/moto/codepipeline/models.py b/moto/codepipeline/models.py index 556682c10..e14c0c2d8 100644 --- a/moto/codepipeline/models.py +++ b/moto/codepipeline/models.py @@ -112,7 +112,7 @@ class CodePipelineBackend(BaseBackend): new_tags = {tag["key"]: tag["value"] for tag in tags} self.pipelines[pipeline["name"]].tags.update(new_tags) - return pipeline, tags + return pipeline, sorted(tags, key=lambda i: i["key"]) def get_pipeline(self, name): codepipeline = self.pipelines.get(name) @@ -174,7 +174,7 @@ class CodePipelineBackend(BaseBackend): tags = [{"key": key, "value": value} for key, value in pipeline.tags.items()] - return tags + return sorted(tags, key=lambda i: i["key"]) def tag_resource(self, arn, tags): name = arn.split(":")[-1]