diff --git a/moto/__init__.py b/moto/__init__.py index c93719cb..d6f84db5 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -14,6 +14,7 @@ from .datapipeline import mock_datapipeline, mock_datapipeline_deprecated # fla from .dynamodb import mock_dynamodb, mock_dynamodb_deprecated # flake8: noqa from .dynamodb2 import mock_dynamodb2, mock_dynamodb2_deprecated # flake8: noqa from .ec2 import mock_ec2, mock_ec2_deprecated # flake8: noqa +from .ecr import mock_ecr, mock_ecr_deprecated # flake8: noqa from .ecs import mock_ecs, mock_ecs_deprecated # flake8: noqa from .elb import mock_elb, mock_elb_deprecated # flake8: noqa from .emr import mock_emr, mock_emr_deprecated # flake8: noqa diff --git a/moto/backends.py b/moto/backends.py index eae94db7..0af4ae2e 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -10,6 +10,7 @@ from moto.datapipeline import datapipeline_backends from moto.dynamodb import dynamodb_backends from moto.dynamodb2 import dynamodb_backends2 from moto.ec2 import ec2_backends +from moto.ecr import ecr_backends from moto.ecs import ecs_backends from moto.elb import elb_backends from moto.emr import emr_backends @@ -39,6 +40,7 @@ BACKENDS = { 'dynamodb': dynamodb_backends, 'dynamodb2': dynamodb_backends2, 'ec2': ec2_backends, + 'ecr': ecr_backends, 'ecs': ecs_backends, 'elb': elb_backends, 'events': events_backends, diff --git a/moto/ecr/__init__.py b/moto/ecr/__init__.py new file mode 100644 index 00000000..56b2cacb --- /dev/null +++ b/moto/ecr/__init__.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals +from .models import ecr_backends +from ..core.models import base_decorator, deprecated_base_decorator + +ecr_backend = ecr_backends['us-east-1'] +mock_ecr = base_decorator(ecr_backends) +mock_ecr_deprecated = deprecated_base_decorator(ecr_backends) diff --git a/moto/ecr/models.py b/moto/ecr/models.py new file mode 100644 index 00000000..82ce2ebd --- /dev/null +++ b/moto/ecr/models.py @@ -0,0 +1,221 @@ +from __future__ import unicode_literals +# from datetime import datetime +from random import random + +from moto.core import BaseBackend, BaseModel +from moto.ec2 import ec2_backends +from copy import copy +import hashlib + + +class BaseObject(BaseModel): + + def camelCase(self, key): + words = [] + for i, word in enumerate(key.split('_')): + if i > 0: + words.append(word.title()) + else: + words.append(word) + return ''.join(words) + + def gen_response_object(self): + response_object = copy(self.__dict__) + for key, value in response_object.items(): + if '_' in key: + response_object[self.camelCase(key)] = value + del response_object[key] + return response_object + + @property + def response_object(self): + return self.gen_response_object() + + +class Repository(BaseObject): + + def __init__(self, repository_name): + self.arn = 'arn:aws:ecr:us-east-1:012345678910:repository/{0}'.format( + repository_name) + self.name = repository_name + # self.created = datetime.utcnow() + self.uri = '012345678910.dkr.ecr.us-east-1.amazonaws.com/{0}'.format( + repository_name + ) + self.registry_id = '012345678910' + self.images = [] + + @property + def physical_resource_id(self): + return self.name + + @property + def response_object(self): + response_object = self.gen_response_object() + + response_object['registryId'] = self.registry_id + response_object['repositoryArn'] = self.arn + response_object['repositoryName'] = self.name + response_object['repositoryUri'] = self.uri + # response_object['createdAt'] = self.created + del response_object['arn'], response_object['name'] + return response_object + + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + + ecr_backend = ecr_backends[region_name] + return ecr_backend.create_repository( + # RepositoryName is optional in CloudFormation, thus create a random + # name if necessary + repository_name=properties.get( + 'RepositoryName', 'ecrrepository{0}'.format(int(random() * 10 ** 6))), + ) + + @classmethod + def update_from_cloudformation_json(cls, original_resource, new_resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + + if original_resource.name != properties['RepositoryName']: + ecr_backend = ecr_backends[region_name] + ecr_backend.delete_cluster(original_resource.arn) + return ecr_backend.create_repository( + # RepositoryName is optional in CloudFormation, thus create a + # random name if necessary + repository_name=properties.get( + 'RepositoryName', 'RepositoryName{0}'.format(int(random() * 10 ** 6))), + ) + else: + # no-op when nothing changed between old and new resources + return original_resource + + +class Image(BaseObject): + + def __init__(self, tag, manifest, repository, registry_id="012345678910"): + self.image_tag = tag + self.image_manifest = manifest + self.image_size_in_bytes = 50 * 1024 * 1024 + self.repository = repository + self.registry_id = registry_id + self.image_digest = None + self.image_pushed_at = None + + def _create_digest(self): + image_contents = 'docker_image{0}'.format(int(random() * 10 ** 6)) + self.image_digest = "sha256:%s" % hashlib.sha256(image_contents.encode('utf-8')).hexdigest() + + def get_image_digest(self): + if not self.image_digest: + self._create_digest() + return self.image_digest + + @property + def response_object(self): + response_object = self.gen_response_object() + response_object['imageId'] = {} + response_object['imageId']['imageTag'] = self.image_tag + response_object['imageId']['imageDigest'] = self.get_image_digest() + response_object['imageManifest'] = self.image_manifest + response_object['repositoryName'] = self.repository + response_object['registryId'] = self.registry_id + return response_object + + @property + def response_list_object(self): + response_object = self.gen_response_object() + response_object['imageTag'] = self.image_tag + response_object['imageDigest'] = "i don't know" + return response_object + + @property + def response_describe_object(self): + response_object = self.gen_response_object() + response_object['imageTags'] = [self.image_tag] + response_object['imageDigest'] = self.get_image_digest() + response_object['imageManifest'] = self.image_manifest + response_object['repositoryName'] = self.repository + response_object['registryId'] = self.registry_id + response_object['imageSizeInBytes'] = self.image_size_in_bytes + response_object['imagePushedAt'] = '2017-05-09' + return response_object + + +class ECRBackend(BaseBackend): + + def __init__(self): + self.repositories = {} + + def describe_repositories(self, registry_id=None, repository_names=None): + """ + maxResults and nextToken not implemented + """ + repositories = [] + for repository in self.repositories.values(): + # If a registry_id was supplied, ensure this repository matches + if registry_id: + if repository.registry_id != registry_id: + continue + # If a list of repository names was supplied, esure this repository + # is in that list + if repository_names: + if repository.name not in repository_names: + continue + repositories.append(repository.response_object) + return repositories + + def create_repository(self, repository_name): + repository = Repository(repository_name) + self.repositories[repository_name] = repository + return repository + + def delete_repository(self, respository_name, registry_id=None): + if respository_name in self.repositories: + return self.repositories.pop(respository_name) + else: + raise Exception("{0} is not a repository".format(respository_name)) + + def list_images(self, repository_name, registry_id=None): + """ + maxResults and filtering not implemented + """ + images = [] + for repository in self.repositories.values(): + if repository_name: + if repository.name != repository_name: + continue + if registry_id: + if repository.registry_id != registry_id: + continue + + for image in repository.images: + images.append(image) + return images + + def describe_images(self, repository_name, registry_id=None, image_id=None): + + if repository_name in self.repositories: + repository = self.repositories[repository_name] + else: + raise Exception("{0} is not a repository".format(repository_name)) + + response = [] + for image in repository.images: + response.append(image) + return response + + def put_image(self, repository_name, image_manifest, image_tag): + if repository_name in self.repositories: + repository = self.repositories[repository_name] + else: + raise Exception("{0} is not a repository".format(repository_name)) + + image = Image(image_tag, image_manifest, repository_name) + repository.images.append(image) + return image + + +ecr_backends = {} +for region, ec2_backend in ec2_backends.items(): + ecr_backends[region] = ECRBackend() diff --git a/moto/ecr/responses.py b/moto/ecr/responses.py new file mode 100644 index 00000000..f8b1606c --- /dev/null +++ b/moto/ecr/responses.py @@ -0,0 +1,150 @@ +from __future__ import unicode_literals +import json + +from moto.core.responses import BaseResponse +from .models import ecr_backends + + +class ECRResponse(BaseResponse): + + @property + def ecr_backend(self): + return ecr_backends[self.region] + + @property + def request_params(self): + try: + return json.loads(self.body) + except ValueError: + return {} + + def _get_param(self, param): + return self.request_params.get(param, None) + + def create_repository(self): + repository_name = self._get_param('repositoryName') + if repository_name is None: + repository_name = 'default' + repository = self.ecr_backend.create_repository(repository_name) + return json.dumps({ + 'repository': repository.response_object + }) + + def describe_repositories(self): + describe_repositories_name = self._get_param('repositoryNames') + registry_id = self._get_param('registryId') + + repositories = self.ecr_backend.describe_repositories( + repository_names=describe_repositories_name, registry_id=registry_id) + return json.dumps({ + 'repositories': repositories, + 'failures': [] + }) + + def delete_repository(self): + repository_str = self._get_param('repositoryName') + repository = self.ecr_backend.delete_repository(repository_str) + return json.dumps({ + 'repository': repository.response_object + }) + + def put_image(self): + repository_str = self._get_param('repositoryName') + image_manifest = self._get_param('imageManifest') + image_tag = self._get_param('imageTag') + image = self.ecr_backend.put_image(repository_str, image_manifest, image_tag) + + return json.dumps({ + 'image': image.response_object + }) + + def list_images(self): + repository_str = self._get_param('repositoryName') + registry_id = self._get_param('registryId') + images = self.ecr_backend.list_images(repository_str, registry_id) + return json.dumps({ + 'imageIds': [image.response_list_object for image in images], + }) + + def describe_images(self): + repository_str = self._get_param('repositoryName') + registry_id = self._get_param('registryId') + images = self.ecr_backend.describe_images(repository_str, registry_id) + return json.dumps({ + 'imageDetails': [image.response_describe_object for image in images], + }) + + def batch_check_layer_availability(self): + if self.is_not_dryrun('BatchCheckLayerAvailability'): + raise NotImplementedError( + 'ECR.batch_check_layer_availability is not yet implemented') + + def batch_delete_image(self): + if self.is_not_dryrun('BatchDeleteImage'): + raise NotImplementedError( + 'ECR.batch_delete_image is not yet implemented') + + def batch_get_image(self): + if self.is_not_dryrun('BatchGetImage'): + raise NotImplementedError( + 'ECR.batch_get_image is not yet implemented') + + def can_paginate(self): + if self.is_not_dryrun('CanPaginate'): + raise NotImplementedError( + 'ECR.can_paginate is not yet implemented') + + def complete_layer_upload(self): + if self.is_not_dryrun('CompleteLayerUpload'): + raise NotImplementedError( + 'ECR.complete_layer_upload is not yet implemented') + + def delete_repository_policy(self): + if self.is_not_dryrun('DeleteRepositoryPolicy'): + raise NotImplementedError( + 'ECR.delete_repository_policy is not yet implemented') + + def generate_presigned_url(self): + if self.is_not_dryrun('GeneratePresignedUrl'): + raise NotImplementedError( + 'ECR.generate_presigned_url is not yet implemented') + + def get_authorization_token(self): + if self.is_not_dryrun('GetAuthorizationToken'): + raise NotImplementedError( + 'ECR.get_authorization_token is not yet implemented') + + def get_download_url_for_layer(self): + if self.is_not_dryrun('GetDownloadUrlForLayer'): + raise NotImplementedError( + 'ECR.get_download_url_for_layer is not yet implemented') + + def get_paginator(self): + if self.is_not_dryrun('GetPaginator'): + raise NotImplementedError( + 'ECR.get_paginator is not yet implemented') + + def get_repository_policy(self): + if self.is_not_dryrun('GetRepositoryPolicy'): + raise NotImplementedError( + 'ECR.get_repository_policy is not yet implemented') + + def get_waiter(self): + if self.is_not_dryrun('GetWaiter'): + raise NotImplementedError( + 'ECR.get_waiter is not yet implemented') + + def initiate_layer_upload(self): + if self.is_not_dryrun('InitiateLayerUpload'): + raise NotImplementedError( + 'ECR.initiate_layer_upload is not yet implemented') + + def set_repository_policy(self): + if self.is_not_dryrun('SetRepositoryPolicy'): + raise NotImplementedError( + 'ECR.set_repository_policy is not yet implemented') + + def upload_layer_part(self): + if self.is_not_dryrun('UploadLayerPart'): + raise NotImplementedError( + 'ECR.upload_layer_part is not yet implemented') diff --git a/moto/ecr/urls.py b/moto/ecr/urls.py new file mode 100644 index 00000000..86b8a8db --- /dev/null +++ b/moto/ecr/urls.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from .responses import ECRResponse + +url_bases = [ + "https?://ecr.(.+).amazonaws.com", +] + +url_paths = { + '{0}/$': ECRResponse.dispatch, +} diff --git a/tests/test_ecr/test_ecr_boto3.py b/tests/test_ecr/test_ecr_boto3.py new file mode 100644 index 00000000..1191c42d --- /dev/null +++ b/tests/test_ecr/test_ecr_boto3.py @@ -0,0 +1,315 @@ +from __future__ import unicode_literals + +import hashlib +import json +from random import random + +import sure # noqa + +import boto3 + +from moto import mock_ecr + + +def _create_image_digest(contents=None): + if not contents: + contents = 'docker_image{0}'.format(int(random() * 10 ** 6)) + return "sha256:%s" % hashlib.sha256(contents.encode('utf-8')).hexdigest() + + +def _create_image_manifest(): + return { + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": + { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 7023, + "digest": _create_image_digest("config") + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 32654, + "digest": _create_image_digest("layer1") + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 16724, + "digest": _create_image_digest("layer2") + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 73109, + "digest": _create_image_digest("layer3") + } + ] + } + + +@mock_ecr +def test_create_repository(): + client = boto3.client('ecr', region_name='us-east-1') + response = client.create_repository( + repositoryName='test_ecr_repository' + ) + response['repository']['repositoryName'].should.equal('test_ecr_repository') + response['repository']['repositoryArn'].should.equal( + 'arn:aws:ecr:us-east-1:012345678910:repository/test_ecr_repository') + response['repository']['registryId'].should.equal('012345678910') + response['repository']['repositoryUri'].should.equal( + '012345678910.dkr.ecr.us-east-1.amazonaws.com/test_ecr_repository') + # response['repository']['createdAt'].should.equal(0) + + +@mock_ecr +def test_describe_repositories(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository1' + ) + _ = client.create_repository( + repositoryName='test_repository0' + ) + response = client.describe_repositories() + len(response['repositories']).should.equal(2) + + respository_arns = ['arn:aws:ecr:us-east-1:012345678910:repository/test_repository1', + 'arn:aws:ecr:us-east-1:012345678910:repository/test_repository0'] + set([response['repositories'][0]['repositoryArn'], + response['repositories'][1]['repositoryArn']]).should.equal(set(respository_arns)) + + respository_uris = ['012345678910.dkr.ecr.us-east-1.amazonaws.com/test_repository1', + '012345678910.dkr.ecr.us-east-1.amazonaws.com/test_repository0'] + set([response['repositories'][0]['repositoryUri'], + response['repositories'][1]['repositoryUri']]).should.equal(set(respository_uris)) + + +@mock_ecr +def test_describe_repositories_1(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository1' + ) + _ = client.create_repository( + repositoryName='test_repository0' + ) + response = client.describe_repositories(registryId='012345678910') + len(response['repositories']).should.equal(2) + + respository_arns = ['arn:aws:ecr:us-east-1:012345678910:repository/test_repository1', + 'arn:aws:ecr:us-east-1:012345678910:repository/test_repository0'] + set([response['repositories'][0]['repositoryArn'], + response['repositories'][1]['repositoryArn']]).should.equal(set(respository_arns)) + + respository_uris = ['012345678910.dkr.ecr.us-east-1.amazonaws.com/test_repository1', + '012345678910.dkr.ecr.us-east-1.amazonaws.com/test_repository0'] + set([response['repositories'][0]['repositoryUri'], + response['repositories'][1]['repositoryUri']]).should.equal(set(respository_uris)) + + +@mock_ecr +def test_describe_repositories_2(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository1' + ) + _ = client.create_repository( + repositoryName='test_repository0' + ) + response = client.describe_repositories(registryId='109876543210') + len(response['repositories']).should.equal(0) + + +@mock_ecr +def test_describe_repositories_3(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository1' + ) + _ = client.create_repository( + repositoryName='test_repository0' + ) + response = client.describe_repositories(repositoryNames=['test_repository1']) + len(response['repositories']).should.equal(1) + respository_arn = 'arn:aws:ecr:us-east-1:012345678910:repository/test_repository1' + response['repositories'][0]['repositoryArn'].should.equal(respository_arn) + + respository_uri = '012345678910.dkr.ecr.us-east-1.amazonaws.com/test_repository1' + response['repositories'][0]['repositoryUri'].should.equal(respository_uri) + + +@mock_ecr +def test_describe_repositories_4(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository1' + ) + _ = client.create_repository( + repositoryName='test_repository0' + ) + response = client.describe_repositories(repositoryNames=['not_a_valid_name']) + len(response['repositories']).should.equal(0) + + +@mock_ecr +def test_delete_repository(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository' + ) + response = client.delete_repository(repositoryName='test_repository') + response['repository']['repositoryName'].should.equal('test_repository') + response['repository']['repositoryArn'].should.equal( + 'arn:aws:ecr:us-east-1:012345678910:repository/test_repository') + response['repository']['registryId'].should.equal('012345678910') + response['repository']['repositoryUri'].should.equal( + '012345678910.dkr.ecr.us-east-1.amazonaws.com/test_repository') + # response['repository']['createdAt'].should.equal(0) + + response = client.describe_repositories() + len(response['repositories']).should.equal(0) + + +@mock_ecr +def test_put_image(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository' + ) + response = client.put_image( + repositoryName='test_repository', + imageManifest=json.dumps(_create_image_manifest()), + imageTag='latest' + ) + + response['image']['repositoryName'].should.equal('test_repository') + response['image']['imageId']['imageTag'].should.equal('latest') + + +@mock_ecr +def test_list_images(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository_1' + ) + + _ = client.create_repository( + repositoryName='test_repository_2' + ) + + _ = client.put_image( + repositoryName='test_repository_1', + imageManifest=json.dumps(_create_image_manifest()), + imageTag='latest' + ) + + _ = client.put_image( + repositoryName='test_repository_1', + imageManifest=json.dumps(_create_image_manifest()), + imageTag='v1' + ) + + _ = client.put_image( + repositoryName='test_repository_1', + imageManifest=json.dumps(_create_image_manifest()), + imageTag='v2' + ) + + _ = client.put_image( + repositoryName='test_repository_2', + imageManifest=json.dumps(_create_image_manifest()), + imageTag='oldest' + ) + + response = client.list_images(repositoryName='test_repository_1') + type(response['imageIds']).should.be(list) + len(response['imageIds']).should.be(3) + + image_tags = ['latest', 'v1', 'v2'] + set([response['imageIds'][0]['imageTag'], + response['imageIds'][1]['imageTag'], + response['imageIds'][2]['imageTag']]).should.equal(set(image_tags)) + + response = client.list_images(repositoryName='test_repository_2') + type(response['imageIds']).should.be(list) + len(response['imageIds']).should.be(1) + response['imageIds'][0]['imageTag'].should.equal('oldest') + + response = client.list_images(repositoryName='test_repository_2', registryId='109876543210') + type(response['imageIds']).should.be(list) + len(response['imageIds']).should.be(0) + + +@mock_ecr +def test_describe_images(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository' + ) + + _ = client.put_image( + repositoryName='test_repository', + imageManifest=json.dumps(_create_image_manifest()), + imageTag='latest' + ) + + _ = client.put_image( + repositoryName='test_repository', + imageManifest=json.dumps(_create_image_manifest()), + imageTag='v1' + ) + + _ = client.put_image( + repositoryName='test_repository', + imageManifest=json.dumps(_create_image_manifest()), + imageTag='v2' + ) + + response = client.describe_images(repositoryName='test_repository') + type(response['imageDetails']).should.be(list) + len(response['imageDetails']).should.be(3) + + response['imageDetails'][0]['imageDigest'].should.contain("sha") + response['imageDetails'][1]['imageDigest'].should.contain("sha") + response['imageDetails'][2]['imageDigest'].should.contain("sha") + + response['imageDetails'][0]['registryId'].should.equal("012345678910") + response['imageDetails'][1]['registryId'].should.equal("012345678910") + response['imageDetails'][2]['registryId'].should.equal("012345678910") + + response['imageDetails'][0]['repositoryName'].should.equal("test_repository") + response['imageDetails'][1]['repositoryName'].should.equal("test_repository") + response['imageDetails'][2]['repositoryName'].should.equal("test_repository") + + len(response['imageDetails'][0]['imageTags']).should.be(1) + len(response['imageDetails'][1]['imageTags']).should.be(1) + len(response['imageDetails'][2]['imageTags']).should.be(1) + + image_tags = ['latest', 'v1', 'v2'] + set([response['imageDetails'][0]['imageTags'][0], + response['imageDetails'][1]['imageTags'][0], + response['imageDetails'][2]['imageTags'][0]]).should.equal(set(image_tags)) + + response['imageDetails'][0]['imageSizeInBytes'].should.equal(52428800) + response['imageDetails'][1]['imageSizeInBytes'].should.equal(52428800) + response['imageDetails'][2]['imageSizeInBytes'].should.equal(52428800) + + +@mock_ecr +def test_put_image(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository' + ) + + response = client.put_image( + repositoryName='test_repository', + imageManifest=json.dumps(_create_image_manifest()), + imageTag='latest' + ) + + response['image']['imageId']['imageTag'].should.equal('latest') + response['image']['imageId']['imageDigest'].should.contain("sha") + response['image']['repositoryName'].should.equal('test_repository') + response['image']['registryId'].should.equal('012345678910') \ No newline at end of file