From b2c44ce50d0d03f7280d706922952b45ed73e602 Mon Sep 17 00:00:00 2001 From: gruebel Date: Sun, 15 Dec 2019 17:28:59 +0100 Subject: [PATCH] Add codepipeline.update_pipeline --- IMPLEMENTATION_COVERAGE.md | 4 +- moto/codepipeline/exceptions.py | 9 + moto/codepipeline/models.py | 25 +- moto/codepipeline/responses.py | 7 + tests/test_codepipeline/test_codepipeline.py | 227 ++++++++++++++++++- 5 files changed, 267 insertions(+), 5 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index fa9efc21e..297e842eb 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -1263,7 +1263,7 @@ - [ ] update_deployment_group ## codepipeline -5% implemented +8% implemented - [ ] acknowledge_job - [ ] acknowledge_third_party_job - [ ] create_custom_action_type @@ -1299,7 +1299,7 @@ - [ ] start_pipeline_execution - [ ] tag_resource - [ ] untag_resource -- [ ] update_pipeline +- [X] update_pipeline ## codestar 0% implemented diff --git a/moto/codepipeline/exceptions.py b/moto/codepipeline/exceptions.py index 360647058..e455298cd 100644 --- a/moto/codepipeline/exceptions.py +++ b/moto/codepipeline/exceptions.py @@ -17,3 +17,12 @@ class PipelineNotFoundException(JsonRESTError): super(PipelineNotFoundException, self).__init__( "PipelineNotFoundException", message ) + + +class ResourceNotFoundException(JsonRESTError): + code = 400 + + def __init__(self, message): + super(ResourceNotFoundException, self).__init__( + "ResourceNotFoundException", message + ) diff --git a/moto/codepipeline/models.py b/moto/codepipeline/models.py index 4ea05298d..de89f21f3 100644 --- a/moto/codepipeline/models.py +++ b/moto/codepipeline/models.py @@ -11,6 +11,7 @@ from moto.iam import iam_backends from moto.codepipeline.exceptions import ( InvalidStructureException, PipelineNotFoundException, + ResourceNotFoundException, ) from moto.core import BaseBackend, BaseModel @@ -19,7 +20,10 @@ DEFAULT_ACCOUNT_ID = "123456789012" class CodePipeline(BaseModel): def __init__(self, region, pipeline): - self.pipeline = self._add_default_values(pipeline) + # the version number for a new pipeline is always 1 + pipeline["version"] = 1 + + self.pipeline = self.add_default_values(pipeline) self.tags = {} self._arn = "arn:aws:codepipeline:{0}:{1}:{2}".format( @@ -36,7 +40,7 @@ class CodePipeline(BaseModel): "updated": iso_8601_datetime_with_milliseconds(self._updated), } - def _add_default_values(self, pipeline): + def add_default_values(self, pipeline): for stage in pipeline["stages"]: for action in stage["actions"]: if "runOrder" not in action: @@ -106,6 +110,23 @@ class CodePipelineBackend(BaseBackend): return codepipeline.pipeline, codepipeline.metadata + def update_pipeline(self, pipeline): + codepipeline = self.pipelines.get(pipeline["name"]) + + if not codepipeline: + raise ResourceNotFoundException( + "The account with id '{0}' does not include a pipeline with the name '{1}'".format( + DEFAULT_ACCOUNT_ID, pipeline["name"] + ) + ) + + # version number is auto incremented + pipeline["version"] = codepipeline.pipeline["version"] + 1 + codepipeline._updated = datetime.utcnow() + codepipeline.pipeline = codepipeline.add_default_values(pipeline) + + return codepipeline.pipeline + codepipeline_backends = {} for region in Session().get_available_regions("codepipeline"): diff --git a/moto/codepipeline/responses.py b/moto/codepipeline/responses.py index 3d41d60a1..c1aa4dc76 100644 --- a/moto/codepipeline/responses.py +++ b/moto/codepipeline/responses.py @@ -22,3 +22,10 @@ class CodePipelineResponse(BaseResponse): ) return json.dumps({"pipeline": pipeline, "metadata": metadata}) + + def update_pipeline(self): + pipeline = self.codepipeline_backend.update_pipeline( + self._get_param("pipeline") + ) + + return json.dumps({"pipeline": pipeline}) diff --git a/tests/test_codepipeline/test_codepipeline.py b/tests/test_codepipeline/test_codepipeline.py index 59273c5f1..f4d7e1ccb 100644 --- a/tests/test_codepipeline/test_codepipeline.py +++ b/tests/test_codepipeline/test_codepipeline.py @@ -110,6 +110,7 @@ def test_create_pipeline(): ], }, ], + "version": 1, } ) response["tags"].should.equal([{"key": "key", "value": "value"}]) @@ -447,6 +448,7 @@ def test_get_pipeline(): ], }, ], + "version": 1, } ) response["metadata"].should.equal( @@ -463,7 +465,7 @@ def test_get_pipeline_errors(): client = boto3.client("codepipeline", region_name="us-east-1") with assert_raises(ClientError) as e: - response = client.get_pipeline(name="not-existing") + client.get_pipeline(name="not-existing") ex = e.exception ex.operation_name.should.equal("GetPipeline") ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) @@ -473,6 +475,229 @@ 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() + with freeze_time("2019-01-01 12:00:00"): + created_time = datetime.now(timezone.utc) + 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"}], + ) + + with freeze_time("2019-01-02 12:00:00"): + updated_time = datetime.now(timezone.utc) + response = client.update_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": "different-bucket", + "S3ObjectKey": "test-object", + }, + "outputArtifacts": [{"name": "artifact"},], + }, + ], + }, + { + "name": "Stage-2", + "actions": [ + { + "name": "Action-1", + "actionTypeId": { + "category": "Approval", + "owner": "AWS", + "provider": "Manual", + "version": "1", + }, + }, + ], + }, + ], + } + ) + + response["pipeline"].should.equal( + { + "name": "test-pipeline", + "roleArn": "arn:aws:iam::123456789012:role/test-role", + "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", + }, + "runOrder": 1, + "configuration": { + "S3Bucket": "different-bucket", + "S3ObjectKey": "test-object", + }, + "outputArtifacts": [{"name": "artifact"}], + "inputArtifacts": [], + } + ], + }, + { + "name": "Stage-2", + "actions": [ + { + "name": "Action-1", + "actionTypeId": { + "category": "Approval", + "owner": "AWS", + "provider": "Manual", + "version": "1", + }, + "runOrder": 1, + "configuration": {}, + "outputArtifacts": [], + "inputArtifacts": [], + } + ], + }, + ], + "version": 2, + } + ) + + response = client.get_pipeline(name="test-pipeline") + response["metadata"].should.equal( + { + "pipelineArn": "arn:aws:codepipeline:us-east-1:123456789012:test-pipeline", + "created": created_time, + "updated": updated_time, + } + ) + + +@mock_codepipeline +def test_update_pipeline_errors(): + client = boto3.client("codepipeline", region_name="us-east-1") + + with assert_raises(ClientError) as e: + client.update_pipeline( + pipeline={ + "name": "not-existing", + "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", + }, + }, + ], + }, + ], + } + ) + ex = e.exception + ex.operation_name.should.equal("UpdatePipeline") + 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")