Skip to content

Commit 85604d9

Browse files
authored
feat(s3): add noncurrentVersionsToRetain property to lifecycle rule (#20348)
Fixes #19784 ---- ### All Submissions: * [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md/#adding-new-unconventional-dependencies) ### New Features * [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/master/INTEGRATION_TESTS.md)? * [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent a6d830e commit 85604d9

File tree

10 files changed

+284
-3
lines changed

10 files changed

+284
-3
lines changed

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

+36
Original file line numberDiff line numberDiff line change
@@ -551,3 +551,39 @@ bucket.transferAccelerationUrlForObject('objectname');
551551
});
552552

553553
```
554+
555+
## Lifecycle Rule
556+
557+
[Managing lifecycle](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html) can be configured transition or expiration actions.
558+
559+
```ts
560+
const bucket = new s3.Bucket(this, 'MyBucket', {
561+
lifecycleRules: [{
562+
abortIncompleteMultipartUploadAfter: cdk.Duration.minutes(30),
563+
enabled: false,
564+
expiration: cdk.Duration.days(30),
565+
expirationDate: new Date(),
566+
expiredObjectDeleteMarker: false,
567+
id: 'id',
568+
noncurrentVersionExpiration: cdk.Duration.days(30),
569+
570+
// the properties below are optional
571+
noncurrentVersionsToRetain: 123,
572+
noncurrentVersionTransitions: [{
573+
storageClass: s3.StorageClass.GLACIER,
574+
transitionAfter: cdk.Duration.days(30),
575+
576+
// the properties below are optional
577+
noncurrentVersionsToRetain: 123,
578+
}],
579+
prefix: 'prefix',
580+
transitions: [{
581+
storageClass: s3.StorageClass.GLACIER,
582+
583+
// the properties below are optional
584+
transitionAfter: cdk.Duration.days(30),
585+
transitionDate: new Date(),
586+
}],
587+
}]
588+
});
589+
```

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -1905,7 +1905,10 @@ export class Bucket extends BucketBase {
19051905
expirationDate: rule.expirationDate,
19061906
expirationInDays: rule.expiration?.toDays(),
19071907
id: rule.id,
1908-
noncurrentVersionExpirationInDays: rule.noncurrentVersionExpiration && rule.noncurrentVersionExpiration.toDays(),
1908+
noncurrentVersionExpiration: rule.noncurrentVersionExpiration && {
1909+
noncurrentDays: rule.noncurrentVersionExpiration.toDays(),
1910+
newerNoncurrentVersions: rule.noncurrentVersionsToRetain,
1911+
},
19091912
noncurrentVersionTransitions: mapOrUndefined(rule.noncurrentVersionTransitions, t => ({
19101913
storageClass: t.storageClass.value,
19111914
transitionInDays: t.transitionAfter.toDays(),

packages/@aws-cdk/aws-s3/lib/rule.ts

+10
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ export interface LifecycleRule {
6666
*/
6767
readonly noncurrentVersionExpiration?: Duration;
6868

69+
/**
70+
* Indicates a maximum number of noncurrent versions to retain.
71+
*
72+
* If there are this many more noncurrent versions,
73+
* Amazon S3 permanently deletes them.
74+
*
75+
* @default No noncurrent versions to retain
76+
*/
77+
readonly noncurrentVersionsToRetain?: number;
78+
6979
/**
7080
* One or more transition rules that specify when non-current objects transition to a specified storage class.
7181
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { App, Duration, Stack } from '@aws-cdk/core';
2+
import { Bucket } from '../lib';
3+
4+
const app = new App();
5+
6+
const stack = new Stack(app, 'aws-cdk-s3');
7+
8+
new Bucket(stack, 'MyBucket', {
9+
lifecycleRules: [{
10+
noncurrentVersionExpiration: Duration.days(30),
11+
noncurrentVersionsToRetain: 123,
12+
}],
13+
versioned: true,
14+
});
15+
16+
app.synth();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"Resources": {
3+
"MyBucketF68F3FF0": {
4+
"Type": "AWS::S3::Bucket",
5+
"Properties": {
6+
"LifecycleConfiguration": {
7+
"Rules": [
8+
{
9+
"NoncurrentVersionExpiration": {
10+
"NewerNoncurrentVersions": 123,
11+
"NoncurrentDays": 30
12+
},
13+
"Status": "Enabled"
14+
}
15+
]
16+
},
17+
"VersioningConfiguration": {
18+
"Status": "Enabled"
19+
}
20+
},
21+
"UpdateReplacePolicy": "Retain",
22+
"DeletionPolicy": "Retain"
23+
}
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"version":"19.0.0"}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"version": "19.0.0",
3+
"testCases": {
4+
"aws-s3/test/integ.lifecycle-expiration": {
5+
"stacks": [
6+
"aws-cdk-s3"
7+
],
8+
"diffAssets": false,
9+
"stackUpdateWorkflow": true
10+
}
11+
},
12+
"synthContext": {},
13+
"enableLookups": false
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"version": "19.0.0",
3+
"artifacts": {
4+
"Tree": {
5+
"type": "cdk:tree",
6+
"properties": {
7+
"file": "tree.json"
8+
}
9+
},
10+
"aws-cdk-s3": {
11+
"type": "aws:cloudformation:stack",
12+
"environment": "aws://unknown-account/unknown-region",
13+
"properties": {
14+
"templateFile": "aws-cdk-s3.template.json",
15+
"validateOnSynth": false
16+
},
17+
"metadata": {
18+
"/aws-cdk-s3/MyBucket/Resource": [
19+
{
20+
"type": "aws:cdk:logicalId",
21+
"data": "MyBucketF68F3FF0"
22+
}
23+
]
24+
},
25+
"displayName": "aws-cdk-s3"
26+
}
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"version": "tree-0.1",
3+
"tree": {
4+
"id": "App",
5+
"path": "",
6+
"children": {
7+
"Tree": {
8+
"id": "Tree",
9+
"path": "Tree",
10+
"constructInfo": {
11+
"fqn": "@aws-cdk/core.Construct",
12+
"version": "0.0.0"
13+
}
14+
},
15+
"aws-cdk-s3": {
16+
"id": "aws-cdk-s3",
17+
"path": "aws-cdk-s3",
18+
"children": {
19+
"MyBucket": {
20+
"id": "MyBucket",
21+
"path": "aws-cdk-s3/MyBucket",
22+
"children": {
23+
"Resource": {
24+
"id": "Resource",
25+
"path": "aws-cdk-s3/MyBucket/Resource",
26+
"attributes": {
27+
"aws:cdk:cloudformation:type": "AWS::S3::Bucket",
28+
"aws:cdk:cloudformation:props": {
29+
"lifecycleConfiguration": {
30+
"rules": [
31+
{
32+
"noncurrentVersionExpiration": {
33+
"noncurrentDays": 30,
34+
"newerNoncurrentVersions": 123
35+
},
36+
"status": "Enabled"
37+
}
38+
]
39+
},
40+
"versioningConfiguration": {
41+
"status": "Enabled"
42+
}
43+
}
44+
},
45+
"constructInfo": {
46+
"fqn": "@aws-cdk/aws-s3.CfnBucket",
47+
"version": "0.0.0"
48+
}
49+
}
50+
},
51+
"constructInfo": {
52+
"fqn": "@aws-cdk/aws-s3.Bucket",
53+
"version": "0.0.0"
54+
}
55+
}
56+
},
57+
"constructInfo": {
58+
"fqn": "@aws-cdk/core.Stack",
59+
"version": "0.0.0"
60+
}
61+
}
62+
},
63+
"constructInfo": {
64+
"fqn": "@aws-cdk/core.App",
65+
"version": "0.0.0"
66+
}
67+
}
68+
}

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

+82-2
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,9 @@ describe('rules', () => {
163163
Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', {
164164
LifecycleConfiguration: {
165165
Rules: [{
166-
NoncurrentVersionExpirationInDays: 10,
166+
NoncurrentVersionExpiration: {
167+
NoncurrentDays: 10,
168+
},
167169
NoncurrentVersionTransitions: [
168170
{
169171
NewerNoncurrentVersions: 1,
@@ -199,7 +201,85 @@ describe('rules', () => {
199201
Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', {
200202
LifecycleConfiguration: {
201203
Rules: [{
202-
NoncurrentVersionExpirationInDays: 10,
204+
NoncurrentVersionExpiration: {
205+
NoncurrentDays: 10,
206+
},
207+
NoncurrentVersionTransitions: [
208+
{
209+
StorageClass: 'GLACIER_IR',
210+
TransitionInDays: 10,
211+
},
212+
],
213+
Status: 'Enabled',
214+
}],
215+
},
216+
});
217+
});
218+
219+
test('Noncurrent expiration rule with versions to retain', () => {
220+
// GIVEN
221+
const stack = new Stack();
222+
223+
// WHEN: Noncurrent version to retain available
224+
new Bucket(stack, 'Bucket1', {
225+
versioned: true,
226+
lifecycleRules: [{
227+
noncurrentVersionExpiration: Duration.days(10),
228+
noncurrentVersionsToRetain: 1,
229+
noncurrentVersionTransitions: [
230+
{
231+
storageClass: StorageClass.GLACIER_INSTANT_RETRIEVAL,
232+
transitionAfter: Duration.days(10),
233+
},
234+
],
235+
}],
236+
});
237+
238+
// THEN
239+
Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', {
240+
LifecycleConfiguration: {
241+
Rules: [{
242+
NoncurrentVersionExpiration: {
243+
NoncurrentDays: 10,
244+
NewerNoncurrentVersions: 1,
245+
},
246+
NoncurrentVersionTransitions: [
247+
{
248+
StorageClass: 'GLACIER_IR',
249+
TransitionInDays: 10,
250+
},
251+
],
252+
Status: 'Enabled',
253+
}],
254+
},
255+
});
256+
});
257+
258+
test('Noncurrent expiration rule without versions to retain', () => {
259+
// GIVEN
260+
const stack = new Stack();
261+
262+
// WHEN: Noncurrent version to retain not set
263+
new Bucket(stack, 'Bucket1', {
264+
versioned: true,
265+
lifecycleRules: [{
266+
noncurrentVersionExpiration: Duration.days(10),
267+
noncurrentVersionTransitions: [
268+
{
269+
storageClass: StorageClass.GLACIER_INSTANT_RETRIEVAL,
270+
transitionAfter: Duration.days(10),
271+
},
272+
],
273+
}],
274+
});
275+
276+
// THEN
277+
Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', {
278+
LifecycleConfiguration: {
279+
Rules: [{
280+
NoncurrentVersionExpiration: {
281+
NoncurrentDays: 10,
282+
},
203283
NoncurrentVersionTransitions: [
204284
{
205285
StorageClass: 'GLACIER_IR',

0 commit comments

Comments
 (0)