Skip to content

Commit 830e6d3

Browse files
authored
fix(ecr): auto delete images on ECR repository containing manifest list (#25789)
Fixes #24822 As I commented on #24822 (comment), auto delete container images in ECR repository fails when it has container manifest list. I fix custom resource Lambda function to delete tagged images first. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 1668dbd commit 830e6d3

File tree

4 files changed

+82
-8
lines changed

4 files changed

+82
-8
lines changed

packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/aws-ecr-integ-stack.template.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
"S3Bucket": {
9090
"Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}"
9191
},
92-
"S3Key": "0bec74976eeec3c24fbc534a8e85197274c1c43a93018353f96c90cbd671cf14.zip"
92+
"S3Key": "9064d7af3a637d340a1e36aada4ccade64a383701b3b15008043e12bbea5a67e.zip"
9393
},
9494
"Timeout": 900,
9595
"MemorySize": 128,

packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.repository-auto-delete-images.ts

+1
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ new cdk.CfnOutput(stack, 'RepositoryURI', {
1717

1818
new IntegTest(app, 'cdk-integ-auto-delete-images', {
1919
testCases: [stack],
20+
diffAssets: true,
2021
});

packages/aws-cdk-lib/aws-ecr/lib/auto-delete-images-handler/index.ts

+24-6
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,34 @@ async function onUpdate(event: AWSLambda.CloudFormationCustomResourceEvent) {
3939
async function emptyRepository(params: ECR.ListImagesRequest) {
4040
const listedImages = await ecr.listImages(params).promise();
4141

42-
const imageIds = listedImages?.imageIds ?? [];
42+
const imageIds: ECR.ImageIdentifier[] = [];
43+
const imageIdsTagged: ECR.ImageIdentifier[] = [];
44+
(listedImages.imageIds ?? []).forEach(imageId => {
45+
if ('imageTag' in imageId) {
46+
imageIdsTagged.push(imageId);
47+
} else {
48+
imageIds.push(imageId);
49+
}
50+
});
51+
4352
const nextToken = listedImages.nextToken ?? null;
44-
if (imageIds.length === 0) {
53+
if (imageIds.length === 0 && imageIdsTagged.length === 0) {
4554
return;
4655
}
4756

48-
await ecr.batchDeleteImage({
49-
repositoryName: params.repositoryName,
50-
imageIds,
51-
}).promise();
57+
if (imageIdsTagged.length !== 0) {
58+
await ecr.batchDeleteImage({
59+
repositoryName: params.repositoryName,
60+
imageIds: imageIdsTagged,
61+
}).promise();
62+
}
63+
64+
if (imageIds.length !== 0) {
65+
await ecr.batchDeleteImage({
66+
repositoryName: params.repositoryName,
67+
imageIds: imageIds,
68+
}).promise();
69+
}
5270

5371
if (nextToken) {
5472
await emptyRepository({

packages/aws-cdk-lib/aws-ecr/test/auto-delete-images-handler.test.ts

+56-1
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ test('deletes all objects when the name changes on update event', async () => {
157157
await invokeHandler(event);
158158

159159
// THEN
160+
expect(mockECRClient.describeRepositories).toHaveBeenCalledTimes(1);
160161
expect(mockECRClient.listImages).toHaveBeenCalledTimes(1);
161162
expect(mockECRClient.listImages).toHaveBeenCalledWith({ repositoryName: 'MyRepo' });
162163
expect(mockECRClient.batchDeleteImage).toHaveBeenCalledTimes(1);
@@ -167,7 +168,6 @@ test('deletes all objects when the name changes on update event', async () => {
167168
{ imageDigest: 'ImageDigest2', imageTag: 'ImageTag2' },
168169
],
169170
});
170-
expect(mockECRClient.describeRepositories).toHaveBeenCalledTimes(1);
171171
});
172172

173173
test('deletes no images on delete event when repository has no images', async () => {
@@ -366,6 +366,61 @@ test('does nothing when the repository does not exist', async () => {
366366
expect(mockECRClient.batchDeleteImage).not.toHaveBeenCalled();
367367
});
368368

369+
test('delete event where repo has tagged images and untagged images', async () => {
370+
// GIVEN
371+
mockAwsPromise(mockECRClient.describeRepositories, {
372+
repositories: [
373+
{ repositoryArn: 'RepositoryArn', respositoryName: 'MyRepo' },
374+
],
375+
});
376+
377+
mockECRClient.promise // listedImages() call
378+
.mockResolvedValueOnce({
379+
imageIds: [
380+
{
381+
imageTag: 'tag1',
382+
imageDigest: 'sha256-1',
383+
},
384+
{
385+
imageDigest: 'sha256-2',
386+
},
387+
],
388+
});
389+
390+
// WHEN
391+
const event: Partial<AWSLambda.CloudFormationCustomResourceDeleteEvent> = {
392+
RequestType: 'Delete',
393+
ResourceProperties: {
394+
ServiceToken: 'Foo',
395+
RepositoryName: 'MyRepo',
396+
},
397+
};
398+
await invokeHandler(event);
399+
400+
// THEN
401+
expect(mockECRClient.describeRepositories).toHaveBeenCalledTimes(1);
402+
expect(mockECRClient.listImages).toHaveBeenCalledTimes(1);
403+
expect(mockECRClient.listImages).toHaveBeenCalledWith({ repositoryName: 'MyRepo' });
404+
expect(mockECRClient.batchDeleteImage).toHaveBeenCalledTimes(2);
405+
expect(mockECRClient.batchDeleteImage).toHaveBeenNthCalledWith(1, {
406+
repositoryName: 'MyRepo',
407+
imageIds: [
408+
{
409+
imageTag: 'tag1',
410+
imageDigest: 'sha256-1',
411+
},
412+
],
413+
});
414+
expect(mockECRClient.batchDeleteImage).toHaveBeenNthCalledWith(2, {
415+
repositoryName: 'MyRepo',
416+
imageIds: [
417+
{
418+
imageDigest: 'sha256-2',
419+
},
420+
],
421+
});
422+
});
423+
369424
// helper function to get around TypeScript expecting a complete event object,
370425
// even though our tests only need some of the fields
371426
async function invokeHandler(event: Partial<AWSLambda.CloudFormationCustomResourceEvent>) {

0 commit comments

Comments
 (0)