From e4da4f6cd58d402a46ba10abfa7a602cac3ced7c Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Thu, 10 Aug 2017 19:33:38 -0400 Subject: [PATCH 1/2] Add more error handling to the ECR backend The error messages were copied from `botocore`. New exceptions: RepositoryNotFoundException & ImageNotFoundException. --- moto/ecr/exceptions.py | 22 +++++++++++ moto/ecr/models.py | 55 +++++++++++++++++++--------- moto/ecr/responses.py | 3 +- requirements-dev.txt | 2 +- tests/test_ecr/test_ecr_boto3.py | 63 +++++++++++++++++++++++++------- 5 files changed, 112 insertions(+), 33 deletions(-) create mode 100644 moto/ecr/exceptions.py diff --git a/moto/ecr/exceptions.py b/moto/ecr/exceptions.py new file mode 100644 index 000000000..f7b951b53 --- /dev/null +++ b/moto/ecr/exceptions.py @@ -0,0 +1,22 @@ +from __future__ import unicode_literals +from moto.core.exceptions import RESTError + + +class RepositoryNotFoundException(RESTError): + code = 400 + + def __init__(self, repository_name, registry_id): + super(RepositoryNotFoundException, self).__init__( + error_type="RepositoryNotFoundException", + message="The repository with name '{0}' does not exist in the registry " + "with id '{1}'".format(repository_name, registry_id)) + + +class ImageNotFoundException(RESTError): + code = 400 + + def __init__(self, image_id, repository_name, registry_id): + super(ImageNotFoundException, self).__init__( + error_type="ImageNotFoundException", + message="The image with imageId {0} does not exist within the repository with name '{1}' " + "in the registry with id '{2}'".format(image_id, repository_name, registry_id)) diff --git a/moto/ecr/models.py b/moto/ecr/models.py index b90700ff4..f5b6f24e4 100644 --- a/moto/ecr/models.py +++ b/moto/ecr/models.py @@ -7,6 +7,11 @@ from moto.ec2 import ec2_backends from copy import copy import hashlib +from moto.ecr.exceptions import ImageNotFoundException, RepositoryNotFoundException + + +DEFAULT_REGISTRY_ID = '012345678910' + class BaseObject(BaseModel): @@ -35,14 +40,13 @@ class BaseObject(BaseModel): class Repository(BaseObject): def __init__(self, repository_name): - self.arn = 'arn:aws:ecr:us-east-1:012345678910:repository/{0}'.format( - repository_name) + self.registry_id = DEFAULT_REGISTRY_ID + self.arn = 'arn:aws:ecr:us-east-1:{0}:repository/{1}'.format( + self.registry_id, 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.uri = '{0}.dkr.ecr.us-east-1.amazonaws.com/{1}'.format( + self.registry_id, repository_name) self.images = [] @property @@ -93,7 +97,7 @@ class Repository(BaseObject): class Image(BaseObject): - def __init__(self, tag, manifest, repository, registry_id="012345678910"): + def __init__(self, tag, manifest, repository, registry_id=DEFAULT_REGISTRY_ID): self.image_tag = tag self.image_manifest = manifest self.image_size_in_bytes = 50 * 1024 * 1024 @@ -151,6 +155,11 @@ class ECRBackend(BaseBackend): """ maxResults and nextToken not implemented """ + if repository_names: + for repository_name in repository_names: + if repository_name not in self.repositories: + raise RepositoryNotFoundException(repository_name, registry_id or DEFAULT_REGISTRY_ID) + repositories = [] for repository in self.repositories.values(): # If a registry_id was supplied, ensure this repository matches @@ -170,11 +179,11 @@ class ECRBackend(BaseBackend): 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) + def delete_repository(self, repository_name, registry_id=None): + if repository_name in self.repositories: + return self.repositories.pop(repository_name) else: - raise Exception("{0} is not a repository".format(respository_name)) + raise RepositoryNotFoundException(repository_name, registry_id or DEFAULT_REGISTRY_ID) def list_images(self, repository_name, registry_id=None): """ @@ -198,17 +207,27 @@ class ECRBackend(BaseBackend): if repository_name in self.repositories: repository = self.repositories[repository_name] else: - raise Exception("{0} is not a repository".format(repository_name)) + raise RepositoryNotFoundException(repository_name, registry_id or DEFAULT_REGISTRY_ID) if image_ids: response = set() for image_id in image_ids: - if 'imageDigest' in image_id: - desired_digest = image_id['imageDigest'] - response.update([i for i in repository.images if i.get_image_digest() == desired_digest]) - if 'imageTag' in image_id: - desired_tag = image_id['imageTag'] - response.update([i for i in repository.images if i.image_tag == desired_tag]) + found = False + for image in repository.images: + if (('imageDigest' in image_id and image.get_image_digest() == image_id['imageDigest']) or + ('imageTag' in image_id and image.image_tag == image_id['imageTag'])): + found = True + response.add(image) + if not found: + image_id_representation = "{imageDigest:'%s', imageTag:'%s'}" % ( + image_id.get('imageDigest', 'null'), + image_id.get('imageTag', 'null'), + ) + raise ImageNotFoundException( + image_id=image_id_representation, + repository_name=repository_name, + registry_id=registry_id or DEFAULT_REGISTRY_ID) + else: response = [] for image in repository.images: diff --git a/moto/ecr/responses.py b/moto/ecr/responses.py index 4fa0946b8..6207de4eb 100644 --- a/moto/ecr/responses.py +++ b/moto/ecr/responses.py @@ -45,7 +45,8 @@ class ECRResponse(BaseResponse): def delete_repository(self): repository_str = self._get_param('repositoryName') - repository = self.ecr_backend.delete_repository(repository_str) + registry_id = self._get_param('registryId') + repository = self.ecr_backend.delete_repository(repository_str, registry_id) return json.dumps({ 'repository': repository.response_object }) diff --git a/requirements-dev.txt b/requirements-dev.txt index 52def6ed0..e2f379a59 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,5 +7,5 @@ flake8 freezegun flask boto3>=1.4.4 -botocore>=1.4.28 +botocore>=1.5.77 six diff --git a/tests/test_ecr/test_ecr_boto3.py b/tests/test_ecr/test_ecr_boto3.py index 581906321..67d1a2cab 100644 --- a/tests/test_ecr/test_ecr_boto3.py +++ b/tests/test_ecr/test_ecr_boto3.py @@ -5,9 +5,11 @@ import json from datetime import datetime from random import random +import re import sure # noqa import boto3 +from botocore.exceptions import ClientError from dateutil.tz import tzlocal from moto import mock_ecr @@ -141,19 +143,6 @@ def test_describe_repositories_3(): 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_describe_repositories_with_image(): client = boto3.client('ecr', region_name='us-east-1') @@ -344,6 +333,54 @@ def test_describe_images_by_tag(): image_detail['imageDigest'].should.equal(put_response['imageId']['imageDigest']) +@mock_ecr +def test_describe_repository_that_doesnt_exist(): + client = boto3.client('ecr', region_name='us-east-1') + + error_msg = re.compile( + r".*The repository with name 'repo-that-doesnt-exist' does not exist in the registry with id '123'.*", + re.MULTILINE) + client.describe_repositories.when.called_with( + repositoryNames=['repo-that-doesnt-exist'], + registryId='123', + ).should.throw(ClientError, error_msg) + +@mock_ecr +def test_describe_image_that_doesnt_exist(): + client = boto3.client('ecr', region_name='us-east-1') + client.create_repository(repositoryName='test_repository') + + error_msg1 = re.compile( + r".*The image with imageId {imageDigest:'null', imageTag:'testtag'} does not exist within " + r"the repository with name 'test_repository' in the registry with id '123'.*", + re.MULTILINE) + + client.describe_images.when.called_with( + repositoryName='test_repository', imageIds=[{'imageTag': 'testtag'}], registryId='123', + ).should.throw(ClientError, error_msg1) + + error_msg2 = re.compile( + r".*The repository with name 'repo-that-doesnt-exist' does not exist in the registry with id '123'.*", + re.MULTILINE) + client.describe_images.when.called_with( + repositoryName='repo-that-doesnt-exist', imageIds=[{'imageTag': 'testtag'}], registryId='123', + ).should.throw(ClientError, error_msg2) + + +@mock_ecr +def test_delete_repository_that_doesnt_exist(): + client = boto3.client('ecr', region_name='us-east-1') + + error_msg = re.compile( + r".*The repository with name 'repo-that-doesnt-exist' does not exist in the registry with id '123'.*", + re.MULTILINE) + + client.delete_repository.when.called_with( + repositoryName='repo-that-doesnt-exist', + registryId='123').should.throw( + ClientError, error_msg) + + @mock_ecr def test_describe_images_by_digest(): client = boto3.client('ecr', region_name='us-east-1') From 973264d9403a3512048ce0239a15c6c96a00dcf0 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Thu, 10 Aug 2017 19:54:00 -0400 Subject: [PATCH 2/2] Convert struct argument to bytestring to avoid errors with Python 2.7.6 --- moto/sqs/models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/moto/sqs/models.py b/moto/sqs/models.py index f6657269c..e6209b4ba 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -59,6 +59,7 @@ class Message(BaseModel): return str.encode('utf-8') return str md5 = hashlib.md5() + struct_format = "!I".encode('ascii') # ensure it's a bytestring for name in sorted(self.message_attributes.keys()): attr = self.message_attributes[name] data_type = attr['data_type'] @@ -67,10 +68,10 @@ class Message(BaseModel): # Each part of each attribute is encoded right after it's # own length is packed into a 4-byte integer # 'timestamp' -> b'\x00\x00\x00\t' - encoded += struct.pack("!I", len(utf8(name))) + utf8(name) + encoded += struct.pack(struct_format, len(utf8(name))) + utf8(name) # The datatype is additionally given a final byte # representing which type it is - encoded += struct.pack("!I", len(data_type)) + utf8(data_type) + encoded += struct.pack(struct_format, len(data_type)) + utf8(data_type) encoded += TRANSPORT_TYPE_ENCODINGS[data_type] if data_type == 'String' or data_type == 'Number': @@ -86,7 +87,7 @@ class Message(BaseModel): # MD5 so as not to break client softwre return('deadbeefdeadbeefdeadbeefdeadbeef') - encoded += struct.pack("!I", len(utf8(value))) + utf8(value) + encoded += struct.pack(struct_format, len(utf8(value))) + utf8(value) md5.update(encoded) return md5.hexdigest()