From b955c6c81a7a9a4816531f44aadfeaccb4338edb Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Wed, 20 Dec 2023 17:56:29 +0900 Subject: [PATCH 1/9] feat(ecr): support tag pattern list for lifecycle policy --- packages/aws-cdk-lib/aws-ecr/lib/lifecycle.ts | 18 +++++++++- .../aws-cdk-lib/aws-ecr/lib/repository.ts | 34 ++++++++++++++----- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/packages/aws-cdk-lib/aws-ecr/lib/lifecycle.ts b/packages/aws-cdk-lib/aws-ecr/lib/lifecycle.ts index 8349745be5599..69159bcb2ad16 100644 --- a/packages/aws-cdk-lib/aws-ecr/lib/lifecycle.ts +++ b/packages/aws-cdk-lib/aws-ecr/lib/lifecycle.ts @@ -33,17 +33,33 @@ export interface LifecycleRule { * Only one rule is allowed to select untagged images, and it must * have the highest rulePriority. * - * @default TagStatus.Tagged if tagPrefixList is given, TagStatus.Any otherwise + * @default TagStatus.Tagged if tagPrefixList or tagPatternList is + * given, TagStatus.Any otherwise */ readonly tagStatus?: TagStatus; /** * Select images that have ALL the given prefixes in their tag. * + * Both tagPrefixList and tagPatternList cannot be specified together. + * * Only if tagStatus == TagStatus.Tagged */ readonly tagPrefixList?: string[]; + /** + * Select images that have ALL the given patterns in their tag. + * + * There is a maximum limit of four wildcards (*) per string. + * For example, ["*test*1*2*3", "test*1*2*3*"] is valid but + * ["test*1*2*3*4*5*6"] is invalid. + * + * Both tagPrefixList and tagPatternList cannot be specified together. + * + * Only if tagStatus == TagStatus.Tagged + */ + readonly tagPatternList?: string[]; + /** * The maximum number of images to retain * diff --git a/packages/aws-cdk-lib/aws-ecr/lib/repository.ts b/packages/aws-cdk-lib/aws-ecr/lib/repository.ts index 008753dbd567d..f536a147469f5 100644 --- a/packages/aws-cdk-lib/aws-ecr/lib/repository.ts +++ b/packages/aws-cdk-lib/aws-ecr/lib/repository.ts @@ -758,14 +758,31 @@ export class Repository extends RepositoryBase { public addLifecycleRule(rule: LifecycleRule) { // Validate rule here so users get errors at the expected location if (rule.tagStatus === undefined) { - rule = { ...rule, tagStatus: rule.tagPrefixList === undefined ? TagStatus.ANY : TagStatus.TAGGED }; - } - - if (rule.tagStatus === TagStatus.TAGGED && (rule.tagPrefixList === undefined || rule.tagPrefixList.length === 0)) { - throw new Error('TagStatus.Tagged requires the specification of a tagPrefixList'); - } - if (rule.tagStatus !== TagStatus.TAGGED && rule.tagPrefixList !== undefined) { - throw new Error('tagPrefixList can only be specified when tagStatus is set to Tagged'); + rule = { ...rule, tagStatus: rule.tagPrefixList === undefined && rule.tagPatternList === undefined ? TagStatus.ANY : TagStatus.TAGGED }; + } + + if (rule.tagStatus === TagStatus.TAGGED + && (rule.tagPrefixList === undefined || rule.tagPrefixList.length === 0) + && (rule.tagPatternList === undefined || rule.tagPatternList.length === 0) + ) { + throw new Error('TagStatus.Tagged requires the specification of a tagPrefixList or a tagPatternList'); + } + if ( + (rule.tagStatus !== TagStatus.TAGGED && rule.tagPrefixList !== undefined) + || (rule.tagStatus !== TagStatus.TAGGED && rule.tagPatternList !== undefined) + ) { + throw new Error('tagPrefixList and tagPatternList can only be specified when tagStatus is set to Tagged'); + } + if (rule.tagPrefixList !== undefined && rule.tagPatternList !== undefined) { + throw new Error('Both tagPrefixList and tagPatternList cannot be specified together'); + } + if (rule.tagPatternList !== undefined) { + rule.tagPatternList.forEach((pattern) => { + const splittedPatternLength = pattern.split('*').length; + if (splittedPatternLength > 5) { + throw new Error(`A tag pattern cannot contain more than four wildcard characters (*), tagPattern: ${pattern}, counts: ${splittedPatternLength - 1}`); + } + }); } if ((rule.maxImageAge !== undefined) === (rule.maxImageCount !== undefined)) { throw new Error(`Life cycle rule must contain exactly one of 'maxImageAge' and 'maxImageCount', got: ${JSON.stringify(rule)}`); @@ -924,6 +941,7 @@ function renderLifecycleRule(rule: LifecycleRule) { selection: { tagStatus: rule.tagStatus || TagStatus.ANY, tagPrefixList: rule.tagPrefixList, + tagPatternList: rule.tagPatternList, countType: rule.maxImageAge !== undefined ? CountType.SINCE_IMAGE_PUSHED : CountType.IMAGE_COUNT_MORE_THAN, countNumber: rule.maxImageAge?.toDays() ?? rule.maxImageCount, countUnit: rule.maxImageAge !== undefined ? 'days' : undefined, From eff75b009e065056f409fd23c6638ab163ef49ae Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Wed, 20 Dec 2023 18:19:07 +0900 Subject: [PATCH 2/9] add unit tests --- .../aws-ecr/test/repository.test.ts | 91 ++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts b/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts index 002c347688457..90761711c74ab 100644 --- a/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts +++ b/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts @@ -49,7 +49,7 @@ describe('repository', () => { }); }); - test('tag-based lifecycle policy', () => { + test('tag-based lifecycle policy with tagPrefixList', () => { // GIVEN const stack = new cdk.Stack(); const repo = new ecr.Repository(stack, 'Repo'); @@ -66,6 +66,95 @@ describe('repository', () => { }); }); + test('tag-based lifecycle policy with tagPatternList', () => { + // GIVEN + const stack = new cdk.Stack(); + const repo = new ecr.Repository(stack, 'Repo'); + + // WHEN + repo.addLifecycleRule({ tagPatternList: ['abc*'], maxImageCount: 1 }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', { + LifecyclePolicy: { + // eslint-disable-next-line max-len + LifecyclePolicyText: '{"rules":[{"rulePriority":1,"selection":{"tagStatus":"tagged","tagPatternList":["abc*"],"countType":"imageCountMoreThan","countNumber":1},"action":{"type":"expire"}}]}', + }, + }); + }); + + test('both tagPrefixList and tagPatternList cannot be specified together', () => { + // GIVEN + const stack = new cdk.Stack(); + const repo = new ecr.Repository(stack, 'Repo'); + + // THEN + expect(() => { + repo.addLifecycleRule({ tagPrefixList: ['abc'], tagPatternList: ['abc*'], maxImageCount: 1 }); + }).toThrow(/Both tagPrefixList and tagPatternList cannot be specified together/); + }); + + test('tagPrefixList can only be specified when tagStatus is set to Tagged', () => { + // GIVEN + const stack = new cdk.Stack(); + const repo = new ecr.Repository(stack, 'Repo'); + + // THEN + expect(() => { + repo.addLifecycleRule({ tagStatus: ecr.TagStatus.ANY, tagPrefixList: ['abc'], maxImageCount: 1 }); + }).toThrow(/tagPrefixList and tagPatternList can only be specified when tagStatus is set to Tagged/); + }); + + test('tagPatternList can only be specified when tagStatus is set to Tagged', () => { + // GIVEN + const stack = new cdk.Stack(); + const repo = new ecr.Repository(stack, 'Repo'); + + // THEN + expect(() => { + repo.addLifecycleRule({ tagStatus: ecr.TagStatus.ANY, tagPatternList: ['abc*'], maxImageCount: 1 }); + }).toThrow(/tagPrefixList and tagPatternList can only be specified when tagStatus is set to Tagged/); + }); + + test('TagStatus.Tagged requires the specification of a tagPrefixList or a tagPatternList', () => { + // GIVEN + const stack = new cdk.Stack(); + const repo = new ecr.Repository(stack, 'Repo'); + + // THEN + expect(() => { + repo.addLifecycleRule({ tagStatus: ecr.TagStatus.TAGGED, maxImageCount: 1 }); + }).toThrow(/TagStatus.Tagged requires the specification of a tagPrefixList or a tagPatternList/); + }); + + test('A tag pattern can contain four wildcard characters', () => { + // GIVEN + const stack = new cdk.Stack(); + const repo = new ecr.Repository(stack, 'Repo'); + + // WHEN + repo.addLifecycleRule({ tagPatternList: ['abc*d*e*f*'], maxImageCount: 1 }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', { + LifecyclePolicy: { + // eslint-disable-next-line max-len + LifecyclePolicyText: '{"rules":[{"rulePriority":1,"selection":{"tagStatus":"tagged","tagPatternList":["abc*d*e*f*"],"countType":"imageCountMoreThan","countNumber":1},"action":{"type":"expire"}}]}', + }, + }); + }); + + test('A tag pattern cannot contain more than four wildcard characters', () => { + // GIVEN + const stack = new cdk.Stack(); + const repo = new ecr.Repository(stack, 'Repo'); + + // THEN + expect(() => { + repo.addLifecycleRule({ tagPatternList: ['abc*d*e*f*g*h'], maxImageCount: 1 }); + }).toThrow(/A tag pattern cannot contain more than four wildcard characters \(\*\), tagPattern: abc\*d\*e\*f\*g\*h, counts: 5/); + }); + test('image tag mutability can be set', () => { // GIVEN const stack = new cdk.Stack(); From f1ba444cdbbb42f302d718df725f5bf1193c5727 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Wed, 20 Dec 2023 18:26:07 +0900 Subject: [PATCH 3/9] add snapshots --- .../aws-ecr-integ-stack.assets.json | 6 +- .../aws-ecr-integ-stack.template.json | 2 +- .../test/integ.basic.js.snapshot/cdk.out | 2 +- ...efaultTestDeployAssert4F7FBFB4.assets.json | 2 +- .../test/integ.basic.js.snapshot/integ.json | 2 +- .../integ.basic.js.snapshot/manifest.json | 6 +- .../test/integ.basic.js.snapshot/tree.json | 62 +++++++++---------- .../test/aws-ecr/test/integ.basic.ts | 2 + 8 files changed, 44 insertions(+), 40 deletions(-) diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/aws-ecr-integ-stack.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/aws-ecr-integ-stack.assets.json index de9244e843078..b1ebebaa8af67 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/aws-ecr-integ-stack.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/aws-ecr-integ-stack.assets.json @@ -1,7 +1,7 @@ { - "version": "31.0.0", + "version": "35.0.0", "files": { - "a047e78171779d23d25e3fc35f2b3ce7ff7313e616a588b6f8773b9360f12b26": { + "830646461dc1fed84d30409177199864dfe0b167864b2fcdca22f4b9aad8a063": { "source": { "path": "aws-ecr-integ-stack.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "a047e78171779d23d25e3fc35f2b3ce7ff7313e616a588b6f8773b9360f12b26.json", + "objectKey": "830646461dc1fed84d30409177199864dfe0b167864b2fcdca22f4b9aad8a063.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/aws-ecr-integ-stack.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/aws-ecr-integ-stack.template.json index fea2eae1cf611..3b8bc77cdd7a2 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/aws-ecr-integ-stack.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/aws-ecr-integ-stack.template.json @@ -4,7 +4,7 @@ "Type": "AWS::ECR::Repository", "Properties": { "LifecyclePolicy": { - "LifecyclePolicyText": "{\"rules\":[{\"rulePriority\":1,\"selection\":{\"tagStatus\":\"any\",\"countType\":\"imageCountMoreThan\",\"countNumber\":5},\"action\":{\"type\":\"expire\"}}]}" + "LifecyclePolicyText": "{\"rules\":[{\"rulePriority\":1,\"selection\":{\"tagStatus\":\"tagged\",\"tagPrefixList\":[\"abc\"],\"countType\":\"imageCountMoreThan\",\"countNumber\":3},\"action\":{\"type\":\"expire\"}},{\"rulePriority\":2,\"selection\":{\"tagStatus\":\"tagged\",\"tagPatternList\":[\"abc*\"],\"countType\":\"imageCountMoreThan\",\"countNumber\":3},\"action\":{\"type\":\"expire\"}},{\"rulePriority\":3,\"selection\":{\"tagStatus\":\"any\",\"countType\":\"imageCountMoreThan\",\"countNumber\":5},\"action\":{\"type\":\"expire\"}}]}" }, "RepositoryPolicyText": { "Statement": [ diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/cdk.out index 7925065efbcc4..c5cb2e5de6344 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/cdk.out +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"31.0.0"} \ No newline at end of file +{"version":"35.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/cdkecrintegtestbasicDefaultTestDeployAssert4F7FBFB4.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/cdkecrintegtestbasicDefaultTestDeployAssert4F7FBFB4.assets.json index fbfe3f9089f79..4a4a176d22fed 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/cdkecrintegtestbasicDefaultTestDeployAssert4F7FBFB4.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/cdkecrintegtestbasicDefaultTestDeployAssert4F7FBFB4.assets.json @@ -1,5 +1,5 @@ { - "version": "31.0.0", + "version": "35.0.0", "files": { "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { "source": { diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/integ.json index 8ded05fd44287..530efded1be3b 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/integ.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "31.0.0", + "version": "35.0.0", "testCases": { "cdk-ecr-integ-test-basic/DefaultTest": { "stacks": [ diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/manifest.json index 299140c835cd5..5b0f84101412b 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "31.0.0", + "version": "35.0.0", "artifacts": { "aws-ecr-integ-stack.assets": { "type": "cdk:asset-manifest", @@ -14,10 +14,11 @@ "environment": "aws://unknown-account/unknown-region", "properties": { "templateFile": "aws-ecr-integ-stack.template.json", + "terminationProtection": false, "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/a047e78171779d23d25e3fc35f2b3ce7ff7313e616a588b6f8773b9360f12b26.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/830646461dc1fed84d30409177199864dfe0b167864b2fcdca22f4b9aad8a063.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -85,6 +86,7 @@ "environment": "aws://unknown-account/unknown-region", "properties": { "templateFile": "cdkecrintegtestbasicDefaultTestDeployAssert4F7FBFB4.template.json", + "terminationProtection": false, "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/tree.json index 5278e08ffe6e2..0bbeb6a6f2096 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.js.snapshot/tree.json @@ -19,7 +19,7 @@ "aws:cdk:cloudformation:type": "AWS::ECR::Repository", "aws:cdk:cloudformation:props": { "lifecyclePolicy": { - "lifecyclePolicyText": "{\"rules\":[{\"rulePriority\":1,\"selection\":{\"tagStatus\":\"any\",\"countType\":\"imageCountMoreThan\",\"countNumber\":5},\"action\":{\"type\":\"expire\"}}]}" + "lifecyclePolicyText": "{\"rules\":[{\"rulePriority\":1,\"selection\":{\"tagStatus\":\"tagged\",\"tagPrefixList\":[\"abc\"],\"countType\":\"imageCountMoreThan\",\"countNumber\":3},\"action\":{\"type\":\"expire\"}},{\"rulePriority\":2,\"selection\":{\"tagStatus\":\"tagged\",\"tagPatternList\":[\"abc*\"],\"countType\":\"imageCountMoreThan\",\"countNumber\":3},\"action\":{\"type\":\"expire\"}},{\"rulePriority\":3,\"selection\":{\"tagStatus\":\"any\",\"countType\":\"imageCountMoreThan\",\"countNumber\":5},\"action\":{\"type\":\"expire\"}}]}" }, "repositoryPolicyText": { "Statement": [ @@ -36,14 +36,14 @@ } }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_ecr.CfnRepository", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.3.0" } } }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_ecr.Repository", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.3.0" } }, "MyUser": { @@ -58,8 +58,8 @@ "aws:cdk:cloudformation:props": {} }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_iam.CfnUser", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.3.0" } }, "DefaultPolicy": { @@ -111,50 +111,50 @@ } }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_iam.CfnPolicy", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.3.0" } } }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_iam.Policy", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.3.0" } } }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_iam.User", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.3.0" } }, "RepositoryURI": { "id": "RepositoryURI", "path": "aws-ecr-integ-stack/RepositoryURI", "constructInfo": { - "fqn": "aws-cdk-lib.CfnOutput", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.3.0" } }, "BootstrapVersion": { "id": "BootstrapVersion", "path": "aws-ecr-integ-stack/BootstrapVersion", "constructInfo": { - "fqn": "aws-cdk-lib.CfnParameter", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.3.0" } }, "CheckBootstrapVersion": { "id": "CheckBootstrapVersion", "path": "aws-ecr-integ-stack/CheckBootstrapVersion", "constructInfo": { - "fqn": "aws-cdk-lib.CfnRule", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.3.0" } } }, "constructInfo": { - "fqn": "aws-cdk-lib.Stack", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.3.0" } }, "cdk-ecr-integ-test-basic": { @@ -170,7 +170,7 @@ "path": "cdk-ecr-integ-test-basic/DefaultTest/Default", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.270" + "version": "10.3.0" } }, "DeployAssert": { @@ -181,22 +181,22 @@ "id": "BootstrapVersion", "path": "cdk-ecr-integ-test-basic/DefaultTest/DeployAssert/BootstrapVersion", "constructInfo": { - "fqn": "aws-cdk-lib.CfnParameter", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.3.0" } }, "CheckBootstrapVersion": { "id": "CheckBootstrapVersion", "path": "cdk-ecr-integ-test-basic/DefaultTest/DeployAssert/CheckBootstrapVersion", "constructInfo": { - "fqn": "aws-cdk-lib.CfnRule", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.3.0" } } }, "constructInfo": { - "fqn": "aws-cdk-lib.Stack", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.3.0" } } }, @@ -216,13 +216,13 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.270" + "version": "10.3.0" } } }, "constructInfo": { - "fqn": "aws-cdk-lib.App", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.3.0" } } } \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.ts index 3830660077478..1e63245465eb3 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecr/test/integ.basic.ts @@ -8,6 +8,8 @@ const stack = new cdk.Stack(app, 'aws-ecr-integ-stack'); const repo = new ecr.Repository(stack, 'Repo'); repo.addLifecycleRule({ maxImageCount: 5 }); +repo.addLifecycleRule({ tagPrefixList: ['abc'], maxImageCount: 3 }); +repo.addLifecycleRule({ tagPatternList: ['abc*'], maxImageCount: 3 }); repo.addToResourcePolicy(new iam.PolicyStatement({ actions: ['ecr:GetDownloadUrlForLayer'], principals: [new iam.AnyPrincipal()], From f8e51fc823ac6e21a63abe590f3b74c9303ded40 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Wed, 20 Dec 2023 18:35:16 +0900 Subject: [PATCH 4/9] change docs --- packages/aws-cdk-lib/aws-ecr/lib/lifecycle.ts | 6 ++++-- packages/aws-cdk-lib/aws-ecr/lib/repository.ts | 2 +- packages/aws-cdk-lib/aws-ecr/test/repository.test.ts | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/aws-cdk-lib/aws-ecr/lib/lifecycle.ts b/packages/aws-cdk-lib/aws-ecr/lib/lifecycle.ts index 69159bcb2ad16..aff39f9909bd8 100644 --- a/packages/aws-cdk-lib/aws-ecr/lib/lifecycle.ts +++ b/packages/aws-cdk-lib/aws-ecr/lib/lifecycle.ts @@ -41,7 +41,8 @@ export interface LifecycleRule { /** * Select images that have ALL the given prefixes in their tag. * - * Both tagPrefixList and tagPatternList cannot be specified together. + * Both tagPrefixList and tagPatternList cannot be specified + * together in a rule. * * Only if tagStatus == TagStatus.Tagged */ @@ -54,7 +55,8 @@ export interface LifecycleRule { * For example, ["*test*1*2*3", "test*1*2*3*"] is valid but * ["test*1*2*3*4*5*6"] is invalid. * - * Both tagPrefixList and tagPatternList cannot be specified together. + * Both tagPrefixList and tagPatternList cannot be specified + * together in a rule. * * Only if tagStatus == TagStatus.Tagged */ diff --git a/packages/aws-cdk-lib/aws-ecr/lib/repository.ts b/packages/aws-cdk-lib/aws-ecr/lib/repository.ts index f536a147469f5..987e94f1c6922 100644 --- a/packages/aws-cdk-lib/aws-ecr/lib/repository.ts +++ b/packages/aws-cdk-lib/aws-ecr/lib/repository.ts @@ -774,7 +774,7 @@ export class Repository extends RepositoryBase { throw new Error('tagPrefixList and tagPatternList can only be specified when tagStatus is set to Tagged'); } if (rule.tagPrefixList !== undefined && rule.tagPatternList !== undefined) { - throw new Error('Both tagPrefixList and tagPatternList cannot be specified together'); + throw new Error('Both tagPrefixList and tagPatternList cannot be specified together in a rule'); } if (rule.tagPatternList !== undefined) { rule.tagPatternList.forEach((pattern) => { diff --git a/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts b/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts index 90761711c74ab..8f31b09a8f726 100644 --- a/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts +++ b/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts @@ -83,7 +83,7 @@ describe('repository', () => { }); }); - test('both tagPrefixList and tagPatternList cannot be specified together', () => { + test('both tagPrefixList and tagPatternList cannot be specified together in a rule', () => { // GIVEN const stack = new cdk.Stack(); const repo = new ecr.Repository(stack, 'Repo'); @@ -91,7 +91,7 @@ describe('repository', () => { // THEN expect(() => { repo.addLifecycleRule({ tagPrefixList: ['abc'], tagPatternList: ['abc*'], maxImageCount: 1 }); - }).toThrow(/Both tagPrefixList and tagPatternList cannot be specified together/); + }).toThrow(/Both tagPrefixList and tagPatternList cannot be specified together in a rule/); }); test('tagPrefixList can only be specified when tagStatus is set to Tagged', () => { From efd864ecce1a55f711bdd523a55987c6cc225ab0 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Wed, 20 Dec 2023 18:35:30 +0900 Subject: [PATCH 5/9] change README --- packages/aws-cdk-lib/aws-ecr/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/aws-cdk-lib/aws-ecr/README.md b/packages/aws-cdk-lib/aws-ecr/README.md index 288b91f214fe1..d0e824187ac8c 100644 --- a/packages/aws-cdk-lib/aws-ecr/README.md +++ b/packages/aws-cdk-lib/aws-ecr/README.md @@ -167,6 +167,14 @@ repository.addLifecycleRule({ tagPrefixList: ['prod'], maxImageCount: 9999 }); repository.addLifecycleRule({ maxImageAge: Duration.days(30) }); ``` +When using `tagPatternList`, an image is successfully matched if it matches +the wildcard filter. + +```ts +declare const repository: ecr.Repository; +repository.addLifecycleRule({ tagPatternList: ['prod*'], maxImageCount: 9999 }); +``` + ### Repository deletion When a repository is removed from a stack (or the stack is deleted), the ECR From f58d63e46b0c10723c1495bf426ecf9d6e0f1561 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Wed, 20 Dec 2023 18:44:04 +0900 Subject: [PATCH 6/9] change awslint.json --- packages/aws-cdk-lib/awslint.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/aws-cdk-lib/awslint.json b/packages/aws-cdk-lib/awslint.json index 6d19ae862876e..9f44fe0c02597 100644 --- a/packages/aws-cdk-lib/awslint.json +++ b/packages/aws-cdk-lib/awslint.json @@ -660,6 +660,7 @@ "props-default-doc:aws-cdk-lib.aws_ecr.LifecycleRule.maxImageAge", "props-default-doc:aws-cdk-lib.aws_ecr.LifecycleRule.maxImageCount", "props-default-doc:aws-cdk-lib.aws_ecr.LifecycleRule.tagPrefixList", + "props-default-doc:aws-cdk-lib.aws_ecr.LifecycleRule.tagPatternList", "docs-public-apis:aws-cdk-lib.aws_ecr.RepositoryAttributes", "docs-public-apis:aws-cdk-lib.aws_ecr.RepositoryAttributes.repositoryArn", "docs-public-apis:aws-cdk-lib.aws_ecr.RepositoryAttributes.repositoryName", @@ -898,4 +899,4 @@ "from-method:aws-cdk-lib.aws_apigatewayv2.WebSocketIntegration", "from-method:aws-cdk-lib.aws_apigatewayv2.WebSocketRoute" ] -} +} \ No newline at end of file From fedb281bdeba90334b257be700a05dd66e2651c2 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Wed, 20 Dec 2023 19:05:59 +0900 Subject: [PATCH 7/9] refactor --- packages/aws-cdk-lib/aws-ecr/lib/repository.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/aws-cdk-lib/aws-ecr/lib/repository.ts b/packages/aws-cdk-lib/aws-ecr/lib/repository.ts index 5b9964a0eb8c6..33108b6b1c293 100644 --- a/packages/aws-cdk-lib/aws-ecr/lib/repository.ts +++ b/packages/aws-cdk-lib/aws-ecr/lib/repository.ts @@ -778,10 +778,7 @@ export class Repository extends RepositoryBase { ) { throw new Error('TagStatus.Tagged requires the specification of a tagPrefixList or a tagPatternList'); } - if ( - (rule.tagStatus !== TagStatus.TAGGED && rule.tagPrefixList !== undefined) - || (rule.tagStatus !== TagStatus.TAGGED && rule.tagPatternList !== undefined) - ) { + if (rule.tagStatus !== TagStatus.TAGGED && (rule.tagPrefixList !== undefined || rule.tagPatternList !== undefined)) { throw new Error('tagPrefixList and tagPatternList can only be specified when tagStatus is set to Tagged'); } if (rule.tagPrefixList !== undefined && rule.tagPatternList !== undefined) { From 2994e220b6d861ad3b2dc9ca2c73faea5afb28b4 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Wed, 20 Dec 2023 22:18:47 +0900 Subject: [PATCH 8/9] change a message --- packages/aws-cdk-lib/aws-ecr/lib/repository.ts | 2 +- packages/aws-cdk-lib/aws-ecr/test/repository.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk-lib/aws-ecr/lib/repository.ts b/packages/aws-cdk-lib/aws-ecr/lib/repository.ts index 33108b6b1c293..6a91b8a8779a3 100644 --- a/packages/aws-cdk-lib/aws-ecr/lib/repository.ts +++ b/packages/aws-cdk-lib/aws-ecr/lib/repository.ts @@ -788,7 +788,7 @@ export class Repository extends RepositoryBase { rule.tagPatternList.forEach((pattern) => { const splittedPatternLength = pattern.split('*').length; if (splittedPatternLength > 5) { - throw new Error(`A tag pattern cannot contain more than four wildcard characters (*), tagPattern: ${pattern}, counts: ${splittedPatternLength - 1}`); + throw new Error(`A tag pattern cannot contain more than four wildcard characters (*), pattern: ${pattern}, counts: ${splittedPatternLength - 1}`); } }); } diff --git a/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts b/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts index 6c02bd4c523f8..700a8e6ca8ea0 100644 --- a/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts +++ b/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts @@ -152,7 +152,7 @@ describe('repository', () => { // THEN expect(() => { repo.addLifecycleRule({ tagPatternList: ['abc*d*e*f*g*h'], maxImageCount: 1 }); - }).toThrow(/A tag pattern cannot contain more than four wildcard characters \(\*\), tagPattern: abc\*d\*e\*f\*g\*h, counts: 5/); + }).toThrow(/A tag pattern cannot contain more than four wildcard characters \(\*\), pattern: abc\*d\*e\*f\*g\*h, counts: 5/); }); test('image tag mutability can be set', () => { From 1a3ca19a78101124216019c3ef26c14212135166 Mon Sep 17 00:00:00 2001 From: Kaizen Conroy <36202692+kaizencc@users.noreply.github.com> Date: Wed, 20 Dec 2023 09:00:16 -0800 Subject: [PATCH 9/9] Update packages/aws-cdk-lib/aws-ecr/lib/repository.ts --- packages/aws-cdk-lib/aws-ecr/lib/repository.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/aws-cdk-lib/aws-ecr/lib/repository.ts b/packages/aws-cdk-lib/aws-ecr/lib/repository.ts index 6a91b8a8779a3..720bf9002331c 100644 --- a/packages/aws-cdk-lib/aws-ecr/lib/repository.ts +++ b/packages/aws-cdk-lib/aws-ecr/lib/repository.ts @@ -786,9 +786,9 @@ export class Repository extends RepositoryBase { } if (rule.tagPatternList !== undefined) { rule.tagPatternList.forEach((pattern) => { - const splittedPatternLength = pattern.split('*').length; - if (splittedPatternLength > 5) { - throw new Error(`A tag pattern cannot contain more than four wildcard characters (*), pattern: ${pattern}, counts: ${splittedPatternLength - 1}`); + const splitPatternLength = pattern.split('*').length; + if (splitPatternLength > 5) { + throw new Error(`A tag pattern cannot contain more than four wildcard characters (*), pattern: ${pattern}, counts: ${splitPatternLength - 1}`); } }); }