Skip to content

Commit a175da8

Browse files
authored
feat(ecr): Add emptyOnDelete CloudFormation property to Repository L2 construct (#28233)
Added `emptyOnDelete` prop to the ecr `Repository` construct. `emptyOndelete` is supported by CloudFormation See here: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecr-repository.html#cfn-ecr-repository-emptyondelete I've also deprecated the `autoDeleteImages` prop that deployed a custom resource. According to #24572 this was added before CloudFormation added the `EmptyOnDelete` property here aws-cloudformation/cloudformation-coverage-roadmap#515 Closes #28196 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent b87212b commit a175da8

27 files changed

+188
-509
lines changed

packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/aws-ecr-integ-stack.assets.json

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/aws-ecr-integ-stack.template.json

+8
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@
6565
}
6666
]
6767
}
68+
},
69+
"RepoWithEmptyOnDeleteCA5C67FA": {
70+
"Type": "AWS::ECR::Repository",
71+
"Properties": {
72+
"EmptyOnDelete": true
73+
},
74+
"UpdateReplacePolicy": "Delete",
75+
"DeletionPolicy": "Delete"
6876
}
6977
},
7078
"Outputs": {

packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/cdk.out

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/cdkecrintegtestbasicDefaultTestDeployAssert4F7FBFB4.assets.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/integ.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/manifest.json

+10-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/tree.json

+26-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.ts

+5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ const user = new iam.User(stack, 'MyUser');
1717
repo.grantRead(user);
1818
repo.grantPullPush(user);
1919

20+
new ecr.Repository(stack, 'RepoWithEmptyOnDelete', {
21+
removalPolicy: cdk.RemovalPolicy.DESTROY,
22+
emptyOnDelete: true,
23+
});
24+
2025
new cdk.CfnOutput(stack, 'RepositoryURI', {
2126
value: repo.repositoryUri,
2227
});

packages/@aws-cdk/app-staging-synthesizer-alpha/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ const app = new App({
253253

254254
By default, the staging resources will be cleaned up on stack deletion. That means that the
255255
S3 Bucket and ECR Repositories are set to `RemovalPolicy.DESTROY` and have `autoDeleteObjects`
256-
or `autoDeleteImages` turned on. This creates custom resources under the hood to facilitate
256+
or `emptyOnDelete` turned on. This creates custom resources under the hood to facilitate
257257
cleanup. To turn this off, specify `autoDeleteStagingAssets: false`.
258258

259259
```ts

packages/@aws-cdk/app-staging-synthesizer-alpha/lib/default-staging-stack.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ export class DefaultStagingStack extends Stack implements IStagingResources {
429429
}],
430430
...(this.autoDeleteStagingAssets ? {
431431
removalPolicy: RemovalPolicy.DESTROY,
432-
autoDeleteImages: true,
432+
emptyOnDelete: true,
433433
} : {
434434
removalPolicy: RemovalPolicy.RETAIN,
435435
}),

packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/StagingStack-default-resourcesmax-ACCOUNT-REGION.template.json

+4-153
Original file line numberDiff line numberDiff line change
@@ -568,175 +568,26 @@
568568
"defaultresourcesmaxecrasset13112F7F9": {
569569
"Type": "AWS::ECR::Repository",
570570
"Properties": {
571+
"EmptyOnDelete": true,
571572
"ImageTagMutability": "IMMUTABLE",
572573
"LifecyclePolicy": {
573574
"LifecyclePolicyText": "{\"rules\":[{\"rulePriority\":1,\"description\":\"Garbage collect old image versions\",\"selection\":{\"tagStatus\":\"any\",\"countType\":\"imageCountMoreThan\",\"countNumber\":3},\"action\":{\"type\":\"expire\"}}]}"
574575
},
575-
"RepositoryName": "default-resourcesmax/ecr-asset/1",
576-
"Tags": [
577-
{
578-
"Key": "aws-cdk:auto-delete-images",
579-
"Value": "true"
580-
}
581-
]
576+
"RepositoryName": "default-resourcesmax/ecr-asset/1"
582577
},
583578
"UpdateReplacePolicy": "Delete",
584579
"DeletionPolicy": "Delete"
585580
},
586-
"defaultresourcesmaxecrasset1AutoDeleteImagesCustomResource0FD7F0F5": {
587-
"Type": "Custom::ECRAutoDeleteImages",
588-
"Properties": {
589-
"ServiceToken": {
590-
"Fn::GetAtt": [
591-
"CustomECRAutoDeleteImagesCustomResourceProviderHandler8D89C030",
592-
"Arn"
593-
]
594-
},
595-
"RepositoryName": {
596-
"Ref": "defaultresourcesmaxecrasset13112F7F9"
597-
}
598-
},
599-
"DependsOn": [
600-
"defaultresourcesmaxecrasset13112F7F9"
601-
],
602-
"UpdateReplacePolicy": "Delete",
603-
"DeletionPolicy": "Delete"
604-
},
605-
"CustomECRAutoDeleteImagesCustomResourceProviderRole665F2773": {
606-
"Type": "AWS::IAM::Role",
607-
"Properties": {
608-
"AssumeRolePolicyDocument": {
609-
"Version": "2012-10-17",
610-
"Statement": [
611-
{
612-
"Action": "sts:AssumeRole",
613-
"Effect": "Allow",
614-
"Principal": {
615-
"Service": "lambda.amazonaws.com"
616-
}
617-
}
618-
]
619-
},
620-
"ManagedPolicyArns": [
621-
{
622-
"Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
623-
}
624-
],
625-
"Policies": [
626-
{
627-
"PolicyName": "Inline",
628-
"PolicyDocument": {
629-
"Version": "2012-10-17",
630-
"Statement": [
631-
{
632-
"Effect": "Allow",
633-
"Action": [
634-
"ecr:BatchDeleteImage",
635-
"ecr:DescribeRepositories",
636-
"ecr:ListImages",
637-
"ecr:ListTagsForResource"
638-
],
639-
"Resource": [
640-
{
641-
"Fn::Join": [
642-
"",
643-
[
644-
"arn:",
645-
{
646-
"Ref": "AWS::Partition"
647-
},
648-
":ecr:",
649-
{
650-
"Ref": "AWS::Region"
651-
},
652-
":",
653-
{
654-
"Ref": "AWS::AccountId"
655-
},
656-
":repository/*"
657-
]
658-
]
659-
}
660-
],
661-
"Condition": {
662-
"StringEquals": {
663-
"ecr:ResourceTag/aws-cdk:auto-delete-images": "true"
664-
}
665-
}
666-
}
667-
]
668-
}
669-
}
670-
]
671-
}
672-
},
673-
"CustomECRAutoDeleteImagesCustomResourceProviderHandler8D89C030": {
674-
"Type": "AWS::Lambda::Function",
675-
"Properties": {
676-
"Code": {
677-
"ZipFile": "\"use strict\";var C=Object.create;var c=Object.defineProperty;var S=Object.getOwnPropertyDescriptor;var w=Object.getOwnPropertyNames;var A=Object.getPrototypeOf,P=Object.prototype.hasOwnProperty;var L=(e,t)=>{for(var o in t)c(e,o,{get:t[o],enumerable:!0})},d=(e,t,o,s)=>{if(t&&typeof t==\"object\"||typeof t==\"function\")for(let r of w(t))!P.call(e,r)&&r!==o&&c(e,r,{get:()=>t[r],enumerable:!(s=S(t,r))||s.enumerable});return e};var p=(e,t,o)=>(o=e!=null?C(A(e)):{},d(t||!e||!e.__esModule?c(o,\"default\",{value:e,enumerable:!0}):o,e)),D=e=>d(c({},\"__esModule\",{value:!0}),e);var W={};L(W,{autoDeleteHandler:()=>I,handler:()=>k});module.exports=D(W);var h=require(\"@aws-sdk/client-ecr\");var m=p(require(\"https\")),R=p(require(\"url\")),n={sendHttpRequest:x,log:N,includeStackTraces:!0,userHandlerIndex:\"./index\"},l=\"AWSCDK::CustomResourceProviderFramework::CREATE_FAILED\",b=\"AWSCDK::CustomResourceProviderFramework::MISSING_PHYSICAL_ID\";function y(e){return async(t,o)=>{let s={...t,ResponseURL:\"...\"};if(n.log(JSON.stringify(s,void 0,2)),t.RequestType===\"Delete\"&&t.PhysicalResourceId===l){n.log(\"ignoring DELETE event caused by a failed CREATE event\"),await u(\"SUCCESS\",t);return}try{let r=await e(s,o),a=T(t,r);await u(\"SUCCESS\",a)}catch(r){let a={...t,Reason:n.includeStackTraces?r.stack:r.message};a.PhysicalResourceId||(t.RequestType===\"Create\"?(n.log(\"CREATE failed, responding with a marker physical resource id so that the subsequent DELETE will be ignored\"),a.PhysicalResourceId=l):n.log(`ERROR: Malformed event. \"PhysicalResourceId\" is required: ${JSON.stringify(t)}`)),await u(\"FAILED\",a)}}}function T(e,t={}){let o=t.PhysicalResourceId??e.PhysicalResourceId??e.RequestId;if(e.RequestType===\"Delete\"&&o!==e.PhysicalResourceId)throw new Error(`DELETE: cannot change the physical resource ID from \"${e.PhysicalResourceId}\" to \"${t.PhysicalResourceId}\" during deletion`);return{...e,...t,PhysicalResourceId:o}}async function u(e,t){let o={Status:e,Reason:t.Reason??e,StackId:t.StackId,RequestId:t.RequestId,PhysicalResourceId:t.PhysicalResourceId||b,LogicalResourceId:t.LogicalResourceId,NoEcho:t.NoEcho,Data:t.Data};n.log(\"submit response to cloudformation\",o);let s=JSON.stringify(o),r=R.parse(t.ResponseURL),a={hostname:r.hostname,path:r.path,method:\"PUT\",headers:{\"content-type\":\"\",\"content-length\":Buffer.byteLength(s,\"utf8\")}};await H({attempts:5,sleep:1e3},n.sendHttpRequest)(a,s)}async function x(e,t){return new Promise((o,s)=>{try{let r=m.request(e,a=>o());r.on(\"error\",s),r.write(t),r.end()}catch(r){s(r)}})}function N(e,...t){console.log(e,...t)}function H(e,t){return async(...o)=>{let s=e.attempts,r=e.sleep;for(;;)try{return await t(...o)}catch(a){if(s--<=0)throw a;await F(Math.floor(Math.random()*r)),r*=2}}}async function F(e){return new Promise(t=>setTimeout(t,e))}var g=\"aws-cdk:auto-delete-images\",i=new h.ECR({}),k=y(I);async function I(e){switch(e.RequestType){case\"Create\":break;case\"Update\":return _(e);case\"Delete\":return f(e.ResourceProperties?.RepositoryName)}}async function _(e){let t=e,o=t.OldResourceProperties?.RepositoryName,s=t.ResourceProperties?.RepositoryName;if(s&&o&&s!==o)return f(o)}async function E(e){let t=await i.listImages(e),o=[],s=[];(t.imageIds??[]).forEach(a=>{\"imageTag\"in a?s.push(a):o.push(a)});let r=t.nextToken??null;o.length===0&&s.length===0||(s.length!==0&&await i.batchDeleteImage({repositoryName:e.repositoryName,imageIds:s}),o.length!==0&&await i.batchDeleteImage({repositoryName:e.repositoryName,imageIds:o}),r&&await E({...e,nextToken:r}))}async function f(e){if(!e)throw new Error(\"No RepositoryName was provided.\");let o=(await i.describeRepositories({repositoryNames:[e]})).repositories?.find(s=>s.repositoryName===e);if(!await q(o?.repositoryArn)){process.stdout.write(`Repository does not have '${g}' tag, skipping cleaning.\n`);return}try{await E({repositoryName:e})}catch(s){if(s.name!==\"RepositoryNotFoundException\")throw s}}async function q(e){return(await i.listTagsForResource({resourceArn:e})).tags?.some(o=>o.Key===g&&o.Value===\"true\")}0&&(module.exports={autoDeleteHandler,handler});\n"
678-
},
679-
"Timeout": 900,
680-
"MemorySize": 128,
681-
"Handler": "index.handler",
682-
"Role": {
683-
"Fn::GetAtt": [
684-
"CustomECRAutoDeleteImagesCustomResourceProviderRole665F2773",
685-
"Arn"
686-
]
687-
},
688-
"Runtime": "nodejs18.x",
689-
"Description": {
690-
"Fn::Join": [
691-
"",
692-
[
693-
"Lambda function for auto-deleting images in ",
694-
{
695-
"Ref": "defaultresourcesmaxecrasset13112F7F9"
696-
},
697-
" repository."
698-
]
699-
]
700-
}
701-
},
702-
"DependsOn": [
703-
"CustomECRAutoDeleteImagesCustomResourceProviderRole665F2773"
704-
]
705-
},
706581
"defaultresourcesmaxecrasset2904B88A7": {
707582
"Type": "AWS::ECR::Repository",
708583
"Properties": {
584+
"EmptyOnDelete": true,
709585
"ImageTagMutability": "IMMUTABLE",
710586
"LifecyclePolicy": {
711587
"LifecyclePolicyText": "{\"rules\":[{\"rulePriority\":1,\"description\":\"Garbage collect old image versions\",\"selection\":{\"tagStatus\":\"any\",\"countType\":\"imageCountMoreThan\",\"countNumber\":3},\"action\":{\"type\":\"expire\"}}]}"
712588
},
713-
"RepositoryName": "default-resourcesmax/ecr-asset-2",
714-
"Tags": [
715-
{
716-
"Key": "aws-cdk:auto-delete-images",
717-
"Value": "true"
718-
}
719-
]
720-
},
721-
"UpdateReplacePolicy": "Delete",
722-
"DeletionPolicy": "Delete"
723-
},
724-
"defaultresourcesmaxecrasset2AutoDeleteImagesCustomResource708714C1": {
725-
"Type": "Custom::ECRAutoDeleteImages",
726-
"Properties": {
727-
"ServiceToken": {
728-
"Fn::GetAtt": [
729-
"CustomECRAutoDeleteImagesCustomResourceProviderHandler8D89C030",
730-
"Arn"
731-
]
732-
},
733-
"RepositoryName": {
734-
"Ref": "defaultresourcesmaxecrasset2904B88A7"
735-
}
589+
"RepositoryName": "default-resourcesmax/ecr-asset-2"
736590
},
737-
"DependsOn": [
738-
"defaultresourcesmaxecrasset2904B88A7"
739-
],
740591
"UpdateReplacePolicy": "Delete",
741592
"DeletionPolicy": "Delete"
742593
}

packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/cdk.out

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integ.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integtestsDefaultTestDeployAssert44C8D370.assets.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)