Skip to content

Commit e15626e

Browse files
authored
chore(s3): add validation on required properties for lifecycle rules (#31806)
### Issue # (if applicable) Closes #<issue number here>. ### Reason for this change If there is a lifecycle rule that does not contain one of the specified properties, an error is raised. ```ts const app = new App(); const stack = new Stack(app, 'aws-cdk-s3'); // An error occurs new Bucket(stack, 'MyBucket', { lifecycleRules: [ // is invalid { objectSizeLessThan: 300000, objectSizeGreaterThan: 200000, }, ], }); ``` ```ts const app = new App(); const stack = new Stack(app, 'aws-cdk-s3'); // An error occurs new Bucket(stack, 'MyBucket', { lifecycleRules: [ // is valid { abortIncompleteMultipartUploadAfter: Duration.days(365), }, // is invalid { objectSizeLessThan: 300000, objectSizeGreaterThan: 200000, }, ], }); ``` A CFn message: ``` Invalid request provided: At least one of [ExpirationDate,ExpirationInDays,AbortIncompleteMultipartUpload,Transition,Transitions,NoncurrentVersionExpirationInDays,NoncurrentVersionTransition,NoncurrentVersionTransitions,NoncurrentVersionExpiration,ExpiredObjectDeleteMarker] needs to be specified ``` The properties in CFn properties: - AbortIncompleteMultipartUpload - ExpirationDate - ExpirationInDays - ExpiredObjectDeleteMarker - NoncurrentVersionExpirationInDays - NoncurrentVersionTransition - NoncurrentVersionTransitions - NoncurrentVersionExpiration - Transition - Transitions The properties in L2 props: - abortIncompleteMultipartUploadAfter - expiration - expirationDate - expiredObjectDeleteMarker - noncurrentVersionExpiration - noncurrentVersionsToRetain - noncurrentVersionTransitions - transitions ### Description of changes Check whether a rule has required properties in lifecycleRules for L2 BucketProps. ```ts if ( rule.abortIncompleteMultipartUploadAfter === undefined && rule.expiration === undefined && rule.expirationDate === undefined && rule.expiredObjectDeleteMarker === undefined && rule.noncurrentVersionExpiration === undefined && rule.noncurrentVersionsToRetain === undefined && rule.noncurrentVersionTransitions === undefined && rule.transitions === undefined ) { 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`'); } ``` ### Description of how you validated changes Unit tests. ### 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 fccb006 commit e15626e

File tree

2 files changed

+173
-1
lines changed

2 files changed

+173
-1
lines changed

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

+13
Original file line numberDiff line numberDiff line change
@@ -2254,6 +2254,19 @@ export class Bucket extends BucketBase {
22542254
throw new Error('ExpiredObjectDeleteMarker cannot be specified with expiration, ExpirationDate, or TagFilters.');
22552255
}
22562256

2257+
if (
2258+
rule.abortIncompleteMultipartUploadAfter === undefined &&
2259+
rule.expiration === undefined &&
2260+
rule.expirationDate === undefined &&
2261+
rule.expiredObjectDeleteMarker === undefined &&
2262+
rule.noncurrentVersionExpiration === undefined &&
2263+
rule.noncurrentVersionsToRetain === undefined &&
2264+
rule.noncurrentVersionTransitions === undefined &&
2265+
rule.transitions === undefined
2266+
) {
2267+
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`');
2268+
}
2269+
22572270
const x: CfnBucket.RuleProperty = {
22582271
// eslint-disable-next-line max-len
22592272
abortIncompleteMultipartUpload: rule.abortIncompleteMultipartUploadAfter !== undefined ? { daysAfterInitiation: rule.abortIncompleteMultipartUploadAfter.toDays() } : undefined,

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

+160-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Template } from '../../assertions';
2-
import { Duration, Stack } from '../../core';
2+
import { App, Duration, Stack } from '../../core';
33
import { Bucket, StorageClass } from '../lib';
44

55
describe('rules', () => {
@@ -320,6 +320,7 @@ describe('rules', () => {
320320
lifecycleRules: [{
321321
objectSizeLessThan: 0,
322322
objectSizeGreaterThan: 0,
323+
expiration: Duration.days(30),
323324
}],
324325
});
325326

@@ -329,9 +330,167 @@ describe('rules', () => {
329330
Rules: [{
330331
ObjectSizeLessThan: 0,
331332
ObjectSizeGreaterThan: 0,
333+
ExpirationInDays: 30,
332334
Status: 'Enabled',
333335
}],
334336
},
335337
});
336338
});
339+
340+
describe('required properties for rules', () => {
341+
test('throw if there is a rule doesn\'t have required properties', () => {
342+
const stack = new Stack();
343+
new Bucket(stack, 'MyBucket', {
344+
lifecycleRules: [
345+
{
346+
objectSizeLessThan: 300000,
347+
objectSizeGreaterThan: 200000,
348+
},
349+
],
350+
});
351+
expect(() => {
352+
Template.fromStack(stack);
353+
}).toThrow(/All rules for `lifecycleRules` must have at least one of the following properties: `abortIncompleteMultipartUploadAfter`, `expiration`, `expirationDate`, `expiredObjectDeleteMarker`, `noncurrentVersionExpiration`, `noncurrentVersionsToRetain`, `noncurrentVersionTransitions`, or `transitions`/);
354+
});
355+
356+
test('throw if there are a valid rule and a rule that doesn\'t have required properties.', () => {
357+
const stack = new Stack();
358+
new Bucket(stack, 'MyBucket', {
359+
lifecycleRules: [
360+
{
361+
abortIncompleteMultipartUploadAfter: Duration.days(365),
362+
},
363+
{
364+
objectSizeLessThan: 300000,
365+
objectSizeGreaterThan: 200000,
366+
},
367+
],
368+
});
369+
expect(() => {
370+
Template.fromStack(stack);
371+
}).toThrow(/All rules for `lifecycleRules` must have at least one of the following properties: `abortIncompleteMultipartUploadAfter`, `expiration`, `expirationDate`, `expiredObjectDeleteMarker`, `noncurrentVersionExpiration`, `noncurrentVersionsToRetain`, `noncurrentVersionTransitions`, or `transitions`/);
372+
});
373+
374+
test('don\'t throw with abortIncompleteMultipartUploadAfter', () => {
375+
const stack = new Stack();
376+
new Bucket(stack, 'MyBucket', {
377+
lifecycleRules: [
378+
{
379+
abortIncompleteMultipartUploadAfter: Duration.days(365),
380+
},
381+
],
382+
});
383+
expect(() => {
384+
Template.fromStack(stack);
385+
}).not.toThrow();
386+
});
387+
388+
test('don\'t throw with expiration', () => {
389+
const stack = new Stack();
390+
new Bucket(stack, 'MyBucket', {
391+
lifecycleRules: [
392+
{
393+
expiration: Duration.days(365),
394+
},
395+
],
396+
});
397+
expect(() => {
398+
Template.fromStack(stack);
399+
}).not.toThrow();
400+
});
401+
402+
test('don\'t throw with expirationDate', () => {
403+
const stack = new Stack();
404+
new Bucket(stack, 'MyBucket', {
405+
lifecycleRules: [
406+
{
407+
expirationDate: new Date('2024-01-01'),
408+
},
409+
],
410+
});
411+
expect(() => {
412+
Template.fromStack(stack);
413+
}).not.toThrow();
414+
});
415+
416+
test('don\'t throw with expiredObjectDeleteMarker', () => {
417+
const stack = new Stack();
418+
new Bucket(stack, 'MyBucket', {
419+
lifecycleRules: [
420+
{
421+
expiredObjectDeleteMarker: true,
422+
},
423+
],
424+
});
425+
expect(() => {
426+
Template.fromStack(stack);
427+
}).not.toThrow();
428+
});
429+
430+
test('don\'t throw with noncurrentVersionExpiration', () => {
431+
const stack = new Stack();
432+
new Bucket(stack, 'MyBucket', {
433+
lifecycleRules: [
434+
{
435+
noncurrentVersionExpiration: Duration.days(365),
436+
},
437+
],
438+
});
439+
expect(() => {
440+
Template.fromStack(stack);
441+
}).not.toThrow();
442+
});
443+
444+
test('don\'t throw with noncurrentVersionsToRetain', () => {
445+
const stack = new Stack();
446+
new Bucket(stack, 'MyBucket', {
447+
lifecycleRules: [
448+
{
449+
noncurrentVersionsToRetain: 10,
450+
},
451+
],
452+
});
453+
expect(() => {
454+
Template.fromStack(stack);
455+
}).not.toThrow();
456+
});
457+
458+
test('don\'t throw with noncurrentVersionTransitions', () => {
459+
const stack = new Stack();
460+
new Bucket(stack, 'MyBucket', {
461+
lifecycleRules: [
462+
{
463+
noncurrentVersionTransitions: [
464+
{
465+
storageClass: StorageClass.GLACIER_INSTANT_RETRIEVAL,
466+
transitionAfter: Duration.days(10),
467+
noncurrentVersionsToRetain: 1,
468+
},
469+
],
470+
},
471+
],
472+
});
473+
expect(() => {
474+
Template.fromStack(stack);
475+
}).not.toThrow();
476+
});
477+
478+
test('don\'t throw with transitions', () => {
479+
const stack = new Stack();
480+
new Bucket(stack, 'MyBucket', {
481+
lifecycleRules: [
482+
{
483+
transitions: [{
484+
storageClass: StorageClass.GLACIER,
485+
transitionAfter: Duration.days(30),
486+
}],
487+
},
488+
],
489+
});
490+
expect(() => {
491+
Template.fromStack(stack);
492+
}).not.toThrow();
493+
});
494+
495+
});
337496
});

0 commit comments

Comments
 (0)