Skip to content

Commit

Permalink
chore(s3): add validation on required properties for lifecycle rules (#…
Browse files Browse the repository at this point in the history
…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*
  • Loading branch information
go-to-k authored Oct 18, 2024
1 parent fccb006 commit e15626e
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 1 deletion.
13 changes: 13 additions & 0 deletions packages/aws-cdk-lib/aws-s3/lib/bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2254,6 +2254,19 @@ export class Bucket extends BucketBase {
throw new Error('ExpiredObjectDeleteMarker cannot be specified with expiration, ExpirationDate, or TagFilters.');
}

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`');
}

const x: CfnBucket.RuleProperty = {
// eslint-disable-next-line max-len
abortIncompleteMultipartUpload: rule.abortIncompleteMultipartUploadAfter !== undefined ? { daysAfterInitiation: rule.abortIncompleteMultipartUploadAfter.toDays() } : undefined,
Expand Down
161 changes: 160 additions & 1 deletion packages/aws-cdk-lib/aws-s3/test/rules.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -320,6 +320,7 @@ describe('rules', () => {
lifecycleRules: [{
objectSizeLessThan: 0,
objectSizeGreaterThan: 0,
expiration: Duration.days(30),
}],
});

Expand All @@ -329,9 +330,167 @@ describe('rules', () => {
Rules: [{
ObjectSizeLessThan: 0,
ObjectSizeGreaterThan: 0,
ExpirationInDays: 30,
Status: 'Enabled',
}],
},
});
});

describe('required properties for rules', () => {
test('throw if there is a rule doesn\'t have required properties', () => {
const stack = new Stack();
new Bucket(stack, 'MyBucket', {
lifecycleRules: [
{
objectSizeLessThan: 300000,
objectSizeGreaterThan: 200000,
},
],
});
expect(() => {
Template.fromStack(stack);
}).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 stack = new Stack();
new Bucket(stack, 'MyBucket', {
lifecycleRules: [
{
abortIncompleteMultipartUploadAfter: Duration.days(365),
},
{
objectSizeLessThan: 300000,
objectSizeGreaterThan: 200000,
},
],
});
expect(() => {
Template.fromStack(stack);
}).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();
});

});
});

0 comments on commit e15626e

Please sign in to comment.