Skip to content

Commit 4128ff4

Browse files
authored
fix(s3): add validation for lifecycle rule transitions (#33731)
### Issue #22103 Closes #22103. ### Reason for this change When configuring S3 bucket lifecycle rules with transitions, CloudFormation requires exactly one of `TransitionDate` or `TransitionInDays` to be specified. However, the CDK currently allows configurations where neither or both properties are specified, which leads to deployment failures. This change adds validation during synthesis to catch these invalid configurations earlier in the development process. ### Description of changes This change adds validation during synthesis to ensure that exactly one of transitionDate or transitionAfter is specified in S3 bucket lifecycle rule transitions. Previously, this validation was only performed during deployment, leading to failed deployments when neither or both properties were specified. The validation is added to the `parseLifecycleRule` function in the S3 bucket implementation, which checks each transition in the lifecycle rules. ### Describe any new or updated permissions being added No new or updated IAM permissions are required for this change. This is purely a validation improvement in the CDK synthesis process. ### Description of how you validated changes Added two unit tests to verify the validation: 1. A test that verifies an error is thrown when neither `transitionDate` nor `transitionAfter` is specified 2. A test that verifies an error is thrown when both `transitionDate` and `transitionAfter` are specified Also ran integration tests to ensure that the change doesn't affect existing valid configurations. No snapshots were updated, confirming that the change only adds validation and doesn't modify the CloudFormation output for valid configurations. ### Checklist - [X] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent c00e647 commit 4128ff4

File tree

3 files changed

+59
-3
lines changed

3 files changed

+59
-3
lines changed

packages/aws-cdk-lib/aws-s3/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -852,9 +852,9 @@ const bucket = new s3.Bucket(this, 'MyBucket', {
852852
{
853853
storageClass: s3.StorageClass.GLACIER,
854854

855-
// the properties below are optional
855+
// exactly one of transitionAfter or transitionDate must be specified
856856
transitionAfter: Duration.days(30),
857-
transitionDate: new Date(),
857+
// transitionDate: new Date(), // cannot specify both
858858
},
859859
],
860860
},

packages/aws-cdk-lib/aws-s3/lib/bucket.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -2545,6 +2545,22 @@ export class Bucket extends BucketBase {
25452545
throw new ValidationError('All rules for `lifecycleRules` must have at least one of the following properties: `abortIncompleteMultipartUploadAfter`, `expiration`, `expirationDate`, `expiredObjectDeleteMarker`, `noncurrentVersionExpiration`, `noncurrentVersionsToRetain`, `noncurrentVersionTransitions`, or `transitions`', self);
25462546
}
25472547

2548+
// Validate transitions: exactly one of transitionDate or transitionAfter must be specified
2549+
if (rule.transitions) {
2550+
for (const transition of rule.transitions) {
2551+
const hasTransitionDate = transition.transitionDate !== undefined;
2552+
const hasTransitionAfter = transition.transitionAfter !== undefined;
2553+
2554+
if (!hasTransitionDate && !hasTransitionAfter) {
2555+
throw new ValidationError('Exactly one of transitionDate or transitionAfter must be specified in lifecycle rule transition', self);
2556+
}
2557+
2558+
if (hasTransitionDate && hasTransitionAfter) {
2559+
throw new ValidationError('Exactly one of transitionDate or transitionAfter must be specified in lifecycle rule transition', self);
2560+
}
2561+
}
2562+
}
2563+
25482564
const x: CfnBucket.RuleProperty = {
25492565
// eslint-disable-next-line max-len
25502566
abortIncompleteMultipartUpload: rule.abortIncompleteMultipartUploadAfter !== undefined ? { daysAfterInitiation: rule.abortIncompleteMultipartUploadAfter.toDays() } : undefined,
@@ -2665,7 +2681,7 @@ export class Bucket extends BucketBase {
26652681
}
26662682

26672683
if (accessControlRequiresObjectOwnership && this.objectOwnership === ObjectOwnership.BUCKET_OWNER_ENFORCED) {
2668-
throw new ValidationError (`objectOwnership must be set to "${ObjectOwnership.OBJECT_WRITER}" when accessControl is "${this.accessControl}"`, this);
2684+
throw new ValidationError(`objectOwnership must be set to "${ObjectOwnership.OBJECT_WRITER}" when accessControl is "${this.accessControl}"`, this);
26692685
}
26702686

26712687
return {

packages/aws-cdk-lib/aws-s3/test/rules.test.ts

+40
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,46 @@ describe('rules', () => {
337337
});
338338
});
339339

340+
test('throws when neither transitionDate nor transitionAfter is specified', () => {
341+
// GIVEN
342+
const stack = new Stack();
343+
344+
// WHEN
345+
new Bucket(stack, 'Bucket', {
346+
lifecycleRules: [{
347+
transitions: [{
348+
storageClass: StorageClass.GLACIER,
349+
}],
350+
}],
351+
});
352+
353+
// THEN
354+
expect(() => {
355+
Template.fromStack(stack).toJSON();
356+
}).toThrow('Exactly one of transitionDate or transitionAfter must be specified in lifecycle rule transition');
357+
});
358+
359+
test('throws when both transitionDate and transitionAfter are specified', () => {
360+
// GIVEN
361+
const stack = new Stack();
362+
363+
// WHEN
364+
new Bucket(stack, 'Bucket', {
365+
lifecycleRules: [{
366+
transitions: [{
367+
storageClass: StorageClass.GLACIER,
368+
transitionDate: new Date('2023-01-01'),
369+
transitionAfter: Duration.days(30),
370+
}],
371+
}],
372+
});
373+
374+
// THEN
375+
expect(() => {
376+
Template.fromStack(stack).toJSON();
377+
}).toThrow('Exactly one of transitionDate or transitionAfter must be specified in lifecycle rule transition');
378+
});
379+
340380
describe('required properties for rules', () => {
341381
test('throw if there is a rule doesn\'t have required properties', () => {
342382
const stack = new Stack();

0 commit comments

Comments
 (0)