aws-cdk
Version:
CDK Toolkit, the command line tool for CDK apps
840 lines • 104 kB
JavaScript
"use strict";
/* eslint-disable import/order */
Object.defineProperty(exports, "__esModule", { value: true });
const client_cloudformation_1 = require("@aws-sdk/client-cloudformation");
const api_1 = require("../../lib/api");
const mock_sdk_1 = require("../util/mock-sdk");
const client_s3_1 = require("@aws-sdk/client-s3");
const stack_refresh_1 = require("../../lib/api/garbage-collection/stack-refresh");
const client_ecr_1 = require("@aws-sdk/client-ecr");
let garbageCollector;
let stderrMock;
const cfnClient = mock_sdk_1.mockCloudFormationClient;
const s3Client = mock_sdk_1.mockS3Client;
const ecrClient = mock_sdk_1.mockECRClient;
const DAY = 24 * 60 * 60 * 1000; // Number of milliseconds in a day
beforeEach(() => {
// By default, we'll return a non-found toolkit info
jest.spyOn(api_1.ToolkitInfo, 'lookup').mockResolvedValue(api_1.ToolkitInfo.bootstrapStackNotFoundInfo('GarbageStack'));
// Suppress stderr to not spam output during tests
stderrMock = jest.spyOn(process.stderr, 'write').mockImplementation(() => {
return true;
});
prepareDefaultCfnMock();
prepareDefaultS3Mock();
prepareDefaultEcrMock();
});
afterEach(() => {
stderrMock.mockReset();
});
function mockTheToolkitInfo(stackProps) {
jest.spyOn(api_1.ToolkitInfo, 'lookup').mockResolvedValue(api_1.ToolkitInfo.fromStack((0, mock_sdk_1.mockBootstrapStack)(stackProps)));
}
function gc(props) {
return new api_1.GarbageCollector({
sdkProvider: new mock_sdk_1.MockSdkProvider(),
action: props.action,
resolvedEnvironment: {
account: '123456789012',
region: 'us-east-1',
name: 'mock',
},
bootstrapStackName: 'GarbageStack',
rollbackBufferDays: props.rollbackBufferDays ?? 0,
createdBufferDays: props.createdAtBufferDays ?? 0,
type: props.type,
confirm: false,
});
}
describe('S3 Garbage Collection', () => {
test('rollbackBufferDays = 0 -- assets to be deleted', async () => {
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
garbageCollector = gc({
type: 's3',
rollbackBufferDays: 0,
action: 'full',
});
await garbageCollector.garbageCollect();
expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1);
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.ListObjectsV2Command, 2);
// no tagging
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.GetObjectTaggingCommand, 0);
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.PutObjectTaggingCommand, 0);
// assets are to be deleted
expect(s3Client).toHaveReceivedCommandWith(client_s3_1.DeleteObjectsCommand, {
Bucket: 'BUCKET_NAME',
Delete: {
Objects: [
{ Key: 'asset1' },
{ Key: 'asset2' },
{ Key: 'asset3' },
],
Quiet: true,
},
});
});
test('rollbackBufferDays > 0 -- assets to be tagged', async () => {
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
garbageCollector = gc({
type: 's3',
rollbackBufferDays: 3,
action: 'full',
});
await garbageCollector.garbageCollect();
expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1);
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.ListObjectsV2Command, 2);
// assets tagged
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.GetObjectTaggingCommand, 3);
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.PutObjectTaggingCommand, 2);
// no deleting
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.DeleteObjectsCommand, 0);
});
test('createdAtBufferDays > 0', async () => {
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
garbageCollector = gc({
type: 's3',
rollbackBufferDays: 0,
createdAtBufferDays: 5,
action: 'full',
});
await garbageCollector.garbageCollect();
expect(s3Client).toHaveReceivedCommandWith(client_s3_1.DeleteObjectsCommand, {
Bucket: 'BUCKET_NAME',
Delete: {
Objects: [
// asset1 not deleted because it is too young
{ Key: 'asset2' },
{ Key: 'asset3' },
],
Quiet: true,
},
});
});
test('action = print -- does not tag or delete', async () => {
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
garbageCollector = gc({
type: 's3',
rollbackBufferDays: 3,
action: 'print',
});
await garbageCollector.garbageCollect();
expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1);
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.ListObjectsV2Command, 2);
// get tags, but dont put tags
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.GetObjectTaggingCommand, 3);
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.PutObjectTaggingCommand, 0);
// no deleting
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.DeleteObjectsCommand, 0);
});
test('action = tag -- does not delete', async () => {
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
garbageCollector = gc({
type: 's3',
rollbackBufferDays: 3,
action: 'tag',
});
await garbageCollector.garbageCollect();
expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1);
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.ListObjectsV2Command, 2);
// tags objects
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.GetObjectTaggingCommand, 3);
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.PutObjectTaggingCommand, 2); // one object already has the tag
// no deleting
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.DeleteObjectsCommand, 0);
});
test('action = delete-tagged -- does not tag', async () => {
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
garbageCollector = gc({
type: 's3',
rollbackBufferDays: 3,
action: 'delete-tagged',
});
await garbageCollector.garbageCollect();
expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1);
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.ListObjectsV2Command, 2);
// get tags, but dont put tags
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.GetObjectTaggingCommand, 3);
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.PutObjectTaggingCommand, 0);
});
test('ignore objects that are modified after gc start', async () => {
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
s3Client.on(client_s3_1.ListObjectsV2Command).resolves({
Contents: [
{ Key: 'asset1', LastModified: new Date(0) },
{ Key: 'asset2', LastModified: new Date(0) },
{ Key: 'asset3', LastModified: new Date(new Date().setFullYear(new Date().getFullYear() + 1)) }, // future date ignored everywhere
],
KeyCount: 3,
});
garbageCollector = gc({
type: 's3',
rollbackBufferDays: 0,
action: 'full',
});
await garbageCollector.garbageCollect();
// assets are to be deleted
expect(s3Client).toHaveReceivedCommandWith(client_s3_1.DeleteObjectsCommand, {
Bucket: 'BUCKET_NAME',
Delete: {
Objects: [
{ Key: 'asset1' },
{ Key: 'asset2' },
// no asset3
],
Quiet: true,
},
});
});
});
describe('ECR Garbage Collection', () => {
test('rollbackBufferDays = 0 -- assets to be deleted', async () => {
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
garbageCollector = gc({
type: 'ecr',
rollbackBufferDays: 0,
action: 'full',
});
await garbageCollector.garbageCollect();
expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.DescribeImagesCommand, 1);
expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.ListImagesCommand, 2);
// no tagging
expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.PutImageCommand, 0);
// assets are to be deleted
expect(ecrClient).toHaveReceivedCommandWith(client_ecr_1.BatchDeleteImageCommand, {
repositoryName: 'REPO_NAME',
imageIds: [
{ imageDigest: 'digest3' },
{ imageDigest: 'digest2' },
],
});
});
test('rollbackBufferDays > 0 -- assets to be tagged', async () => {
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
garbageCollector = gc({
type: 'ecr',
rollbackBufferDays: 3,
action: 'full',
});
await garbageCollector.garbageCollect();
// assets tagged
expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.PutImageCommand, 2);
// no deleting
expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.BatchDeleteImageCommand, 0);
});
test('createdAtBufferDays > 0', async () => {
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
garbageCollector = gc({
type: 'ecr',
rollbackBufferDays: 0,
createdAtBufferDays: 5,
action: 'full',
});
await garbageCollector.garbageCollect();
expect(ecrClient).toHaveReceivedCommandWith(client_ecr_1.BatchDeleteImageCommand, {
repositoryName: 'REPO_NAME',
imageIds: [
// digest3 is too young to be deleted
{ imageDigest: 'digest2' },
],
});
});
test('action = print -- does not tag or delete', async () => {
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
garbageCollector = gc({
type: 'ecr',
rollbackBufferDays: 3,
action: 'print',
});
await garbageCollector.garbageCollect();
expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1);
// dont put tags
expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.PutImageCommand, 0);
// no deleting
expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.BatchDeleteImageCommand, 0);
});
test('action = tag -- does not delete', async () => {
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
garbageCollector = gc({
type: 'ecr',
rollbackBufferDays: 3,
action: 'tag',
});
await garbageCollector.garbageCollect();
expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1);
// tags objects
expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.PutImageCommand, 2);
// no deleting
expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.BatchDeleteImageCommand, 0);
});
test('action = delete-tagged -- does not tag', async () => {
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
garbageCollector = gc({
type: 'ecr',
rollbackBufferDays: 3,
action: 'delete-tagged',
});
await garbageCollector.garbageCollect();
expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1);
// dont put tags
expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.PutImageCommand, 0);
});
test('ignore images that are modified after gc start', async () => {
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
prepareDefaultEcrMock();
ecrClient.on(client_ecr_1.DescribeImagesCommand).resolves({
imageDetails: [
{
imageDigest: 'digest3',
imageTags: ['klmno'],
imagePushedAt: daysInThePast(2),
imageSizeInBytes: 100,
},
{
imageDigest: 'digest2',
imageTags: ['fghij'],
imagePushedAt: yearsInTheFuture(1),
imageSizeInBytes: 300000000,
},
{
imageDigest: 'digest1',
imageTags: ['abcde'],
imagePushedAt: daysInThePast(100),
imageSizeInBytes: 1000000000,
},
],
});
prepareDefaultCfnMock();
garbageCollector = gc({
type: 'ecr',
rollbackBufferDays: 0,
action: 'full',
});
await garbageCollector.garbageCollect();
// assets are to be deleted
expect(ecrClient).toHaveReceivedCommandWith(client_ecr_1.BatchDeleteImageCommand, {
repositoryName: 'REPO_NAME',
imageIds: [
{ imageDigest: 'digest3' },
],
});
});
test('succeeds when no images are present', async () => {
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
prepareDefaultEcrMock();
ecrClient.on(client_ecr_1.ListImagesCommand).resolves({
imageIds: [],
});
garbageCollector = gc({
type: 'ecr',
rollbackBufferDays: 0,
action: 'full',
});
// succeeds without hanging
await garbageCollector.garbageCollect();
});
test('tags are unique', async () => {
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
garbageCollector = gc({
type: 'ecr',
rollbackBufferDays: 3,
action: 'tag',
});
await garbageCollector.garbageCollect();
expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1);
// tags objects
expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.PutImageCommand, 2);
expect(ecrClient).toHaveReceivedCommandWith(client_ecr_1.PutImageCommand, {
repositoryName: 'REPO_NAME',
imageDigest: 'digest3',
imageManifest: expect.any(String),
imageTag: expect.stringContaining(`0-${api_1.ECR_ISOLATED_TAG}`),
});
expect(ecrClient).toHaveReceivedCommandWith(client_ecr_1.PutImageCommand, {
repositoryName: 'REPO_NAME',
imageDigest: 'digest2',
imageManifest: expect.any(String),
imageTag: expect.stringContaining(`1-${api_1.ECR_ISOLATED_TAG}`),
});
});
test('listImagesCommand returns nextToken', async () => {
// This test is to ensure that the garbage collector can handle paginated responses from the ECR API
// If not handled correctly, the garbage collector will continue to make requests to the ECR API
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
prepareDefaultEcrMock();
ecrClient.on(client_ecr_1.ListImagesCommand).resolves({
imageIds: [
{
imageDigest: 'digest1',
imageTag: 'abcde',
},
{
imageDigest: 'digest2',
imageTag: 'fghij',
},
],
nextToken: 'nextToken',
}).on(client_ecr_1.ListImagesCommand, {
repositoryName: 'REPO_NAME',
nextToken: 'nextToken',
}).resolves({
imageIds: [
{
imageDigest: 'digest3',
imageTag: 'klmno',
},
],
});
ecrClient.on(client_ecr_1.BatchGetImageCommand).resolvesOnce({
images: [
{ imageId: { imageDigest: 'digest1' } },
{ imageId: { imageDigest: 'digest2' } },
],
}).resolvesOnce({
images: [
{ imageId: { imageDigest: 'digest3' } },
],
});
ecrClient.on(client_ecr_1.DescribeImagesCommand).resolvesOnce({
imageDetails: [
{
imageDigest: 'digest1',
imageTags: ['abcde'],
imagePushedAt: daysInThePast(100),
imageSizeInBytes: 1000000000,
},
{ imageDigest: 'digest2', imageTags: ['fghij'], imagePushedAt: daysInThePast(10), imageSizeInBytes: 300000000 },
],
}).resolvesOnce({
imageDetails: [
{ imageDigest: 'digest3', imageTags: ['klmno'], imagePushedAt: daysInThePast(2), imageSizeInBytes: 100 },
],
});
prepareDefaultCfnMock();
garbageCollector = gc({
type: 'ecr',
rollbackBufferDays: 0,
action: 'full',
});
await garbageCollector.garbageCollect();
expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.DescribeImagesCommand, 2);
expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.ListImagesCommand, 4);
// no tagging
expect(ecrClient).toHaveReceivedCommandTimes(client_ecr_1.PutImageCommand, 0);
expect(ecrClient).toHaveReceivedCommandWith(client_ecr_1.BatchDeleteImageCommand, {
repositoryName: 'REPO_NAME',
imageIds: [
{ imageDigest: 'digest2' },
{ imageDigest: 'digest3' },
],
});
});
});
describe('CloudFormation API calls', () => {
test('bootstrap filters out other bootstrap versions', async () => {
mockTheToolkitInfo({
Parameters: [{
ParameterKey: 'Qualifier',
ParameterValue: 'zzzzzz',
}],
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
garbageCollector = gc({
type: 's3',
rollbackBufferDays: 3,
action: 'full',
});
await garbageCollector.garbageCollect();
expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.GetTemplateSummaryCommand, 2);
expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.GetTemplateCommand, 0);
});
test('parameter hashes are included', async () => {
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
cfnClient.on(client_cloudformation_1.GetTemplateSummaryCommand).resolves({
Parameters: [{
ParameterKey: 'AssetParametersasset1',
DefaultValue: 'asset1',
}],
});
garbageCollector = gc({
type: 's3',
rollbackBufferDays: 0,
action: 'full',
});
await garbageCollector.garbageCollect();
expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1);
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.ListObjectsV2Command, 2);
// no tagging
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.GetObjectTaggingCommand, 0);
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.PutObjectTaggingCommand, 0);
// assets are to be deleted
expect(s3Client).toHaveReceivedCommandWith(client_s3_1.DeleteObjectsCommand, {
Bucket: 'BUCKET_NAME',
Delete: {
Objects: [
// no 'asset1'
{ Key: 'asset2' },
{ Key: 'asset3' },
],
Quiet: true,
},
});
});
});
function prepareDefaultCfnMock() {
const client = cfnClient;
client.reset();
client.on(client_cloudformation_1.ListStacksCommand).resolves({
StackSummaries: [
{ StackName: 'Stack1', StackStatus: 'CREATE_COMPLETE', CreationTime: new Date() },
{ StackName: 'Stack2', StackStatus: 'UPDATE_COMPLETE', CreationTime: new Date() },
],
});
client.on(client_cloudformation_1.GetTemplateSummaryCommand).resolves({
Parameters: [{
ParameterKey: 'BootstrapVersion',
DefaultValue: '/cdk-bootstrap/abcde/version',
}],
});
client.on(client_cloudformation_1.GetTemplateCommand).resolves({
TemplateBody: 'abcde',
});
return client;
}
function prepareDefaultS3Mock() {
const client = s3Client;
client.reset();
client.on(client_s3_1.ListObjectsV2Command).resolves({
Contents: [
{ Key: 'asset1', LastModified: new Date(Date.now() - (2 * DAY)) },
{ Key: 'asset2', LastModified: new Date(Date.now() - (10 * DAY)) },
{ Key: 'asset3', LastModified: new Date(Date.now() - (100 * DAY)) },
],
KeyCount: 3,
});
client.on(client_s3_1.GetObjectTaggingCommand).callsFake((params) => ({
TagSet: params.Key === 'asset2' ? [{ Key: api_1.S3_ISOLATED_TAG, Value: new Date().toISOString() }] : [],
}));
return client;
}
function prepareDefaultEcrMock() {
const client = ecrClient;
client.reset();
client.on(client_ecr_1.BatchGetImageCommand).resolves({
images: [
{ imageId: { imageDigest: 'digest1' } },
{ imageId: { imageDigest: 'digest2' } },
{ imageId: { imageDigest: 'digest3' } },
],
});
client.on(client_ecr_1.DescribeImagesCommand).resolves({
imageDetails: [
{ imageDigest: 'digest3', imageTags: ['klmno'], imagePushedAt: daysInThePast(2), imageSizeInBytes: 100 },
{ imageDigest: 'digest2', imageTags: ['fghij'], imagePushedAt: daysInThePast(10), imageSizeInBytes: 300000000 },
{
imageDigest: 'digest1',
imageTags: ['abcde'],
imagePushedAt: daysInThePast(100),
imageSizeInBytes: 1000000000,
},
],
});
client.on(client_ecr_1.ListImagesCommand).resolves({
imageIds: [
{ imageDigest: 'digest1', imageTag: 'abcde' }, // inuse
{ imageDigest: 'digest2', imageTag: 'fghij' },
{ imageDigest: 'digest3', imageTag: 'klmno' },
],
});
return client;
}
describe('Garbage Collection with large # of objects', () => {
const keyCount = 10000;
test('tag only', async () => {
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
mockClientsForLargeObjects();
garbageCollector = gc({
type: 's3',
rollbackBufferDays: 1,
action: 'tag',
});
await garbageCollector.garbageCollect();
expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1);
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.ListObjectsV2Command, 2);
// tagging is performed
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.GetObjectTaggingCommand, keyCount);
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.DeleteObjectTaggingCommand, 1000); // 1000 in use assets are erroneously tagged
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.PutObjectTaggingCommand, 5000); // 8000-4000 assets need to be tagged, + 1000 (since untag also calls this)
});
test('delete-tagged only', async () => {
mockTheToolkitInfo({
Outputs: [
{
OutputKey: 'BootstrapVersion',
OutputValue: '999',
},
],
});
mockClientsForLargeObjects();
garbageCollector = gc({
type: 's3',
rollbackBufferDays: 1,
action: 'delete-tagged',
});
await garbageCollector.garbageCollect();
expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1);
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.ListObjectsV2Command, 2);
// delete previously tagged objects
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.GetObjectTaggingCommand, keyCount);
expect(s3Client).toHaveReceivedCommandTimes(client_s3_1.DeleteObjectsCommand, 4); // 4000 isolated assets are already tagged, deleted in batches of 1000
});
function mockClientsForLargeObjects() {
cfnClient.on(client_cloudformation_1.ListStacksCommand).resolves({
StackSummaries: [
{ StackName: 'Stack1', StackStatus: 'CREATE_COMPLETE', CreationTime: new Date() },
],
});
cfnClient.on(client_cloudformation_1.GetTemplateSummaryCommand).resolves({
Parameters: [{
ParameterKey: 'BootstrapVersion',
DefaultValue: '/cdk-bootstrap/abcde/version',
}],
});
// add every 5th asset hash to the mock template body: 8000 assets are isolated
const mockTemplateBody = [];
for (let i = 0; i < keyCount; i += 5) {
mockTemplateBody.push(`asset${i}hash`);
}
cfnClient.on(client_cloudformation_1.GetTemplateCommand).resolves({
TemplateBody: mockTemplateBody.join('-'),
});
const contents = [];
for (let i = 0; i < keyCount; i++) {
contents.push({
Key: `asset${i}hash`,
LastModified: new Date(0),
});
}
s3Client.on(client_s3_1.ListObjectsV2Command).resolves({
Contents: contents,
KeyCount: keyCount,
});
// every other object has the isolated tag: of the 8000 isolated assets, 4000 already are tagged.
// of the 2000 in use assets, 1000 are tagged.
s3Client.on(client_s3_1.GetObjectTaggingCommand).callsFake((params) => ({
TagSet: Number(params.Key[params.Key.length - 5]) % 2 === 0
? [{ Key: api_1.S3_ISOLATED_TAG, Value: new Date(2000, 1, 1).toISOString() }]
: [],
}));
}
});
describe('BackgroundStackRefresh', () => {
let backgroundRefresh;
let refreshProps;
let setTimeoutSpy;
beforeEach(() => {
jest.useFakeTimers();
setTimeoutSpy = jest.spyOn(global, 'setTimeout');
const foo = new mock_sdk_1.MockSdk();
refreshProps = {
cfn: foo.cloudFormation(),
activeAssets: new stack_refresh_1.ActiveAssetCache(),
};
backgroundRefresh = new stack_refresh_1.BackgroundStackRefresh(refreshProps);
});
afterEach(() => {
jest.clearAllTimers();
setTimeoutSpy.mockRestore();
});
test('should start after a delay', () => {
void backgroundRefresh.start();
expect(setTimeoutSpy).toHaveBeenCalledTimes(1);
expect(setTimeoutSpy).toHaveBeenLastCalledWith(expect.any(Function), 300000);
});
test('should refresh stacks and schedule next refresh', async () => {
void backgroundRefresh.start();
// Run the first timer (which should trigger the first refresh)
await jest.runOnlyPendingTimersAsync();
expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 1);
expect(setTimeoutSpy).toHaveBeenCalledTimes(2); // Once for start, once for next refresh
expect(setTimeoutSpy).toHaveBeenLastCalledWith(expect.any(Function), 300000);
// Run the first timer (which triggers the first refresh)
await jest.runOnlyPendingTimersAsync();
expect(cfnClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStacksCommand, 2);
expect(setTimeoutSpy).toHaveBeenCalledTimes(3); // Two refreshes plus one more scheduled
});
test('should wait for the next refresh if called within time frame', async () => {
void backgroundRefresh.start();
// Run the first timer (which triggers the first refresh)
await jest.runOnlyPendingTimersAsync();
const waitPromise = backgroundRefresh.noOlderThan(180000); // 3 minutes
jest.advanceTimersByTime(120000); // Advance time by 2 minutes
await expect(waitPromise).resolves.toBeUndefined();
});
test('should wait for the next refresh if refresh lands before the timeout', async () => {
void backgroundRefresh.start();
// Run the first timer (which triggers the first refresh)
await jest.runOnlyPendingTimersAsync();
jest.advanceTimersByTime(24000); // Advance time by 4 minutes
const waitPromise = backgroundRefresh.noOlderThan(300000); // 5 minutes
jest.advanceTimersByTime(120000); // Advance time by 2 minutes, refresh should fire
await expect(waitPromise).resolves.toBeUndefined();
});
test('should reject if the refresh takes too long', async () => {
void backgroundRefresh.start();
// Run the first timer (which triggers the first refresh)
await jest.runOnlyPendingTimersAsync();
jest.advanceTimersByTime(120000); // Advance time by 2 minutes
const waitPromise = backgroundRefresh.noOlderThan(0); // 0 seconds
jest.advanceTimersByTime(120000); // Advance time by 2 minutes
await expect(waitPromise).rejects.toThrow('refreshStacks took too long; the background thread likely threw an error');
});
});
function daysInThePast(days) {
const d = new Date();
d.setDate(d.getDate() - days);
return d;
}
function yearsInTheFuture(years) {
const d = new Date();
d.setFullYear(d.getFullYear() + years);
return d;
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ2FyYmFnZS1jb2xsZWN0aW9uLnRlc3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJnYXJiYWdlLWNvbGxlY3Rpb24udGVzdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUEsaUNBQWlDOztBQUVqQywwRUFLd0M7QUFDeEMsdUNBQWlHO0FBQ2pHLCtDQUF1STtBQUN2SSxrREFNNEI7QUFDNUIsa0ZBSXdEO0FBQ3hELG9EQU02QjtBQUU3QixJQUFJLGdCQUFrQyxDQUFDO0FBRXZDLElBQUksVUFBNEIsQ0FBQztBQUNqQyxNQUFNLFNBQVMsR0FBRyxtQ0FBd0IsQ0FBQztBQUMzQyxNQUFNLFFBQVEsR0FBRyx1QkFBWSxDQUFDO0FBQzlCLE1BQU0sU0FBUyxHQUFHLHdCQUFhLENBQUM7QUFFaEMsTUFBTSxHQUFHLEdBQUcsRUFBRSxHQUFHLEVBQUUsR0FBRyxFQUFFLEdBQUcsSUFBSSxDQUFDLENBQUMsa0NBQWtDO0FBRW5FLFVBQVUsQ0FBQyxHQUFHLEVBQUU7SUFDZCxvREFBb0Q7SUFDcEQsSUFBSSxDQUFDLEtBQUssQ0FBQyxpQkFBVyxFQUFFLFFBQVEsQ0FBQyxDQUFDLGlCQUFpQixDQUFDLGlCQUFXLENBQUMsMEJBQTBCLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQztJQUU1RyxrREFBa0Q7SUFDbEQsVUFBVSxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLE1BQU0sRUFBRSxPQUFPLENBQUMsQ0FBQyxrQkFBa0IsQ0FBQyxHQUFHLEVBQUU7UUFDdkUsT0FBTyxJQUFJLENBQUM7SUFDZCxDQUFDLENBQUMsQ0FBQztJQUVILHFCQUFxQixFQUFFLENBQUM7SUFDeEIsb0JBQW9CLEVBQUUsQ0FBQztJQUN2QixxQkFBcUIsRUFBRSxDQUFDO0FBQzFCLENBQUMsQ0FBQyxDQUFDO0FBRUgsU0FBUyxDQUFDLEdBQUcsRUFBRTtJQUNiLFVBQVUsQ0FBQyxTQUFTLEVBQUUsQ0FBQztBQUN6QixDQUFDLENBQUMsQ0FBQztBQUVILFNB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