From b855fee2e4109eab04a9d55699ffeffb1ca51df6 Mon Sep 17 00:00:00 2001 From: Mike Bjerkness Date: Sat, 30 Dec 2017 20:39:23 -0600 Subject: [PATCH] Add batch_get_image support for ECR (#1406) * Add batch_get_image for ECR * Add tests for batch_get_image * Add tests for batch_get_image * Undo local commits * Undo local commits * Adding object representation for batch_get_image * Update responses. Add a couple more tests. --- moto/ecr/models.py | 52 ++++++++++++-- moto/ecr/responses.py | 10 ++- tests/test_ecr/test_ecr_boto3.py | 116 ++++++++++++++++++++++++++++++- 3 files changed, 170 insertions(+), 8 deletions(-) diff --git a/moto/ecr/models.py b/moto/ecr/models.py index f5b6f24e4..e20c550c9 100644 --- a/moto/ecr/models.py +++ b/moto/ecr/models.py @@ -1,14 +1,14 @@ from __future__ import unicode_literals -# from datetime import datetime + +import hashlib +from copy import copy from random import random from moto.core import BaseBackend, BaseModel from moto.ec2 import ec2_backends -from copy import copy -import hashlib - from moto.ecr.exceptions import ImageNotFoundException, RepositoryNotFoundException +from botocore.exceptions import ParamValidationError DEFAULT_REGISTRY_ID = '012345678910' @@ -145,6 +145,17 @@ class Image(BaseObject): response_object['imagePushedAt'] = '2017-05-09' return response_object + @property + def response_batch_get_image(self): + 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 + class ECRBackend(BaseBackend): @@ -245,6 +256,39 @@ class ECRBackend(BaseBackend): repository.images.append(image) return image + def batch_get_image(self, repository_name, registry_id=None, image_ids=None, accepted_media_types=None): + if repository_name in self.repositories: + repository = self.repositories[repository_name] + else: + raise RepositoryNotFoundException(repository_name, registry_id or DEFAULT_REGISTRY_ID) + + if not image_ids: + raise ParamValidationError(msg='Missing required parameter in input: "imageIds"') + + response = { + 'images': [], + 'failures': [], + } + + for image_id in image_ids: + 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['images'].append(image.response_batch_get_image) + + if not found: + response['failures'].append({ + 'imageId': { + 'imageTag': image_id.get('imageTag', 'null') + }, + 'failureCode': 'ImageNotFound', + 'failureReason': 'Requested image not found' + }) + + return response + ecr_backends = {} for region, ec2_backend in ec2_backends.items(): diff --git a/moto/ecr/responses.py b/moto/ecr/responses.py index 6207de4eb..ca45c63c9 100644 --- a/moto/ecr/responses.py +++ b/moto/ecr/responses.py @@ -89,9 +89,13 @@ class ECRResponse(BaseResponse): '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') + repository_str = self._get_param('repositoryName') + registry_id = self._get_param('registryId') + image_ids = self._get_param('imageIds') + accepted_media_types = self._get_param('acceptedMediaTypes') + + response = self.ecr_backend.batch_get_image(repository_str, registry_id, image_ids, accepted_media_types) + return json.dumps(response) def can_paginate(self): if self.is_not_dryrun('CanPaginate'): diff --git a/tests/test_ecr/test_ecr_boto3.py b/tests/test_ecr/test_ecr_boto3.py index 00628e22f..b4497ef60 100644 --- a/tests/test_ecr/test_ecr_boto3.py +++ b/tests/test_ecr/test_ecr_boto3.py @@ -9,7 +9,7 @@ import re import sure # noqa import boto3 -from botocore.exceptions import ClientError +from botocore.exceptions import ClientError, ParamValidationError from dateutil.tz import tzlocal from moto import mock_ecr @@ -445,3 +445,117 @@ def test_get_authorization_token_explicit_regions(): } ]) + + +@mock_ecr +def test_batch_get_image(): + 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.batch_get_image( + repositoryName='test_repository', + imageIds=[ + { + 'imageTag': 'v2' + }, + ], + ) + + type(response['images']).should.be(list) + len(response['images']).should.be(1) + + response['images'][0]['imageManifest'].should.contain("vnd.docker.distribution.manifest.v2+json") + response['images'][0]['registryId'].should.equal("012345678910") + response['images'][0]['repositoryName'].should.equal("test_repository") + + response['images'][0]['imageId']['imageTag'].should.equal("v2") + response['images'][0]['imageId']['imageDigest'].should.contain("sha") + + type(response['failures']).should.be(list) + len(response['failures']).should.be(0) + + +@mock_ecr +def test_batch_get_image_that_doesnt_exist(): + 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.batch_get_image( + repositoryName='test_repository', + imageIds=[ + { + 'imageTag': 'v5' + }, + ], + ) + + type(response['images']).should.be(list) + len(response['images']).should.be(0) + + type(response['failures']).should.be(list) + len(response['failures']).should.be(1) + response['failures'][0]['failureReason'].should.equal("Requested image not found") + response['failures'][0]['failureCode'].should.equal("ImageNotFound") + response['failures'][0]['imageId']['imageTag'].should.equal("v5") + + +@mock_ecr +def test_batch_get_image_no_tags(): + 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' + ) + + error_msg = re.compile( + r".*Missing required parameter in input: \"imageIds\".*", + re.MULTILINE) + + client.batch_get_image.when.called_with( + repositoryName='test_repository').should.throw( + ParamValidationError, error_msg)