diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 24009f948..fa9efc21e 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -1263,7 +1263,7 @@ - [ ] update_deployment_group ## codepipeline -2% implemented +5% implemented - [ ] acknowledge_job - [ ] acknowledge_third_party_job - [ ] create_custom_action_type @@ -1275,7 +1275,7 @@ - [ ] disable_stage_transition - [ ] enable_stage_transition - [ ] get_job_details -- [ ] get_pipeline +- [X] get_pipeline - [ ] get_pipeline_execution - [ ] get_pipeline_state - [ ] get_third_party_job_details diff --git a/moto/codepipeline/exceptions.py b/moto/codepipeline/exceptions.py index 82bf6192a..360647058 100644 --- a/moto/codepipeline/exceptions.py +++ b/moto/codepipeline/exceptions.py @@ -8,3 +8,12 @@ class InvalidStructureException(JsonRESTError): super(InvalidStructureException, self).__init__( "InvalidStructureException", message ) + + +class PipelineNotFoundException(JsonRESTError): + code = 400 + + def __init__(self, message): + super(PipelineNotFoundException, self).__init__( + "PipelineNotFoundException", message + ) diff --git a/moto/codepipeline/models.py b/moto/codepipeline/models.py index 98975c242..4ea05298d 100644 --- a/moto/codepipeline/models.py +++ b/moto/codepipeline/models.py @@ -1,21 +1,41 @@ import json +from datetime import datetime from boto3 import Session +from moto.core.utils import iso_8601_datetime_with_milliseconds + from moto.iam.exceptions import IAMNotFoundException from moto.iam import iam_backends -from moto.codepipeline.exceptions import InvalidStructureException +from moto.codepipeline.exceptions import ( + InvalidStructureException, + PipelineNotFoundException, +) from moto.core import BaseBackend, BaseModel DEFAULT_ACCOUNT_ID = "123456789012" class CodePipeline(BaseModel): - def __init__(self, pipeline): + def __init__(self, region, pipeline): self.pipeline = self._add_default_values(pipeline) self.tags = {} + self._arn = "arn:aws:codepipeline:{0}:{1}:{2}".format( + region, DEFAULT_ACCOUNT_ID, pipeline["name"] + ) + self._created = datetime.utcnow() + self._updated = datetime.utcnow() + + @property + def metadata(self): + return { + "pipelineArn": self._arn, + "created": iso_8601_datetime_with_milliseconds(self._created), + "updated": iso_8601_datetime_with_milliseconds(self._updated), + } + def _add_default_values(self, pipeline): for stage in pipeline["stages"]: for action in stage["actions"]: @@ -28,6 +48,8 @@ class CodePipeline(BaseModel): if "inputArtifacts" not in action: action["inputArtifacts"] = [] + return pipeline + class CodePipelineBackend(BaseBackend): def __init__(self): @@ -37,7 +59,7 @@ class CodePipelineBackend(BaseBackend): def iam_backend(self): return iam_backends["global"] - def create_pipeline(self, pipeline, tags): + def create_pipeline(self, region, pipeline, tags): if pipeline["name"] in self.pipelines: raise InvalidStructureException( "A pipeline with the name '{0}' already exists in account '{1}'".format( @@ -64,7 +86,7 @@ class CodePipelineBackend(BaseBackend): "Pipeline has only 1 stage(s). There should be a minimum of 2 stages in a pipeline" ) - self.pipelines[pipeline["name"]] = CodePipeline(pipeline) + self.pipelines[pipeline["name"]] = CodePipeline(region, pipeline) if tags: new_tags = {tag["key"]: tag["value"] for tag in tags} @@ -72,6 +94,18 @@ class CodePipelineBackend(BaseBackend): return pipeline, tags + def get_pipeline(self, name): + codepipeline = self.pipelines.get(name) + + if not codepipeline: + raise PipelineNotFoundException( + "Account '{0}' does not have a pipeline with name '{1}'".format( + DEFAULT_ACCOUNT_ID, name + ) + ) + + return codepipeline.pipeline, codepipeline.metadata + codepipeline_backends = {} for region in Session().get_available_regions("codepipeline"): diff --git a/moto/codepipeline/responses.py b/moto/codepipeline/responses.py index 9bff98bb1..3d41d60a1 100644 --- a/moto/codepipeline/responses.py +++ b/moto/codepipeline/responses.py @@ -11,7 +11,14 @@ class CodePipelineResponse(BaseResponse): def create_pipeline(self): pipeline, tags = self.codepipeline_backend.create_pipeline( - self._get_param("pipeline"), self._get_param("tags") + self.region, self._get_param("pipeline"), self._get_param("tags") ) return json.dumps({"pipeline": pipeline, "tags": tags}) + + def get_pipeline(self): + pipeline, metadata = self.codepipeline_backend.get_pipeline( + self._get_param("name") + ) + + return json.dumps({"pipeline": pipeline, "metadata": metadata}) diff --git a/tests/test_codepipeline/test_codepipeline.py b/tests/test_codepipeline/test_codepipeline.py index f9eb5624c..59273c5f1 100644 --- a/tests/test_codepipeline/test_codepipeline.py +++ b/tests/test_codepipeline/test_codepipeline.py @@ -1,8 +1,10 @@ import json +from datetime import datetime, timezone import boto3 import sure # noqa from botocore.exceptions import ClientError +from freezegun import freeze_time from nose.tools import assert_raises from moto import mock_codepipeline, mock_iam @@ -343,6 +345,134 @@ def test_create_pipeline_errors(): ) +@freeze_time("2019-01-01 12:00:00") +@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"}], + ) + + response = client.get_pipeline(name="test-pipeline") + + 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": "test-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": [], + } + ], + }, + ], + } + ) + response["metadata"].should.equal( + { + "pipelineArn": "arn:aws:codepipeline:us-east-1:123456789012:test-pipeline", + "created": datetime.now(timezone.utc), + "updated": datetime.now(timezone.utc), + } + ) + + +@mock_codepipeline +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") + ex = e.exception + ex.operation_name.should.equal("GetPipeline") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("PipelineNotFoundException") + ex.response["Error"]["Message"].should.equal( + "Account '123456789012' does not have a pipeline with name 'not-existing'" + ) + + @mock_iam def get_role_arn(): iam = boto3.client("iam", region_name="us-east-1")