-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli): garbage collect s3 assets (under
--unstable
flag) (#31611)
## S3 Asset Garbage Collection This PR introduces a new CLI command under the new `--unstable` flag. This flag ensures that users understand and opt-in to experimental or incomplete CLI features. `cdk gc` will garbage collect unused assets in your bootstrapped S3 bucket. It goes through each object in the bucket, checks to see if the asset hash shows up in a cloudformation stack, and if not, tags the object as unused and/or deletes the object (depending on your configuration). ## **THIS COMMAND WILL DELETE OBJECTS IN YOUR BOOTSTRAPPED S3 BUCKET** basic garbage collection (immediately deletes objects that are unused): ```bash cdk gc aws://0123456789012/us-east-1 \ --unstable='gc' \ --type='s3' ``` garbage collection with a buffer (deletes unused objects > # of days specified): ```bash cdk gc aws://0123456789012/us-east-1 \ --unstable='gc' \ --type='s3' \ --rollback-buffer-days=30 ``` garbage collection with a created at buffer (deletes unused objects only if they have lived longer than this many days): ```bash cdk gc aws://0123456789012/us-east-1 \ --unstable='gc' \ --type='s3' \ --created-buffer-days=5 ``` garbage collect a specific bootstrap stack: ```bash cdk gc aws://0123456789012/us-east-1 \ --unstable='gc' \ --type='s3' \ --bootstrap-stack-name=cdktest-0lc2i3vebi7-bootstrap-stack ``` before actually deleting your assets, you will be prompted one last time: ```bash Found 1 objects to delete based off of the following criteria: - objects have been isolated for > 0 days - objects were created > 0 days ago Delete this batch (yes/no/delete-all)? ``` To disable this, specify the `--skip-delete-prompt` option. ## Todo in another PR - [ ] ECR asset collection ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
- Loading branch information
Showing
11 changed files
with
1,940 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
202 changes: 202 additions & 0 deletions
202
packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/garbage-collection.integtest.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
import { GetObjectTaggingCommand, ListObjectsV2Command, PutObjectTaggingCommand } from '@aws-sdk/client-s3'; | ||
import { integTest, randomString, withoutBootstrap } from '../../lib'; | ||
|
||
jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime | ||
|
||
integTest( | ||
'Garbage Collection deletes unused assets', | ||
withoutBootstrap(async (fixture) => { | ||
const toolkitStackName = fixture.bootstrapStackName; | ||
const bootstrapBucketName = `aws-cdk-garbage-collect-integ-test-bckt-${randomString()}`; | ||
fixture.rememberToDeleteBucket(bootstrapBucketName); // just in case | ||
|
||
await fixture.cdkBootstrapModern({ | ||
toolkitStackName, | ||
bootstrapBucketName, | ||
}); | ||
|
||
await fixture.cdkDeploy('lambda', { | ||
options: [ | ||
'--context', `bootstrapBucket=${bootstrapBucketName}`, | ||
'--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, | ||
'--toolkit-stack-name', toolkitStackName, | ||
'--force', | ||
], | ||
}); | ||
fixture.log('Setup complete!'); | ||
|
||
await fixture.cdkDestroy('lambda', { | ||
options: [ | ||
'--context', `bootstrapBucket=${bootstrapBucketName}`, | ||
'--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, | ||
'--toolkit-stack-name', toolkitStackName, | ||
'--force', | ||
], | ||
}); | ||
|
||
await fixture.cdkGarbageCollect({ | ||
rollbackBufferDays: 0, | ||
type: 's3', | ||
bootstrapStackName: toolkitStackName, | ||
}); | ||
fixture.log('Garbage collection complete!'); | ||
|
||
// assert that the bootstrap bucket is empty | ||
await fixture.aws.s3.send(new ListObjectsV2Command({ Bucket: bootstrapBucketName })) | ||
.then((result) => { | ||
expect(result.Contents).toBeUndefined(); | ||
}); | ||
}), | ||
); | ||
|
||
integTest( | ||
'Garbage Collection keeps in use assets', | ||
withoutBootstrap(async (fixture) => { | ||
const toolkitStackName = fixture.bootstrapStackName; | ||
const bootstrapBucketName = `aws-cdk-garbage-collect-integ-test-bckt-${randomString()}`; | ||
fixture.rememberToDeleteBucket(bootstrapBucketName); // just in case | ||
|
||
await fixture.cdkBootstrapModern({ | ||
toolkitStackName, | ||
bootstrapBucketName, | ||
}); | ||
|
||
await fixture.cdkDeploy('lambda', { | ||
options: [ | ||
'--context', `bootstrapBucket=${bootstrapBucketName}`, | ||
'--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, | ||
'--toolkit-stack-name', toolkitStackName, | ||
'--force', | ||
], | ||
}); | ||
fixture.log('Setup complete!'); | ||
|
||
await fixture.cdkGarbageCollect({ | ||
rollbackBufferDays: 0, | ||
type: 's3', | ||
bootstrapStackName: toolkitStackName, | ||
}); | ||
fixture.log('Garbage collection complete!'); | ||
|
||
// assert that the bootstrap bucket has the object | ||
await fixture.aws.s3.send(new ListObjectsV2Command({ Bucket: bootstrapBucketName })) | ||
.then((result) => { | ||
expect(result.Contents).toHaveLength(1); | ||
}); | ||
|
||
await fixture.cdkDestroy('lambda', { | ||
options: [ | ||
'--context', `bootstrapBucket=${bootstrapBucketName}`, | ||
'--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, | ||
'--toolkit-stack-name', toolkitStackName, | ||
'--force', | ||
], | ||
}); | ||
fixture.log('Teardown complete!'); | ||
}), | ||
); | ||
|
||
integTest( | ||
'Garbage Collection tags unused assets', | ||
withoutBootstrap(async (fixture) => { | ||
const toolkitStackName = fixture.bootstrapStackName; | ||
const bootstrapBucketName = `aws-cdk-garbage-collect-integ-test-bckt-${randomString()}`; | ||
fixture.rememberToDeleteBucket(bootstrapBucketName); // just in case | ||
|
||
await fixture.cdkBootstrapModern({ | ||
toolkitStackName, | ||
bootstrapBucketName, | ||
}); | ||
|
||
await fixture.cdkDeploy('lambda', { | ||
options: [ | ||
'--context', `bootstrapBucket=${bootstrapBucketName}`, | ||
'--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, | ||
'--toolkit-stack-name', toolkitStackName, | ||
'--force', | ||
], | ||
}); | ||
fixture.log('Setup complete!'); | ||
|
||
await fixture.cdkDestroy('lambda', { | ||
options: [ | ||
'--context', `bootstrapBucket=${bootstrapBucketName}`, | ||
'--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, | ||
'--toolkit-stack-name', toolkitStackName, | ||
'--force', | ||
], | ||
}); | ||
|
||
await fixture.cdkGarbageCollect({ | ||
rollbackBufferDays: 100, // this will ensure that we do not delete assets immediately (and just tag them) | ||
type: 's3', | ||
bootstrapStackName: toolkitStackName, | ||
}); | ||
fixture.log('Garbage collection complete!'); | ||
|
||
// assert that the bootstrap bucket has the object and is tagged | ||
await fixture.aws.s3.send(new ListObjectsV2Command({ Bucket: bootstrapBucketName })) | ||
.then(async (result) => { | ||
expect(result.Contents).toHaveLength(2); // also the CFN template | ||
const key = result.Contents![0].Key; | ||
const tags = await fixture.aws.s3.send(new GetObjectTaggingCommand({ Bucket: bootstrapBucketName, Key: key })); | ||
expect(tags.TagSet).toHaveLength(1); | ||
}); | ||
}), | ||
); | ||
|
||
integTest( | ||
'Garbage Collection untags in-use assets', | ||
withoutBootstrap(async (fixture) => { | ||
const toolkitStackName = fixture.bootstrapStackName; | ||
const bootstrapBucketName = `aws-cdk-garbage-collect-integ-test-bckt-${randomString()}`; | ||
fixture.rememberToDeleteBucket(bootstrapBucketName); // just in case | ||
|
||
await fixture.cdkBootstrapModern({ | ||
toolkitStackName, | ||
bootstrapBucketName, | ||
}); | ||
|
||
await fixture.cdkDeploy('lambda', { | ||
options: [ | ||
'--context', `bootstrapBucket=${bootstrapBucketName}`, | ||
'--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, | ||
'--toolkit-stack-name', toolkitStackName, | ||
'--force', | ||
], | ||
}); | ||
fixture.log('Setup complete!'); | ||
|
||
// Artificially add tagging to the asset in the bootstrap bucket | ||
const result = await fixture.aws.s3.send(new ListObjectsV2Command({ Bucket: bootstrapBucketName })); | ||
const key = result.Contents!.filter((c) => c.Key?.split('.')[1] == 'zip')[0].Key; // fancy footwork to make sure we have the asset key | ||
await fixture.aws.s3.send(new PutObjectTaggingCommand({ | ||
Bucket: bootstrapBucketName, | ||
Key: key, | ||
Tagging: { | ||
TagSet: [{ | ||
Key: 'aws-cdk:isolated', | ||
Value: '12345', | ||
}, { | ||
Key: 'bogus', | ||
Value: 'val', | ||
}], | ||
}, | ||
})); | ||
|
||
await fixture.cdkGarbageCollect({ | ||
rollbackBufferDays: 100, // this will ensure that we do not delete assets immediately (and just tag them) | ||
type: 's3', | ||
bootstrapStackName: toolkitStackName, | ||
}); | ||
fixture.log('Garbage collection complete!'); | ||
|
||
// assert that the isolated object tag is removed while the other tag remains | ||
const newTags = await fixture.aws.s3.send(new GetObjectTaggingCommand({ Bucket: bootstrapBucketName, Key: key })); | ||
|
||
expect(newTags.TagSet).toEqual([{ | ||
Key: 'bogus', | ||
Value: 'val', | ||
}]); | ||
}), | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.