diff --git a/packages/aws-cdk-lib/aws-s3/lib/bucket.ts b/packages/aws-cdk-lib/aws-s3/lib/bucket.ts index dc542a0a0199c..5cf7d8312f69d 100644 --- a/packages/aws-cdk-lib/aws-s3/lib/bucket.ts +++ b/packages/aws-cdk-lib/aws-s3/lib/bucket.ts @@ -2241,6 +2241,20 @@ export class Bucket extends BucketBase { if (!this.lifecycleRules || this.lifecycleRules.length === 0) { return undefined; } + const isValid = this.lifecycleRules.every( + (rule: LifecycleRule): boolean => + rule.abortIncompleteMultipartUploadAfter !== undefined || + rule.expiration !== undefined || + rule.expirationDate !== undefined || + rule.expiredObjectDeleteMarker !== undefined || + rule.noncurrentVersionExpiration !== undefined || + rule.noncurrentVersionsToRetain !== undefined || + rule.noncurrentVersionTransitions !== undefined || + rule.transitions !== undefined, + ); + if (!isValid) { + throw new Error('All rules for `lifecycleRules` must have at least one of the following properties: `abortIncompleteMultipartUploadAfter`, `expiration`, `expirationDate`, `expiredObjectDeleteMarker`, `noncurrentVersionExpiration`, `noncurrentVersionsToRetain`, `noncurrentVersionTransitions`, or `transitions`'); + } const self = this; diff --git a/packages/aws-cdk-lib/aws-s3/test/rules.test.ts b/packages/aws-cdk-lib/aws-s3/test/rules.test.ts index 668c84afce1f0..c5907f7092b61 100644 --- a/packages/aws-cdk-lib/aws-s3/test/rules.test.ts +++ b/packages/aws-cdk-lib/aws-s3/test/rules.test.ts @@ -1,5 +1,5 @@ import { Template } from '../../assertions'; -import { Duration, Stack } from '../../core'; +import { App, Duration, Stack } from '../../core'; import { Bucket, StorageClass } from '../lib'; describe('rules', () => { @@ -334,4 +334,163 @@ describe('rules', () => { }, }); }); + + describe('required properties for rules', () => { + test('throw if there is a rule doesn\'t have required properties', () => { + const app = new App(); + const stack = new Stack(app); + new Bucket(stack, 'MyBucket', { + lifecycleRules: [ + { + objectSizeLessThan: 300000, + objectSizeGreaterThan: 200000, + }, + ], + }); + expect(() => { + app.synth(); + }).toThrow(/All rules for `lifecycleRules` must have at least one of the following properties: `abortIncompleteMultipartUploadAfter`, `expiration`, `expirationDate`, `expiredObjectDeleteMarker`, `noncurrentVersionExpiration`, `noncurrentVersionsToRetain`, `noncurrentVersionTransitions`, or `transitions`/); + }); + + test('throw if there are a valid rule and a rule that doesn\'t have required properties.', () => { + const app = new App(); + const stack = new Stack(app); + new Bucket(stack, 'MyBucket', { + lifecycleRules: [ + { + abortIncompleteMultipartUploadAfter: Duration.days(365), + }, + { + objectSizeLessThan: 300000, + objectSizeGreaterThan: 200000, + }, + ], + }); + expect(() => { + app.synth(); + }).toThrow(/All rules for `lifecycleRules` must have at least one of the following properties: `abortIncompleteMultipartUploadAfter`, `expiration`, `expirationDate`, `expiredObjectDeleteMarker`, `noncurrentVersionExpiration`, `noncurrentVersionsToRetain`, `noncurrentVersionTransitions`, or `transitions`/); + }); + + test('don\'t throw with abortIncompleteMultipartUploadAfter', () => { + const stack = new Stack(); + new Bucket(stack, 'MyBucket', { + lifecycleRules: [ + { + abortIncompleteMultipartUploadAfter: Duration.days(365), + }, + ], + }); + expect(() => { + Template.fromStack(stack); + }).not.toThrow(); + }); + + test('don\'t throw with expiration', () => { + const stack = new Stack(); + new Bucket(stack, 'MyBucket', { + lifecycleRules: [ + { + expiration: Duration.days(365), + }, + ], + }); + expect(() => { + Template.fromStack(stack); + }).not.toThrow(); + }); + + test('don\'t throw with expirationDate', () => { + const stack = new Stack(); + new Bucket(stack, 'MyBucket', { + lifecycleRules: [ + { + expirationDate: new Date('2024-01-01'), + }, + ], + }); + expect(() => { + Template.fromStack(stack); + }).not.toThrow(); + }); + + test('don\'t throw with expiredObjectDeleteMarker', () => { + const stack = new Stack(); + new Bucket(stack, 'MyBucket', { + lifecycleRules: [ + { + expiredObjectDeleteMarker: true, + }, + ], + }); + expect(() => { + Template.fromStack(stack); + }).not.toThrow(); + }); + + test('don\'t throw with noncurrentVersionExpiration', () => { + const stack = new Stack(); + new Bucket(stack, 'MyBucket', { + lifecycleRules: [ + { + noncurrentVersionExpiration: Duration.days(365), + }, + ], + }); + expect(() => { + Template.fromStack(stack); + }).not.toThrow(); + }); + + test('don\'t throw with noncurrentVersionsToRetain', () => { + const stack = new Stack(); + new Bucket(stack, 'MyBucket', { + lifecycleRules: [ + { + noncurrentVersionsToRetain: 10, + }, + ], + }); + expect(() => { + Template.fromStack(stack); + }).not.toThrow(); + }); + + test('don\'t throw with noncurrentVersionTransitions', () => { + const stack = new Stack(); + new Bucket(stack, 'MyBucket', { + lifecycleRules: [ + { + noncurrentVersionTransitions: [ + { + storageClass: StorageClass.GLACIER_INSTANT_RETRIEVAL, + transitionAfter: Duration.days(10), + noncurrentVersionsToRetain: 1, + }, + ], + }, + ], + }); + expect(() => { + Template.fromStack(stack); + }).not.toThrow(); + }); + + test('don\'t throw with transitions', () => { + const stack = new Stack(); + new Bucket(stack, 'MyBucket', { + lifecycleRules: [ + { + transitions: [{ + storageClass: StorageClass.GLACIER, + transitionAfter: Duration.days(30), + }], + }, + ], + }); + expect(() => { + Template.fromStack(stack); + }).not.toThrow(); + }); + + }); });