diff --git a/packages/@aws-cdk/aws-ecr/README.md b/packages/@aws-cdk/aws-ecr/README.md index 7a866e77f7a49..fdfbe2319f9d3 100644 --- a/packages/@aws-cdk/aws-ecr/README.md +++ b/packages/@aws-cdk/aws-ecr/README.md @@ -118,3 +118,21 @@ declare const repository: ecr.Repository; repository.addLifecycleRule({ tagPrefixList: ['prod'], maxImageCount: 9999 }); repository.addLifecycleRule({ maxImageAge: Duration.days(30) }); ``` + +### Repository deletion + +When a repository is removed from a stack (or the stack is deleted), the ECR +repository will be removed according to its removal policy (which by default will +simply orphan the repository and leave it in your AWS account). If the removal +policy is set to `RemovalPolicy.DESTROY`, the repository will be deleted as long +as it does not contain any images. + +To override this and force all images to get deleted during repository deletion, +enable the`autoDeleteImages` option. + +```ts +const repository = new Repository(this, 'MyTempRepo', { + removalPolicy: RemovalPolicy.DESTROY, + autoDeleteImages: true, +}); +``` diff --git a/packages/@aws-cdk/aws-ecr/lib/auto-delete-images-handler/index.ts b/packages/@aws-cdk/aws-ecr/lib/auto-delete-images-handler/index.ts new file mode 100644 index 0000000000000..fbc6ca60add7d --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/lib/auto-delete-images-handler/index.ts @@ -0,0 +1,94 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { ECR } from 'aws-sdk'; + +const AUTO_DELETE_IMAGES_TAG = 'aws-cdk:auto-delete-images'; + +const ecr = new ECR(); + +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) { + switch (event.RequestType) { + case 'Create': + break; + case 'Update': + return onUpdate(event); + case 'Delete': + return onDelete(event.ResourceProperties?.RepositoryName); + } +} + +async function onUpdate(event: AWSLambda.CloudFormationCustomResourceEvent) { + const updateEvent = event as AWSLambda.CloudFormationCustomResourceUpdateEvent; + const oldRepositoryName = updateEvent.OldResourceProperties?.RepositoryName; + const newRepositoryName = updateEvent.ResourceProperties?.RepositoryName; + const repositoryNameHasChanged = (newRepositoryName && oldRepositoryName) + && (newRepositoryName !== oldRepositoryName); + + /* If the name of the repository has changed, CloudFormation will try to delete the repository + and create a new one with the new name. So we have to delete the images in the + repository so that this operation does not fail. */ + if (repositoryNameHasChanged) { + return onDelete(oldRepositoryName); + } +} + +/** + * Recursively delete all images in the repository + * + * @param ECR.ListImagesRequest the repositoryName & nextToken if presented + */ +async function emptyRepository(params: ECR.ListImagesRequest) { + const listedImages = await ecr.listImages(params).promise(); + + const imageIds = listedImages?.imageIds ?? []; + const nextToken = listedImages.nextToken ?? null; + if (imageIds.length === 0) { + return; + } + + await ecr.batchDeleteImage({ + repositoryName: params.repositoryName, + imageIds, + }).promise(); + + if (nextToken) { + await emptyRepository({ + ...params, + nextToken, + }); + } +} + +async function onDelete(repositoryName: string) { + if (!repositoryName) { + throw new Error('No RepositoryName was provided.'); + } + + const response = await ecr.describeRepositories({ repositoryNames: [repositoryName] }).promise(); + const repository = response.repositories?.find(repo => repo.repositoryName === repositoryName); + + if (!await isRepositoryTaggedForDeletion(repository?.repositoryArn!)) { + process.stdout.write(`Repository does not have '${AUTO_DELETE_IMAGES_TAG}' tag, skipping cleaning.\n`); + return; + } + try { + await emptyRepository({ repositoryName }); + } catch (e) { + if (e.name !== 'RepositoryNotFoundException') { + throw e; + } + // Repository doesn't exist. Ignoring + } +} + +/** + * The repository will only be tagged for deletion if it's being deleted in the same + * deployment as this Custom Resource. + * + * If the Custom Resource is ever deleted before the repository, it must be because + * `autoDeleteImages` has been switched to false, in which case the tag would have + * been removed before we get to this Delete event. + */ +async function isRepositoryTaggedForDeletion(repositoryArn: string) { + const response = await ecr.listTagsForResource({ resourceArn: repositoryArn }).promise(); + return response.tags?.some(tag => tag.Key === AUTO_DELETE_IMAGES_TAG && tag.Value === 'true'); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index d32f3430e3c62..7992af2774b30 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -1,12 +1,29 @@ import { EOL } from 'os'; +import * as path from 'path'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; -import { ArnFormat, IResource, Lazy, RemovalPolicy, Resource, Stack, Tags, Token, TokenComparison } from '@aws-cdk/core'; +import { + ArnFormat, + IResource, + Lazy, + RemovalPolicy, + Resource, + Stack, + Tags, + Token, + TokenComparison, + CustomResource, + CustomResourceProvider, + CustomResourceProviderRuntime, +} from '@aws-cdk/core'; import { IConstruct, Construct } from 'constructs'; import { CfnRepository } from './ecr.generated'; import { LifecycleRule, TagStatus } from './lifecycle'; +const AUTO_DELETE_IMAGES_RESOURCE_TYPE = 'Custom::ECRAutoDeleteImages'; +const AUTO_DELETE_IMAGES_TAG = 'aws-cdk:auto-delete-images'; + /** * Represents an ECR repository. */ @@ -479,6 +496,16 @@ export interface RepositoryProps { * @default TagMutability.MUTABLE */ readonly imageTagMutability?: TagMutability; + + /** + * Whether all images should be automatically deleted when the repository is + * removed from the stack or when the stack is deleted. + * + * Requires the `removalPolicy` to be set to `RemovalPolicy.DESTROY`. + * + * @default false + */ + readonly autoDeleteImages?: boolean; } export interface RepositoryAttributes { @@ -589,6 +616,7 @@ export class Repository extends RepositoryBase { private readonly lifecycleRules = new Array(); private readonly registryId?: string; private policyDocument?: iam.PolicyDocument; + private readonly _resource: CfnRepository; constructor(scope: Construct, id: string, props: RepositoryProps = {}) { super(scope, id, { @@ -606,6 +634,14 @@ export class Repository extends RepositoryBase { imageTagMutability: props.imageTagMutability || undefined, encryptionConfiguration: this.parseEncryption(props), }); + this._resource = resource; + + if (props.autoDeleteImages) { + if (props.removalPolicy !== RemovalPolicy.DESTROY) { + throw new Error('Cannot use \'autoDeleteImages\' property on a repository without setting removal policy to \'DESTROY\'.'); + } + this.enableAutoDeleteImages(); + } resource.applyRemovalPolicy(props.removalPolicy); @@ -741,6 +777,44 @@ export class Repository extends RepositoryBase { throw new Error(`Unexpected 'encryptionType': ${encryptionType}`); } + + private enableAutoDeleteImages() { + // Use a iam policy to allow the custom resource to list & delete + // images in the repository and the ability to get all repositories to find the arn needed on delete. + const provider = CustomResourceProvider.getOrCreateProvider(this, AUTO_DELETE_IMAGES_RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'auto-delete-images-handler'), + runtime: CustomResourceProviderRuntime.NODEJS_14_X, + description: `Lambda function for auto-deleting images in ${this.repositoryName} repository.`, + policyStatements: [ + { + Effect: 'Allow', + Action: [ + 'ecr:BatchDeleteImage', + 'ecr:DescribeRepositories', + 'ecr:ListImages', + 'ecr:ListTagsForResource', + ], + Resource: [this._resource.attrArn], + }, + ], + }); + + const customResource = new CustomResource(this, 'AutoDeleteImagesCustomResource', { + resourceType: AUTO_DELETE_IMAGES_RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + RepositoryName: Lazy.any({ produce: () => this.repositoryName }), + }, + }); + customResource.node.addDependency(this); + + // We also tag the repository to record the fact that we want it autodeleted. + // The custom resource will check this tag before actually doing the delete. + // Because tagging and untagging will ALWAYS happen before the CR is deleted, + // we can set `autoDeleteImages: false` without the removal of the CR emptying + // the repository as a side effect. + Tags.of(this._resource).add(AUTO_DELETE_IMAGES_TAG, 'true'); + } } function validateAnyRuleLast(rules: LifecycleRule[]) { diff --git a/packages/@aws-cdk/aws-ecr/test/auto-delete-images-handler.test.ts b/packages/@aws-cdk/aws-ecr/test/auto-delete-images-handler.test.ts new file mode 100644 index 0000000000000..9bbebdf32c498 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/auto-delete-images-handler.test.ts @@ -0,0 +1,397 @@ +const mockECRClient = { + listImages: jest.fn().mockReturnThis(), + batchDeleteImage: jest.fn().mockReturnThis(), + describeRepositories: jest.fn().mockReturnThis(), + listTagsForResource: jest.fn().mockReturnThis(), + promise: jest.fn(), +}; + +import { handler } from '../lib/auto-delete-images-handler'; + +jest.mock('aws-sdk', () => { + return { ECR: jest.fn(() => mockECRClient) }; +}); + +beforeEach(() => { + mockECRClient.listImages.mockReturnThis(); + mockECRClient.batchDeleteImage.mockReturnThis(); + mockECRClient.listTagsForResource.mockReturnThis(); + mockECRClient.describeRepositories.mockReturnThis(); + givenTaggedForDeletion(); +}); + +afterEach(() => { + jest.resetAllMocks(); +}); + +test('does nothing on create event', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockECRClient.listImages).toHaveBeenCalledTimes(0); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledTimes(0); +}); + +test('does nothing on update event when everything remains the same', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Update', + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + OldResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockECRClient.describeRepositories).toHaveBeenCalledTimes(0); + expect(mockECRClient.listImages).toHaveBeenCalledTimes(0); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledTimes(0); +}); + +test('does nothing on update event when the repository name remains the same but the service token changes', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Update', + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + OldResourceProperties: { + ServiceToken: 'Bar', + RespositoryName: 'MyRepo', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockECRClient.describeRepositories).toHaveBeenCalledTimes(0); + expect(mockECRClient.listImages).toHaveBeenCalledTimes(0); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledTimes(0); +}); + +test('does nothing on update event when the new resource properties are absent', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Update', + OldResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockECRClient.listImages).toHaveBeenCalledTimes(0); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledTimes(0); + expect(mockECRClient.describeRepositories).toHaveBeenCalledTimes(0); +}); + +test('does nothing on update event when the old resource properties are absent', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Update', + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockECRClient.listImages).toHaveBeenCalledTimes(0); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledTimes(0); + expect(mockECRClient.describeRepositories).toHaveBeenCalledTimes(0); +}); + +test('deletes all objects when the name changes on update event', async () => { + // GIVEN + mockAwsPromise(mockECRClient.describeRepositories, { + repositories: [ + { repositoryArn: 'RepositoryArn', respositoryName: 'MyRepo' }, + ], + }); + + mockAwsPromise(mockECRClient.listImages, { + imageIds: [ + { imageDigest: 'ImageDigest1', imageTag: 'ImageTag1' }, + { imageDigest: 'ImageDigest2', imageTag: 'ImageTag2' }, + ], + }); + + const event: Partial = { + RequestType: 'Update', + OldResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo-renamed', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockECRClient.listImages).toHaveBeenCalledTimes(1); + expect(mockECRClient.listImages).toHaveBeenCalledWith({ repositoryName: 'MyRepo' }); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledTimes(1); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledWith({ + repositoryName: 'MyRepo', + imageIds: [ + { imageDigest: 'ImageDigest1', imageTag: 'ImageTag1' }, + { imageDigest: 'ImageDigest2', imageTag: 'ImageTag2' }, + ], + }); + expect(mockECRClient.describeRepositories).toHaveBeenCalledTimes(1); +}); + +test('deletes no images on delete event when repository has no images', async () => { + // GIVEN + mockECRClient.promise.mockResolvedValue({ imageIds: [] }); // listedImages() call + + // WHEN + const event: Partial = { + RequestType: 'Delete', + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + }; + await invokeHandler(event); + + // THEN + expect(mockECRClient.listImages).toHaveBeenCalledTimes(1); + expect(mockECRClient.listImages).toHaveBeenCalledWith({ repositoryName: 'MyRepo' }); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledTimes(0); + expect(mockECRClient.describeRepositories).toHaveBeenCalledTimes(1); +}); + +test('deletes all images on delete event', async () => { + mockECRClient.promise.mockResolvedValue({ // listedImages() call + imageIds: [ + { + imageTag: 'tag1', + imageDigest: 'sha256-1', + }, + { + imageTag: 'tag2', + imageDigest: 'sha256-2', + }, + ], + }); + + // WHEN + const event: Partial = { + RequestType: 'Delete', + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + }; + await invokeHandler(event); + + // THEN + expect(mockECRClient.listImages).toHaveBeenCalledTimes(1); + expect(mockECRClient.listImages).toHaveBeenCalledWith({ repositoryName: 'MyRepo' }); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledTimes(1); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledWith({ + repositoryName: 'MyRepo', + imageIds: [ + { + imageTag: 'tag1', + imageDigest: 'sha256-1', + }, + { + imageTag: 'tag2', + imageDigest: 'sha256-2', + }, + ], + }); +}); + +test('does not empty repository if it is not tagged', async () => { + // GIVEN + givenNotTaggedForDeletion(); + mockECRClient.promise.mockResolvedValue({ // listedImages() call + imageIds: [ + { + imageTag: 'tag1', + imageDigest: 'sha256-1', + }, + { + imageTag: 'tag2', + imageDigest: 'sha256-2', + }, + ], + }); + + // WHEN + const event: Partial = { + RequestType: 'Delete', + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + }; + await invokeHandler(event); + + // THEN + expect(mockECRClient.batchDeleteImage).not.toHaveBeenCalled(); +}); + +test('delete event where repo has many images does recurse appropriately', async () => { + // GIVEN + mockAwsPromise(mockECRClient.describeRepositories, { + repositories: [ + { repositoryArn: 'RepositoryArn', respositoryName: 'MyRepo' }, + ], + }); + + mockECRClient.promise // listedImages() call + .mockResolvedValueOnce({ + imageIds: [ + { + imageTag: 'tag1', + imageDigest: 'sha256-1', + }, + { + imageTag: 'tag2', + imageDigest: 'sha256-2', + }, + ], + nextToken: 'token1', + }) + .mockResolvedValueOnce(undefined) // batchDeleteImage() call + .mockResolvedValueOnce({ // listedImages() call + imageIds: [ + { + imageTag: 'tag3', + imageDigest: 'sha256-3', + }, + { + imageTag: 'tag4', + imageDigest: 'sha256-4', + }, + ], + }); + + // WHEN + const event: Partial = { + RequestType: 'Delete', + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + }; + await invokeHandler(event); + + // THEN + expect(mockECRClient.describeRepositories).toHaveBeenCalledTimes(1); + expect(mockECRClient.listImages).toHaveBeenCalledTimes(2); + expect(mockECRClient.listImages).toHaveBeenCalledWith({ repositoryName: 'MyRepo' }); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledTimes(2); + expect(mockECRClient.batchDeleteImage).toHaveBeenNthCalledWith(1, { + repositoryName: 'MyRepo', + imageIds: [ + { + imageTag: 'tag1', + imageDigest: 'sha256-1', + }, + { + imageTag: 'tag2', + imageDigest: 'sha256-2', + }, + ], + }); + expect(mockECRClient.batchDeleteImage).toHaveBeenNthCalledWith(2, { + repositoryName: 'MyRepo', + imageIds: [ + { + imageTag: 'tag3', + imageDigest: 'sha256-3', + }, + { + imageTag: 'tag4', + imageDigest: 'sha256-4', + }, + ], + }); +}); + +test('does nothing when the repository does not exist', async () => { + // GIVEN + mockECRClient.promise.mockRejectedValue({ name: 'RepositoryNotFoundException' }); + + mockAwsPromise(mockECRClient.describeRepositories, { + repositories: [ + { repositoryArn: 'RepositoryArn', respositoryName: 'MyRepo' }, + ], + }); + + // WHEN + const event: Partial = { + RequestType: 'Delete', + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + }; + await invokeHandler(event); + + expect(mockECRClient.batchDeleteImage).not.toHaveBeenCalled(); +}); + +// helper function to get around TypeScript expecting a complete event object, +// even though our tests only need some of the fields +async function invokeHandler(event: Partial) { + return handler(event as AWSLambda.CloudFormationCustomResourceEvent); +} + +function mockAwsPromise(fn: jest.Mock, value: A, when: 'once' | 'always' = 'always') { + (when === 'always' ? fn.mockReturnValue : fn.mockReturnValueOnce).call(fn, { + promise: () => value, + }); +} + +function givenTaggedForDeletion() { + mockAwsPromise(mockECRClient.listTagsForResource, { + tags: [ + { + Key: 'aws-cdk:auto-delete-images', + Value: 'true', + }, + ], + + }); +} + +function givenNotTaggedForDeletion() { + mockAwsPromise(mockECRClient.listTagsForResource, { + tags: [], + }); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/asset.6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e/__entrypoint__.js b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/asset.6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e/__entrypoint__.js new file mode 100644 index 0000000000000..c366685b1451b --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/asset.6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e/__entrypoint__.js @@ -0,0 +1,147 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withRetries = exports.handler = exports.external = void 0; +const https = require("https"); +const url = require("url"); +// for unit tests +exports.external = { + sendHttpRequest: defaultSendHttpRequest, + log: defaultLog, + includeStackTraces: true, + userHandlerIndex: './index', +}; +const CREATE_FAILED_PHYSICAL_ID_MARKER = 'AWSCDK::CustomResourceProviderFramework::CREATE_FAILED'; +const MISSING_PHYSICAL_ID_MARKER = 'AWSCDK::CustomResourceProviderFramework::MISSING_PHYSICAL_ID'; +async function handler(event, context) { + const sanitizedEvent = { ...event, ResponseURL: '...' }; + exports.external.log(JSON.stringify(sanitizedEvent, undefined, 2)); + // ignore DELETE event when the physical resource ID is the marker that + // indicates that this DELETE is a subsequent DELETE to a failed CREATE + // operation. + if (event.RequestType === 'Delete' && event.PhysicalResourceId === CREATE_FAILED_PHYSICAL_ID_MARKER) { + exports.external.log('ignoring DELETE event caused by a failed CREATE event'); + await submitResponse('SUCCESS', event); + return; + } + try { + // invoke the user handler. this is intentionally inside the try-catch to + // ensure that if there is an error it's reported as a failure to + // cloudformation (otherwise cfn waits). + // eslint-disable-next-line @typescript-eslint/no-require-imports + const userHandler = require(exports.external.userHandlerIndex).handler; + const result = await userHandler(sanitizedEvent, context); + // validate user response and create the combined event + const responseEvent = renderResponse(event, result); + // submit to cfn as success + await submitResponse('SUCCESS', responseEvent); + } + catch (e) { + const resp = { + ...event, + Reason: exports.external.includeStackTraces ? e.stack : e.message, + }; + if (!resp.PhysicalResourceId) { + // special case: if CREATE fails, which usually implies, we usually don't + // have a physical resource id. in this case, the subsequent DELETE + // operation does not have any meaning, and will likely fail as well. to + // address this, we use a marker so the provider framework can simply + // ignore the subsequent DELETE. + if (event.RequestType === 'Create') { + exports.external.log('CREATE failed, responding with a marker physical resource id so that the subsequent DELETE will be ignored'); + resp.PhysicalResourceId = CREATE_FAILED_PHYSICAL_ID_MARKER; + } + else { + // otherwise, if PhysicalResourceId is not specified, something is + // terribly wrong because all other events should have an ID. + exports.external.log(`ERROR: Malformed event. "PhysicalResourceId" is required: ${JSON.stringify(event)}`); + } + } + // this is an actual error, fail the activity altogether and exist. + await submitResponse('FAILED', resp); + } +} +exports.handler = handler; +function renderResponse(cfnRequest, handlerResponse = {}) { + // if physical ID is not returned, we have some defaults for you based + // on the request type. + const physicalResourceId = handlerResponse.PhysicalResourceId ?? cfnRequest.PhysicalResourceId ?? cfnRequest.RequestId; + // if we are in DELETE and physical ID was changed, it's an error. + if (cfnRequest.RequestType === 'Delete' && physicalResourceId !== cfnRequest.PhysicalResourceId) { + throw new Error(`DELETE: cannot change the physical resource ID from "${cfnRequest.PhysicalResourceId}" to "${handlerResponse.PhysicalResourceId}" during deletion`); + } + // merge request event and result event (result prevails). + return { + ...cfnRequest, + ...handlerResponse, + PhysicalResourceId: physicalResourceId, + }; +} +async function submitResponse(status, event) { + const json = { + Status: status, + Reason: event.Reason ?? status, + StackId: event.StackId, + RequestId: event.RequestId, + PhysicalResourceId: event.PhysicalResourceId || MISSING_PHYSICAL_ID_MARKER, + LogicalResourceId: event.LogicalResourceId, + NoEcho: event.NoEcho, + Data: event.Data, + }; + exports.external.log('submit response to cloudformation', json); + const responseBody = JSON.stringify(json); + const parsedUrl = url.parse(event.ResponseURL); + const req = { + hostname: parsedUrl.hostname, + path: parsedUrl.path, + method: 'PUT', + headers: { + 'content-type': '', + 'content-length': Buffer.byteLength(responseBody, 'utf8'), + }, + }; + const retryOptions = { + attempts: 5, + sleep: 1000, + }; + await withRetries(retryOptions, exports.external.sendHttpRequest)(req, responseBody); +} +async function defaultSendHttpRequest(options, responseBody) { + return new Promise((resolve, reject) => { + try { + const request = https.request(options, _ => resolve()); + request.on('error', reject); + request.write(responseBody); + request.end(); + } + catch (e) { + reject(e); + } + }); +} +function defaultLog(fmt, ...params) { + // eslint-disable-next-line no-console + console.log(fmt, ...params); +} +function withRetries(options, fn) { + return async (...xs) => { + let attempts = options.attempts; + let ms = options.sleep; + while (true) { + try { + return await fn(...xs); + } + catch (e) { + if (attempts-- <= 0) { + throw e; + } + await sleep(Math.floor(Math.random() * ms)); + ms *= 2; + } + } + }; +} +exports.withRetries = withRetries; +async function sleep(ms) { + return new Promise((ok) => setTimeout(ok, ms)); +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibm9kZWpzLWVudHJ5cG9pbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJub2RlanMtZW50cnlwb2ludC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSwrQkFBK0I7QUFDL0IsMkJBQTJCO0FBRTNCLGlCQUFpQjtBQUNKLFFBQUEsUUFBUSxHQUFHO0lBQ3RCLGVBQWUsRUFBRSxzQkFBc0I7SUFDdkMsR0FBRyxFQUFFLFVBQVU7SUFDZixrQkFBa0IsRUFBRSxJQUFJO0lBQ3hCLGdCQUFnQixFQUFFLFNBQVM7Q0FDNUIsQ0FBQztBQUVGLE1BQU0sZ0NBQWdDLEdBQUcsd0RBQXdELENBQUM7QUFDbEcsTUFBTSwwQkFBMEIsR0FBRyw4REFBOEQsQ0FBQztBQVczRixLQUFLLFVBQVUsT0FBTyxDQUFDLEtBQWtELEVBQUUsT0FBMEI7SUFDMUcsTUFBTSxjQUFjLEdBQUcsRUFBRSxHQUFHLEtBQUssRUFBRSxXQUFXLEVBQUUsS0FBSyxFQUFFLENBQUM7SUFDeEQsZ0JBQVEsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxjQUFjLEVBQUUsU0FBUyxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFFM0QsdUVBQXVFO0lBQ3ZFLHVFQUF1RTtJQUN2RSxhQUFhO0lBQ2IsSUFBSSxLQUFLLENBQUMsV0FBVyxLQUFLLFFBQVEsSUFBSSxLQUFLLENBQUMsa0JBQWtCLEtBQUssZ0NBQWdDLEVBQUU7UUFDbkcsZ0JBQVEsQ0FBQyxHQUFHLENBQUMsdURBQXVELENBQUMsQ0FBQztRQUN0RSxNQUFNLGNBQWMsQ0FBQyxTQUFTLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFDdkMsT0FBTztLQUNSO0lBRUQsSUFBSTtRQUNGLHlFQUF5RTtRQUN6RSxpRUFBaUU7UUFDakUsd0NBQXdDO1FBQ3hDLGlFQUFpRTtRQUNqRSxNQUFNLFdBQVcsR0FBWSxPQUFPLENBQUMsZ0JBQVEsQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDLE9BQU8sQ0FBQztRQUN4RSxNQUFN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LE9BQU8sS0FBSyxFQUFFLEdBQUcsRUFBSyxFQUFFLEVBQUU7UUFDeEIsSUFBSSxRQUFRLEdBQUcsT0FBTyxDQUFDLFFBQVEsQ0FBQztRQUNoQyxJQUFJLEVBQUUsR0FBRyxPQUFPLENBQUMsS0FBSyxDQUFDO1FBQ3ZCLE9BQU8sSUFBSSxFQUFFO1lBQ1gsSUFBSTtnQkFDRixPQUFPLE1BQU0sRUFBRSxDQUFDLEdBQUcsRUFBRSxDQUFDLENBQUM7YUFDeEI7WUFBQyxPQUFPLENBQUMsRUFBRTtnQkFDVixJQUFJLFFBQVEsRUFBRSxJQUFJLENBQUMsRUFBRTtvQkFDbkIsTUFBTSxDQUFDLENBQUM7aUJBQ1Q7Z0JBQ0QsTUFBTSxLQUFLLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsTUFBTSxFQUFFLEdBQUcsRUFBRSxDQUFDLENBQUMsQ0FBQztnQkFDNUMsRUFBRSxJQUFJLENBQUMsQ0FBQzthQUNUO1NBQ0Y7SUFDSCxDQUFDLENBQUM7QUFDSixDQUFDO0FBaEJELGtDQWdCQztBQUVELEtBQUssVUFBVSxLQUFLLENBQUMsRUFBVTtJQUM3QixPQUFPLElBQUksT0FBTyxDQUFDLENBQUMsRUFBRSxFQUFFLEVBQUUsQ0FBQyxVQUFVLENBQUMsRUFBRSxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUM7QUFDakQsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCAqIGFzIGh0dHBzIGZyb20gJ2h0dHBzJztcbmltcG9ydCAqIGFzIHVybCBmcm9tICd1cmwnO1xuXG4vLyBmb3IgdW5pdCB0ZXN0c1xuZXhwb3J0IGNvbnN0IGV4dGVybmFsID0ge1xuICBzZW5kSHR0cFJlcXVlc3Q6IGRlZmF1bHRTZW5kSHR0cFJlcXVlc3QsXG4gIGxvZzogZGVmYXVsdExvZyxcbiAgaW5jbHVkZVN0YWNrVHJhY2VzOiB0cnVlLFxuICB1c2VySGFuZGxlckluZGV4OiAnLi9pbmRleCcsXG59O1xuXG5jb25zdCBDUkVBVEVfRkFJTEVEX1BIWVNJQ0FMX0lEX01BUktFUiA9ICdBV1NDREs6OkN1c3RvbVJlc291cmNlUHJvdmlkZXJGcmFtZXdvcms6OkNSRUFURV9GQUlMRUQnO1xuY29uc3QgTUlTU0lOR19QSFlTSUNBTF9JRF9NQVJLRVIgPSAnQVdTQ0RLOjpDdXN0b21SZXNvdXJjZVByb3ZpZGVyRnJhbWV3b3JrOjpNSVNTSU5HX1BIWVNJQ0FMX0lEJztcblxuZXhwb3J0IHR5cGUgUmVzcG9uc2UgPSBBV1NMYW1iZGEuQ2xvdWRGb3JtYXRpb25DdXN0b21SZXNvdXJjZUV2ZW50ICYgSGFuZGxlclJlc3BvbnNlO1xuZXhwb3J0IHR5cGUgSGFuZGxlciA9IChldmVudDogQVdTTGFtYmRhLkNsb3VkRm9ybWF0aW9uQ3VzdG9tUmVzb3VyY2VFdmVudCwgY29udGV4dDogQVdTTGFtYmRhLkNvbnRleHQpID0+IFByb21pc2U8SGFuZGxlclJlc3BvbnNlIHwgdm9pZD47XG5leHBvcnQgdHlwZSBIYW5kbGVyUmVzcG9uc2UgPSB1bmRlZmluZWQgfCB7XG4gIERhdGE/OiBhbnk7XG4gIFBoeXNpY2FsUmVzb3VyY2VJZD86IHN0cmluZztcbiAgUmVhc29uPzogc3RyaW5nO1xuICBOb0VjaG8/OiBib29sZWFuO1xufTtcblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGhhbmRsZXIoZXZlbnQ6IEFXU0xhbWJkYS5DbG91ZEZvcm1hdGlvbkN1c3RvbVJlc291cmNlRXZlbnQsIGNvbnRleHQ6IEFXU0xhbWJkYS5Db250ZXh0KSB7XG4gIGNvbnN0IHNhbml0aXplZEV2ZW50ID0geyAuLi5ldmVudCwgUmVzcG9uc2VVUkw6ICcuLi4nIH07XG4gIGV4dGVybmFsLmxvZyhKU09OLnN0cmluZ2lmeShzYW5pdGl6ZWRFdmVudCwgdW5kZWZpbmVkLCAyKSk7XG5cbiAgLy8gaWdub3JlIERFTEVURSBldmVudCB3aGVuIHRoZSBwaHlzaWNhbCByZXNvdXJjZSBJRCBpcyB0aGUgbWFya2VyIHRoYXRcbiAgLy8gaW5kaWNhdGVzIHRoYXQgdGhpcyBERUxFVEUgaXMgYSBzdWJzZXF1ZW50IERFTEVURSB0byBhIGZhaWxlZCBDUkVBVEVcbiAgLy8gb3BlcmF0aW9uLlxuICBpZiAoZXZlbnQuUmVxdWVzdFR5cGUgPT09ICdEZWxldGUnICYmIGV2ZW50LlBoeXNpY2FsUmVzb3VyY2VJZCA9PT0gQ1JFQVRFX0ZBSUxFRF9QSFlTSUNBTF9JRF9NQVJLRVIpIHtcbiAgICBleHRlcm5hbC5sb2coJ2lnbm9yaW5nIERFTEVURSBldmVudCBjYXVzZWQgYnkgYSBmYWlsZWQgQ1JFQVRFIGV2ZW50Jyk7XG4gICAgYXdhaXQgc3VibWl0UmVzcG9uc2UoJ1NVQ0NFU1MnLCBldmVudCk7XG4gICAgcmV0dXJuO1xuICB9XG5cbiAgdHJ5IHtcbiAgICAvLyBpbnZva2UgdGhlIHVzZXIgaGFuZGxlci4gdGhpcyBpcyBpbnRlbnRpb25hbGx5IGluc2lkZSB0aGUgdHJ5LWNhdGNoIHRvXG4gICAgLy8gZW5zdXJlIHRoYXQgaWYgdGhlcmUgaXMgYW4gZXJyb3IgaXQncyByZXBvcnRlZCBhcyBhIGZhaWx1cmUgdG9cbiAgICAvLyBjbG91ZGZvcm1hdGlvbiAob3RoZXJ3aXNlIGNmbiB3YWl0cykuXG4gICAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIEB0eXBlc2NyaXB0LWVzbGludC9uby1yZXF1aXJlLWltcG9ydHNcbiAgICBjb25zdCB1c2VySGFuZGxlcjogSGFuZGxlciA9IHJlcXVpcmUoZXh0ZXJuYWwudXNlckhhbmRsZXJJbmRleCkuaGFuZGxlcjtcbiAgICBjb25zdCByZXN1bHQgPSBhd2FpdCB1c2VySGFuZGxlcihzYW5pdGl6ZWRFdmVudCwgY29udGV4dCk7XG5cbiAgICAvLyB2YWxpZGF0ZSB1c2VyIHJlc3BvbnNlIGFuZCBjcmVhdGUgdGhlIGNvbWJpbmVkIGV2ZW50XG4gICAgY29uc3QgcmVzcG9uc2VFdmVudCA9IHJlbmRlclJlc3BvbnNlKGV2ZW50LCByZXN1bHQpO1xuXG4gICAgLy8gc3VibWl0IHRvIGNmbiBhcyBzdWNjZXNzXG4gICAgYXdhaXQgc3VibWl0UmVzcG9uc2UoJ1NVQ0NFU1MnLCByZXNwb25zZUV2ZW50KTtcbiAgfSBjYXRjaCAoZSkge1xuICAgIGNvbnN0IHJlc3A6IFJlc3BvbnNlID0ge1xuICAgICAgLi4uZXZlbnQsXG4gICAgICBSZWFzb246IGV4dGVybmFsLmluY2x1ZGVTdGFja1RyYWNlcyA/IGUuc3RhY2sgOiBlLm1lc3NhZ2UsXG4gICAgfTtcblxuICAgIGlmICghcmVzcC5QaHlzaWNhbFJlc291cmNlSWQpIHtcbiAgICAgIC8vIHNwZWNpYWwgY2FzZTogaWYgQ1JFQVRFIGZhaWxzLCB3aGljaCB1c3VhbGx5IGltcGxpZXMsIHdlIHVzdWFsbHkgZG9uJ3RcbiAgICAgIC8vIGhhdmUgYSBwaHlzaWNhbCByZXNvdXJjZSBpZC4gaW4gdGhpcyBjYXNlLCB0aGUgc3Vic2VxdWVudCBERUxFVEVcbiAgICAgIC8vIG9wZXJhdGlvbiBkb2VzIG5vdCBoYXZlIGFueSBtZWFuaW5nLCBhbmQgd2lsbCBsaWtlbHkgZmFpbCBhcyB3ZWxsLiB0b1xuICAgICAgLy8gYWRkcmVzcyB0aGlzLCB3ZSB1c2UgYSBtYXJrZXIgc28gdGhlIHByb3ZpZGVyIGZyYW1ld29yayBjYW4gc2ltcGx5XG4gICAgICAvLyBpZ25vcmUgdGhlIHN1YnNlcXVlbnQgREVMRVRFLlxuICAgICAgaWYgKGV2ZW50LlJlcXVlc3RUeXBlID09PSAnQ3JlYXRlJykge1xuICAgICAgICBleHRlcm5hbC5sb2coJ0NSRUFURSBmYWlsZWQsIHJlc3BvbmRpbmcgd2l0aCBhIG1hcmtlciBwaHlzaWNhbCByZXNvdXJjZSBpZCBzbyB0aGF0IHRoZSBzdWJzZXF1ZW50IERFTEVURSB3aWxsIGJlIGlnbm9yZWQnKTtcbiAgICAgICAgcmVzcC5QaHlzaWNhbFJlc291cmNlSWQgPSBDUkVBVEVfRkFJTEVEX1BIWVNJQ0FMX0lEX01BUktFUjtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIC8vIG90aGVyd2lzZSwgaWYgUGh5c2ljYWxSZXNvdXJjZUlkIGlzIG5vdCBzcGVjaWZpZWQsIHNvbWV0aGluZyBpc1xuICAgICAgICAvLyB0ZXJyaWJseSB3cm9uZyBiZWNhdXNlIGFsbCBvdGhlciBldmVudHMgc2hvdWxkIGhhdmUgYW4gSUQuXG4gICAgICAgIGV4dGVybmFsLmxvZyhgRVJST1I6IE1hbGZvcm1lZCBldmVudC4gXCJQaHlzaWNhbFJlc291cmNlSWRcIiBpcyByZXF1aXJlZDogJHtKU09OLnN0cmluZ2lmeShldmVudCl9YCk7XG4gICAgICB9XG4gICAgfVxuXG4gICAgLy8gdGhpcyBpcyBhbiBhY3R1YWwgZXJyb3IsIGZhaWwgdGhlIGFjdGl2aXR5IGFsdG9nZXRoZXIgYW5kIGV4aXN0LlxuICAgIGF3YWl0IHN1Ym1pdFJlc3BvbnNlKCdGQUlMRUQnLCByZXNwKTtcbiAgfVxufVxuXG5mdW5jdGlvbiByZW5kZXJSZXNwb25zZShcbiAgY2ZuUmVxdWVzdDogQVdTTGFtYmRhLkNsb3VkRm9ybWF0aW9uQ3VzdG9tUmVzb3VyY2VFdmVudCAmIHsgUGh5c2ljYWxSZXNvdXJjZUlkPzogc3RyaW5nIH0sXG4gIGhhbmRsZXJSZXNwb25zZTogdm9pZCB8IEhhbmRsZXJSZXNwb25zZSA9IHsgfSk6IFJlc3BvbnNlIHtcblxuICAvLyBpZiBwaHlzaWNhbCBJRCBpcyBub3QgcmV0dXJuZWQsIHdlIGhhdmUgc29tZSBkZWZhdWx0cyBmb3IgeW91IGJhc2VkXG4gIC8vIG9uIHRoZSByZXF1ZXN0IHR5cGUuXG4gIGNvbnN0IHBoeXNpY2FsUmVzb3VyY2VJZCA9IGhhbmRsZXJSZXNwb25zZS5QaHlzaWNhbFJlc291cmNlSWQgPz8gY2ZuUmVxdWVzdC5QaHlzaWNhbFJlc291cmNlSWQgPz8gY2ZuUmVxdWVzdC5SZXF1ZXN0SWQ7XG5cbiAgLy8gaWYgd2UgYXJlIGluIERFTEVURSBhbmQgcGh5c2ljYWwgSUQgd2FzIGNoYW5nZWQsIGl0J3MgYW4gZXJyb3IuXG4gIGlmIChjZm5SZXF1ZXN0LlJlcXVlc3RUeXBlID09PSAnRGVsZXRlJyAmJiBwaHlzaWNhbFJlc291cmNlSWQgIT09IGNmblJlcXVlc3QuUGh5c2ljYWxSZXNvdXJjZUlkKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKGBERUxFVEU6IGNhbm5vdCBjaGFuZ2UgdGhlIHBoeXNpY2FsIHJlc291cmNlIElEIGZyb20gXCIke2NmblJlcXVlc3QuUGh5c2ljYWxSZXNvdXJjZUlkfVwiIHRvIFwiJHtoYW5kbGVyUmVzcG9uc2UuUGh5c2ljYWxSZXNvdXJjZUlkfVwiIGR1cmluZyBkZWxldGlvbmApO1xuICB9XG5cbiAgLy8gbWVyZ2UgcmVxdWVzdCBldmVudCBhbmQgcmVzdWx0IGV2ZW50IChyZXN1bHQgcHJldmFpbHMpLlxuICByZXR1cm4ge1xuICAgIC4uLmNmblJlcXVlc3QsXG4gICAgLi4uaGFuZGxlclJlc3BvbnNlLFxuICAgIFBoeXNpY2FsUmVzb3VyY2VJZDogcGh5c2ljYWxSZXNvdXJjZUlkLFxuICB9O1xufVxuXG5hc3luYyBmdW5jdGlvbiBzdWJtaXRSZXNwb25zZShzdGF0dXM6ICdTVUNDRVNTJyB8ICdGQUlMRUQnLCBldmVudDogUmVzcG9uc2UpIHtcbiAgY29uc3QganNvbjogQVdTTGFtYmRhLkNsb3VkRm9ybWF0aW9uQ3VzdG9tUmVzb3VyY2VSZXNwb25zZSA9IHtcbiAgICBTdGF0dXM6IHN0YXR1cyxcbiAgICBSZWFzb246IGV2ZW50LlJlYXNvbiA/PyBzdGF0dXMsXG4gICAgU3RhY2tJZDogZXZlbnQuU3RhY2tJZCxcbiAgICBSZXF1ZXN0SWQ6IGV2ZW50LlJlcXVlc3RJZCxcbiAgICBQaHlzaWNhbFJlc291cmNlSWQ6IGV2ZW50LlBoeXNpY2FsUmVzb3VyY2VJZCB8fCBNSVNTSU5HX1BIWVNJQ0FMX0lEX01BUktFUixcbiAgICBMb2dpY2FsUmVzb3VyY2VJZDogZXZlbnQuTG9naWNhbFJlc291cmNlSWQsXG4gICAgTm9FY2hvOiBldmVudC5Ob0VjaG8sXG4gICAgRGF0YTogZXZlbnQuRGF0YSxcbiAgfTtcblxuICBleHRlcm5hbC5sb2coJ3N1Ym1pdCByZXNwb25zZSB0byBjbG91ZGZvcm1hdGlvbicsIGpzb24pO1xuXG4gIGNvbnN0IHJlc3BvbnNlQm9keSA9IEpTT04uc3RyaW5naWZ5KGpzb24pO1xuICBjb25zdCBwYXJzZWRVcmwgPSB1cmwucGFyc2UoZXZlbnQuUmVzcG9uc2VVUkwpO1xuICBjb25zdCByZXEgPSB7XG4gICAgaG9zdG5hbWU6IHBhcnNlZFVybC5ob3N0bmFtZSxcbiAgICBwYXRoOiBwYXJzZWRVcmwucGF0aCxcbiAgICBtZXRob2Q6ICdQVVQnLFxuICAgIGhlYWRlcnM6IHtcbiAgICAgICdjb250ZW50LXR5cGUnOiAnJyxcbiAgICAgICdjb250ZW50LWxlbmd0aCc6IEJ1ZmZlci5ieXRlTGVuZ3RoKHJlc3BvbnNlQm9keSwgJ3V0ZjgnKSxcbiAgICB9LFxuICB9O1xuXG4gIGNvbnN0IHJldHJ5T3B0aW9ucyA9IHtcbiAgICBhdHRlbXB0czogNSxcbiAgICBzbGVlcDogMTAwMCxcbiAgfTtcbiAgYXdhaXQgd2l0aFJldHJpZXMocmV0cnlPcHRpb25zLCBleHRlcm5hbC5zZW5kSHR0cFJlcXVlc3QpKHJlcSwgcmVzcG9uc2VCb2R5KTtcbn1cblxuYXN5bmMgZnVuY3Rpb24gZGVmYXVsdFNlbmRIdHRwUmVxdWVzdChvcHRpb25zOiBodHRwcy5SZXF1ZXN0T3B0aW9ucywgcmVzcG9uc2VCb2R5OiBzdHJpbmcpOiBQcm9taXNlPHZvaWQ+IHtcbiAgcmV0dXJuIG5ldyBQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICB0cnkge1xuICAgICAgY29uc3QgcmVxdWVzdCA9IGh0dHBzLnJlcXVlc3Qob3B0aW9ucywgXyA9PiByZXNvbHZlKCkpO1xuICAgICAgcmVxdWVzdC5vbignZXJyb3InLCByZWplY3QpO1xuICAgICAgcmVxdWVzdC53cml0ZShyZXNwb25zZUJvZHkpO1xuICAgICAgcmVxdWVzdC5lbmQoKTtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICByZWplY3QoZSk7XG4gICAgfVxuICB9KTtcbn1cblxuZnVuY3Rpb24gZGVmYXVsdExvZyhmbXQ6IHN0cmluZywgLi4ucGFyYW1zOiBhbnlbXSkge1xuICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tY29uc29sZVxuICBjb25zb2xlLmxvZyhmbXQsIC4uLnBhcmFtcyk7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgUmV0cnlPcHRpb25zIHtcbiAgLyoqIEhvdyBtYW55IHJldHJpZXMgKHdpbGwgYXQgbGVhc3QgdHJ5IG9uY2UpICovXG4gIHJlYWRvbmx5IGF0dGVtcHRzOiBudW1iZXI7XG4gIC8qKiBTbGVlcCBiYXNlLCBpbiBtcyAqL1xuICByZWFkb25seSBzbGVlcDogbnVtYmVyO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gd2l0aFJldHJpZXM8QSBleHRlbmRzIEFycmF5PGFueT4sIEI+KG9wdGlvbnM6IFJldHJ5T3B0aW9ucywgZm46ICguLi54czogQSkgPT4gUHJvbWlzZTxCPik6ICguLi54czogQSkgPT4gUHJvbWlzZTxCPiB7XG4gIHJldHVybiBhc3luYyAoLi4ueHM6IEEpID0+IHtcbiAgICBsZXQgYXR0ZW1wdHMgPSBvcHRpb25zLmF0dGVtcHRzO1xuICAgIGxldCBtcyA9IG9wdGlvbnMuc2xlZXA7XG4gICAgd2hpbGUgKHRydWUpIHtcbiAgICAgIHRyeSB7XG4gICAgICAgIHJldHVybiBhd2FpdCBmbiguLi54cyk7XG4gICAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIGlmIChhdHRlbXB0cy0tIDw9IDApIHtcbiAgICAgICAgICB0aHJvdyBlO1xuICAgICAgICB9XG4gICAgICAgIGF3YWl0IHNsZWVwKE1hdGguZmxvb3IoTWF0aC5yYW5kb20oKSAqIG1zKSk7XG4gICAgICAgIG1zICo9IDI7XG4gICAgICB9XG4gICAgfVxuICB9O1xufVxuXG5hc3luYyBmdW5jdGlvbiBzbGVlcChtczogbnVtYmVyKTogUHJvbWlzZTx2b2lkPiB7XG4gIHJldHVybiBuZXcgUHJvbWlzZSgob2spID0+IHNldFRpbWVvdXQob2ssIG1zKSk7XG59Il19 \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/asset.6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e/index.js b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/asset.6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e/index.js new file mode 100644 index 0000000000000..442bb4f1c65b8 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/asset.6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e/index.js @@ -0,0 +1,87 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.handler = void 0; +// eslint-disable-next-line import/no-extraneous-dependencies +const aws_sdk_1 = require("aws-sdk"); +const AUTO_DELETE_IMAGES_TAG = 'aws-cdk:auto-delete-images'; +const ecr = new aws_sdk_1.ECR(); +async function handler(event) { + switch (event.RequestType) { + case 'Create': + break; + case 'Update': + return onUpdate(event); + case 'Delete': + return onDelete(event.ResourceProperties?.RepositoryName); + } +} +exports.handler = handler; +async function onUpdate(event) { + const updateEvent = event; + const oldRepositoryName = updateEvent.OldResourceProperties?.RepositoryName; + const newRepositoryName = updateEvent.ResourceProperties?.RepositoryName; + const repositoryNameHasChanged = (newRepositoryName && oldRepositoryName) + && (newRepositoryName !== oldRepositoryName); + /* If the name of the repository has changed, CloudFormation will try to delete the repository + and create a new one with the new name. So we have to delete the images in the + repository so that this operation does not fail. */ + if (repositoryNameHasChanged) { + return onDelete(oldRepositoryName); + } +} +/** + * Recursively delete all images in the repository + * + * @param ECR.ListImagesRequest the repositoryName & nextToken if presented + */ +async function emptyRepository(params) { + const listedImages = await ecr.listImages(params).promise(); + const imageIds = listedImages?.imageIds ?? []; + const nextToken = listedImages.nextToken ?? null; + if (imageIds.length === 0) { + return; + } + await ecr.batchDeleteImage({ + repositoryName: params.repositoryName, + imageIds, + }).promise(); + if (nextToken) { + await emptyRepository({ + ...params, + nextToken, + }); + } +} +async function onDelete(repositoryName) { + if (!repositoryName) { + throw new Error('No RepositoryName was provided.'); + } + const response = await ecr.describeRepositories({ repositoryNames: [repositoryName] }).promise(); + const repository = response.repositories?.find(repo => repo.repositoryName === repositoryName); + if (!await isRepositoryTaggedForDeletion(repository?.repositoryArn)) { + process.stdout.write(`Repository does not have '${AUTO_DELETE_IMAGES_TAG}' tag, skipping cleaning.\n`); + return; + } + try { + await emptyRepository({ repositoryName }); + } + catch (e) { + if (e.name !== 'RepositoryNotFoundException') { + throw e; + } + // Repository doesn't exist. Ignoring + } +} +/** + * The repository will only be tagged for deletion if it's being deleted in the same + * deployment as this Custom Resource. + * + * If the Custom Resource is ever deleted before the repository, it must be because + * `autoDeleteImages` has been switched to false, in which case the tag would have + * been removed before we get to this Delete event. + */ +async function isRepositoryTaggedForDeletion(repositoryArn) { + const response = await ecr.listTagsForResource({ resourceArn: repositoryArn }).promise(); + return response.tags?.some(tag => tag.Key === AUTO_DELETE_IMAGES_TAG && tag.Value === 'true'); +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSw2REFBNkQ7QUFDN0QscUNBQThCO0FBRTlCLE1BQU0sc0JBQXNCLEdBQUcsNEJBQTRCLENBQUM7QUFFNUQsTUFBTSxHQUFHLEdBQUcsSUFBSSxhQUFHLEVBQUUsQ0FBQztBQUVmLEtBQUssVUFBVSxPQUFPLENBQUMsS0FBa0Q7SUFDOUUsUUFBUSxLQUFLLENBQUMsV0FBVyxFQUFFO1FBQ3pCLEtBQUssUUFBUTtZQUNYLE1BQU07UUFDUixLQUFLLFFBQVE7WUFDWCxPQUFPLFFBQVEsQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUN6QixLQUFLLFFBQVE7WUFDWCxPQUFPLFFBQVEsQ0FBQyxLQUFLLENBQUMsa0JBQWtCLEVBQUUsY0FBYyxDQUFDLENBQUM7S0FDN0Q7QUFDSCxDQUFDO0FBVEQsMEJBU0M7QUFFRCxLQUFLLFVBQVUsUUFBUSxDQUFDLEtBQWtEO0lBQ3hFLE1BQU0sV0FBVyxHQUFHLEtBQTBELENBQUM7SUFDL0UsTUFBTSxpQkFBaUIsR0FBRyxXQUFXLENBQUMscUJBQXFCLEVBQUUsY0FBYyxDQUFDO0lBQzVFLE1BQU0saUJBQWlCLEdBQUcsV0FBVyxDQUFDLGtCQUFrQixFQUFFLGNBQWMsQ0FBQztJQUN6RSxNQUFNLHdCQUF3QixHQUFHLENBQUMsaUJBQWlCLElBQUksaUJBQWlCLENBQUM7V0FDcEUsQ0FBQyxpQkFBaUIsS0FBSyxpQkFBaUIsQ0FBQyxDQUFDO0lBRS9DOzswREFFc0Q7SUFDdEQsSUFBSSx3QkFBd0IsRUFBRTtRQUM1QixPQUFPLFFBQVEsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0tBQ3BDO0FBQ0gsQ0FBQztBQUVEOzs7O0dBSUc7QUFDSCxLQUFLLFVBQVUsZUFBZSxDQUFDLE1BQTZCO0lBQzFELE1BQU0sWUFBWSxHQUFHLE1BQU0sR0FBRyxDQUFDLFVBQVUsQ0FBQyxNQUFNLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQztJQUU1RCxNQUFNLFFBQVEsR0FBRyxZQUFZLEVBQUUsUUFBUSxJQUFJLEVBQUUsQ0FBQztJQUM5QyxNQUFNLFNBQVMsR0FBRyxZQUFZLENBQUMsU0FBUyxJQUFJLElBQUksQ0FBQztJQUNqRCxJQUFJLFFBQVEsQ0FBQyxNQUFNLEtBQUssQ0FBQyxFQUFFO1FBQ3pCLE9BQU87S0FDUjtJQUVELE1BQU0sR0FBRyxDQUFDLGdCQUFnQixDQUFDO1FBQ3pCLGNBQWMsRUFBRSxNQUFNLENBQUMsY0FBYztRQUNyQyxRQUFRO0tBQ1QsQ0FBQyxDQUFDLE9BQU8sRUFBRSxDQUFDO0lBRWIsSUFBSSxTQUFTLEVBQUU7UUFDYixNQUFNLGVBQWUsQ0FBQztZQUNwQixHQUFHLE1BQU07WUFDVCxTQUFTO1NBQ1YsQ0FBQyxDQUFDO0tBQ0o7QUFDSCxDQUFDO0FBRUQsS0FBSyxVQUFVLFFBQVEsQ0FBQyxjQUFzQjtJQUM1QyxJQUFJLENBQUMsY0FBYyxFQUFFO1FBQ25CLE1BQU0sSUFBSSxLQUFLLENBQUMsaUNBQWlDLENBQUMsQ0FBQztLQUNwRDtJQUVELE1BQU0sUUFBUSxHQUFHLE1BQU0sR0FBRyxDQUFDLG9CQUFvQixDQUFDLEVBQUUsZUFBZSxFQUFFLENBQUMsY0FBYyxDQUFDLEVBQUUsQ0FBQyxDQUFDLE9BQU8sRUFBRSxDQUFDO0lBQ2pHLE1BQU0sVUFBVSxHQUFHLFFBQVEsQ0FBQyxZQUFZLEVBQUUsSUFBSSxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUFDLGNBQWMsS0FBSyxjQUFjLENBQUMsQ0FBQztJQUUvRixJQUFJLENBQUMsTUFBTSw2QkFBNkIsQ0FBQyxVQUFVLEVBQUUsYUFBYyxDQUFDLEVBQUU7UUFDcEUsT0FBTyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsNkJBQTZCLHNCQUFzQiw2QkFBNkIsQ0FBQyxDQUFDO1FBQ3ZHLE9BQU87S0FDUjtJQUNELElBQUk7UUFDRixNQUFNLGVBQWUsQ0FBQyxFQUFFLGNBQWMsRUFBRSxDQUFDLENBQUM7S0FDM0M7SUFBQyxPQUFPLENBQUMsRUFBRTtRQUNWLElBQUksQ0FBQyxDQUFDLElBQUksS0FBSyw2QkFBNkIsRUFBRTtZQUM1QyxNQUFNLENBQUMsQ0FBQztTQUNUO1FBQ0QscUNBQXFDO0tBQ3RDO0FBQ0gsQ0FBQztBQUVEOzs7Ozs7O0dBT0c7QUFDSCxLQUFLLFVBQVUsNkJBQTZCLENBQUMsYUFBcUI7SUFDaEUsTUFBTSxRQUFRLEdBQUcsTUFBTSxHQUFHLENBQUMsbUJBQW1CLENBQUMsRUFBRSxXQUFXLEVBQUUsYUFBYSxFQUFFLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQztJQUN6RixPQUFPLFFBQVEsQ0FBQyxJQUFJLEVBQUUsSUFBSSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsR0FBRyxDQUFDLEdBQUcsS0FBSyxzQkFBc0IsSUFBSSxHQUFHLENBQUMsS0FBSyxLQUFLLE1BQU0sQ0FBQyxDQUFDO0FBQ2hHLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgaW1wb3J0L25vLWV4dHJhbmVvdXMtZGVwZW5kZW5jaWVzXG5pbXBvcnQgeyBFQ1IgfSBmcm9tICdhd3Mtc2RrJztcblxuY29uc3QgQVVUT19ERUxFVEVfSU1BR0VTX1RBRyA9ICdhd3MtY2RrOmF1dG8tZGVsZXRlLWltYWdlcyc7XG5cbmNvbnN0IGVjciA9IG5ldyBFQ1IoKTtcblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGhhbmRsZXIoZXZlbnQ6IEFXU0xhbWJkYS5DbG91ZEZvcm1hdGlvbkN1c3RvbVJlc291cmNlRXZlbnQpIHtcbiAgc3dpdGNoIChldmVudC5SZXF1ZXN0VHlwZSkge1xuICAgIGNhc2UgJ0NyZWF0ZSc6XG4gICAgICBicmVhaztcbiAgICBjYXNlICdVcGRhdGUnOlxuICAgICAgcmV0dXJuIG9uVXBkYXRlKGV2ZW50KTtcbiAgICBjYXNlICdEZWxldGUnOlxuICAgICAgcmV0dXJuIG9uRGVsZXRlKGV2ZW50LlJlc291cmNlUHJvcGVydGllcz8uUmVwb3NpdG9yeU5hbWUpO1xuICB9XG59XG5cbmFzeW5jIGZ1bmN0aW9uIG9uVXBkYXRlKGV2ZW50OiBBV1NMYW1iZGEuQ2xvdWRGb3JtYXRpb25DdXN0b21SZXNvdXJjZUV2ZW50KSB7XG4gIGNvbnN0IHVwZGF0ZUV2ZW50ID0gZXZlbnQgYXMgQVdTTGFtYmRhLkNsb3VkRm9ybWF0aW9uQ3VzdG9tUmVzb3VyY2VVcGRhdGVFdmVudDtcbiAgY29uc3Qgb2xkUmVwb3NpdG9yeU5hbWUgPSB1cGRhdGVFdmVudC5PbGRSZXNvdXJjZVByb3BlcnRpZXM/LlJlcG9zaXRvcnlOYW1lO1xuICBjb25zdCBuZXdSZXBvc2l0b3J5TmFtZSA9IHVwZGF0ZUV2ZW50LlJlc291cmNlUHJvcGVydGllcz8uUmVwb3NpdG9yeU5hbWU7XG4gIGNvbnN0IHJlcG9zaXRvcnlOYW1lSGFzQ2hhbmdlZCA9IChuZXdSZXBvc2l0b3J5TmFtZSAmJiBvbGRSZXBvc2l0b3J5TmFtZSlcbiAgICAmJiAobmV3UmVwb3NpdG9yeU5hbWUgIT09IG9sZFJlcG9zaXRvcnlOYW1lKTtcblxuICAvKiBJZiB0aGUgbmFtZSBvZiB0aGUgcmVwb3NpdG9yeSBoYXMgY2hhbmdlZCwgQ2xvdWRGb3JtYXRpb24gd2lsbCB0cnkgdG8gZGVsZXRlIHRoZSByZXBvc2l0b3J5XG4gICAgIGFuZCBjcmVhdGUgYSBuZXcgb25lIHdpdGggdGhlIG5ldyBuYW1lLiBTbyB3ZSBoYXZlIHRvIGRlbGV0ZSB0aGUgaW1hZ2VzIGluIHRoZVxuICAgICByZXBvc2l0b3J5IHNvIHRoYXQgdGhpcyBvcGVyYXRpb24gZG9lcyBub3QgZmFpbC4gKi9cbiAgaWYgKHJlcG9zaXRvcnlOYW1lSGFzQ2hhbmdlZCkge1xuICAgIHJldHVybiBvbkRlbGV0ZShvbGRSZXBvc2l0b3J5TmFtZSk7XG4gIH1cbn1cblxuLyoqXG4gKiBSZWN1cnNpdmVseSBkZWxldGUgYWxsIGltYWdlcyBpbiB0aGUgcmVwb3NpdG9yeVxuICpcbiAqIEBwYXJhbSBFQ1IuTGlzdEltYWdlc1JlcXVlc3QgdGhlIHJlcG9zaXRvcnlOYW1lICYgbmV4dFRva2VuIGlmIHByZXNlbnRlZFxuICovXG5hc3luYyBmdW5jdGlvbiBlbXB0eVJlcG9zaXRvcnkocGFyYW1zOiBFQ1IuTGlzdEltYWdlc1JlcXVlc3QpIHtcbiAgY29uc3QgbGlzdGVkSW1hZ2VzID0gYXdhaXQgZWNyLmxpc3RJbWFnZXMocGFyYW1zKS5wcm9taXNlKCk7XG5cbiAgY29uc3QgaW1hZ2VJZHMgPSBsaXN0ZWRJbWFnZXM/LmltYWdlSWRzID8/IFtdO1xuICBjb25zdCBuZXh0VG9rZW4gPSBsaXN0ZWRJbWFnZXMubmV4dFRva2VuID8/IG51bGw7XG4gIGlmIChpbWFnZUlkcy5sZW5ndGggPT09IDApIHtcbiAgICByZXR1cm47XG4gIH1cblxuICBhd2FpdCBlY3IuYmF0Y2hEZWxldGVJbWFnZSh7XG4gICAgcmVwb3NpdG9yeU5hbWU6IHBhcmFtcy5yZXBvc2l0b3J5TmFtZSxcbiAgICBpbWFnZUlkcyxcbiAgfSkucHJvbWlzZSgpO1xuXG4gIGlmIChuZXh0VG9rZW4pIHtcbiAgICBhd2FpdCBlbXB0eVJlcG9zaXRvcnkoe1xuICAgICAgLi4ucGFyYW1zLFxuICAgICAgbmV4dFRva2VuLFxuICAgIH0pO1xuICB9XG59XG5cbmFzeW5jIGZ1bmN0aW9uIG9uRGVsZXRlKHJlcG9zaXRvcnlOYW1lOiBzdHJpbmcpIHtcbiAgaWYgKCFyZXBvc2l0b3J5TmFtZSkge1xuICAgIHRocm93IG5ldyBFcnJvcignTm8gUmVwb3NpdG9yeU5hbWUgd2FzIHByb3ZpZGVkLicpO1xuICB9XG5cbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBlY3IuZGVzY3JpYmVSZXBvc2l0b3JpZXMoeyByZXBvc2l0b3J5TmFtZXM6IFtyZXBvc2l0b3J5TmFtZV0gfSkucHJvbWlzZSgpO1xuICBjb25zdCByZXBvc2l0b3J5ID0gcmVzcG9uc2UucmVwb3NpdG9yaWVzPy5maW5kKHJlcG8gPT4gcmVwby5yZXBvc2l0b3J5TmFtZSA9PT0gcmVwb3NpdG9yeU5hbWUpO1xuXG4gIGlmICghYXdhaXQgaXNSZXBvc2l0b3J5VGFnZ2VkRm9yRGVsZXRpb24ocmVwb3NpdG9yeT8ucmVwb3NpdG9yeUFybiEpKSB7XG4gICAgcHJvY2Vzcy5zdGRvdXQud3JpdGUoYFJlcG9zaXRvcnkgZG9lcyBub3QgaGF2ZSAnJHtBVVRPX0RFTEVURV9JTUFHRVNfVEFHfScgdGFnLCBza2lwcGluZyBjbGVhbmluZy5cXG5gKTtcbiAgICByZXR1cm47XG4gIH1cbiAgdHJ5IHtcbiAgICBhd2FpdCBlbXB0eVJlcG9zaXRvcnkoeyByZXBvc2l0b3J5TmFtZSB9KTtcbiAgfSBjYXRjaCAoZSkge1xuICAgIGlmIChlLm5hbWUgIT09ICdSZXBvc2l0b3J5Tm90Rm91bmRFeGNlcHRpb24nKSB7XG4gICAgICB0aHJvdyBlO1xuICAgIH1cbiAgICAvLyBSZXBvc2l0b3J5IGRvZXNuJ3QgZXhpc3QuIElnbm9yaW5nXG4gIH1cbn1cblxuLyoqXG4gKiBUaGUgcmVwb3NpdG9yeSB3aWxsIG9ubHkgYmUgdGFnZ2VkIGZvciBkZWxldGlvbiBpZiBpdCdzIGJlaW5nIGRlbGV0ZWQgaW4gdGhlIHNhbWVcbiAqIGRlcGxveW1lbnQgYXMgdGhpcyBDdXN0b20gUmVzb3VyY2UuXG4gKlxuICogSWYgdGhlIEN1c3RvbSBSZXNvdXJjZSBpcyBldmVyIGRlbGV0ZWQgYmVmb3JlIHRoZSByZXBvc2l0b3J5LCBpdCBtdXN0IGJlIGJlY2F1c2VcbiAqIGBhdXRvRGVsZXRlSW1hZ2VzYCBoYXMgYmVlbiBzd2l0Y2hlZCB0byBmYWxzZSwgaW4gd2hpY2ggY2FzZSB0aGUgdGFnIHdvdWxkIGhhdmVcbiAqIGJlZW4gcmVtb3ZlZCBiZWZvcmUgd2UgZ2V0IHRvIHRoaXMgRGVsZXRlIGV2ZW50LlxuICovXG5hc3luYyBmdW5jdGlvbiBpc1JlcG9zaXRvcnlUYWdnZWRGb3JEZWxldGlvbihyZXBvc2l0b3J5QXJuOiBzdHJpbmcpIHtcbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBlY3IubGlzdFRhZ3NGb3JSZXNvdXJjZSh7IHJlc291cmNlQXJuOiByZXBvc2l0b3J5QXJuIH0pLnByb21pc2UoKTtcbiAgcmV0dXJuIHJlc3BvbnNlLnRhZ3M/LnNvbWUodGFnID0+IHRhZy5LZXkgPT09IEFVVE9fREVMRVRFX0lNQUdFU19UQUcgJiYgdGFnLlZhbHVlID09PSAndHJ1ZScpO1xufSJdfQ== \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/aws-ecr-integ-stack.assets.json b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/aws-ecr-integ-stack.assets.json new file mode 100644 index 0000000000000..309ab8a13dc43 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/aws-ecr-integ-stack.assets.json @@ -0,0 +1,32 @@ +{ + "version": "31.0.0", + "files": { + "6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e": { + "source": { + "path": "asset.6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "fae74e473c2d47b28ba8bb9bdcf00530bc1f6b9032ff87dc77d277c8b9c100a5": { + "source": { + "path": "aws-ecr-integ-stack.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "fae74e473c2d47b28ba8bb9bdcf00530bc1f6b9032ff87dc77d277c8b9c100a5.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/aws-ecr-integ-stack.template.json b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/aws-ecr-integ-stack.template.json new file mode 100644 index 0000000000000..e755da927abf5 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/aws-ecr-integ-stack.template.json @@ -0,0 +1,197 @@ +{ + "Resources": { + "Repo02AC86CF": { + "Type": "AWS::ECR::Repository", + "Properties": { + "RepositoryName": "delete-even-if-containing-images", + "Tags": [ + { + "Key": "aws-cdk:auto-delete-images", + "Value": "true" + } + ] + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "RepoAutoDeleteImagesCustomResource65201E29": { + "Type": "Custom::ECRAutoDeleteImages", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomECRAutoDeleteImagesCustomResourceProviderHandler8D89C030", + "Arn" + ] + }, + "RepositoryName": { + "Ref": "Repo02AC86CF" + } + }, + "DependsOn": [ + "Repo02AC86CF" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomECRAutoDeleteImagesCustomResourceProviderRole665F2773": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ], + "Policies": [ + { + "PolicyName": "Inline", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ecr:BatchDeleteImage", + "ecr:DescribeRepositories", + "ecr:ListImages", + "ecr:ListTagsForResource" + ], + "Resource": [ + { + "Fn::GetAtt": [ + "Repo02AC86CF", + "Arn" + ] + } + ] + } + ] + } + } + ] + } + }, + "CustomECRAutoDeleteImagesCustomResourceProviderHandler8D89C030": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e.zip" + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "__entrypoint__.handler", + "Role": { + "Fn::GetAtt": [ + "CustomECRAutoDeleteImagesCustomResourceProviderRole665F2773", + "Arn" + ] + }, + "Runtime": "nodejs14.x", + "Description": "Lambda function for auto-deleting images in undefined repository." + }, + "DependsOn": [ + "CustomECRAutoDeleteImagesCustomResourceProviderRole665F2773" + ] + } + }, + "Outputs": { + "RepositoryURI": { + "Value": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "Repo02AC86CF", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "Repo02AC86CF", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "Repo02AC86CF" + } + ] + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/cdk.out b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/cdk.out new file mode 100644 index 0000000000000..7925065efbcc4 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"31.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.assets.json b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.assets.json new file mode 100644 index 0000000000000..9ff70a8cba8bc --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.assets.json @@ -0,0 +1,19 @@ +{ + "version": "31.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.template.json b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/integ.json b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/integ.json new file mode 100644 index 0000000000000..3b51355c51c47 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "31.0.0", + "testCases": { + "cdk-integ-auto-delete-images/DefaultTest": { + "stacks": [ + "aws-ecr-integ-stack" + ], + "assertionStack": "cdk-integ-auto-delete-images/DefaultTest/DeployAssert", + "assertionStackName": "cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/manifest.json b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/manifest.json new file mode 100644 index 0000000000000..849b1be4fed32 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/manifest.json @@ -0,0 +1,135 @@ +{ + "version": "31.0.0", + "artifacts": { + "aws-ecr-integ-stack.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-ecr-integ-stack.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-ecr-integ-stack": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-ecr-integ-stack.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/fae74e473c2d47b28ba8bb9bdcf00530bc1f6b9032ff87dc77d277c8b9c100a5.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-ecr-integ-stack.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-ecr-integ-stack.assets" + ], + "metadata": { + "/aws-ecr-integ-stack/Repo/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Repo02AC86CF" + } + ], + "/aws-ecr-integ-stack/Repo/AutoDeleteImagesCustomResource/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "RepoAutoDeleteImagesCustomResource65201E29" + } + ], + "/aws-ecr-integ-stack/Custom::ECRAutoDeleteImagesCustomResourceProvider/Role": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomECRAutoDeleteImagesCustomResourceProviderRole665F2773" + } + ], + "/aws-ecr-integ-stack/Custom::ECRAutoDeleteImagesCustomResourceProvider/Handler": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomECRAutoDeleteImagesCustomResourceProviderHandler8D89C030" + } + ], + "/aws-ecr-integ-stack/RepositoryURI": [ + { + "type": "aws:cdk:logicalId", + "data": "RepositoryURI" + } + ], + "/aws-ecr-integ-stack/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-ecr-integ-stack/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-ecr-integ-stack" + }, + "cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.assets" + ], + "metadata": { + "/cdk-integ-auto-delete-images/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/cdk-integ-auto-delete-images/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "cdk-integ-auto-delete-images/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/tree.json b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/tree.json new file mode 100644 index 0000000000000..3d686786ff658 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/tree.json @@ -0,0 +1,191 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "aws-ecr-integ-stack": { + "id": "aws-ecr-integ-stack", + "path": "aws-ecr-integ-stack", + "children": { + "Repo": { + "id": "Repo", + "path": "aws-ecr-integ-stack/Repo", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-ecr-integ-stack/Repo/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ECR::Repository", + "aws:cdk:cloudformation:props": { + "repositoryName": "delete-even-if-containing-images", + "tags": [ + { + "key": "aws-cdk:auto-delete-images", + "value": "true" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ecr.CfnRepository", + "version": "0.0.0" + } + }, + "AutoDeleteImagesCustomResource": { + "id": "AutoDeleteImagesCustomResource", + "path": "aws-ecr-integ-stack/Repo/AutoDeleteImagesCustomResource", + "children": { + "Default": { + "id": "Default", + "path": "aws-ecr-integ-stack/Repo/AutoDeleteImagesCustomResource/Default", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ecr.Repository", + "version": "0.0.0" + } + }, + "Custom::ECRAutoDeleteImagesCustomResourceProvider": { + "id": "Custom::ECRAutoDeleteImagesCustomResourceProvider", + "path": "aws-ecr-integ-stack/Custom::ECRAutoDeleteImagesCustomResourceProvider", + "children": { + "Staging": { + "id": "Staging", + "path": "aws-ecr-integ-stack/Custom::ECRAutoDeleteImagesCustomResourceProvider/Staging", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "Role": { + "id": "Role", + "path": "aws-ecr-integ-stack/Custom::ECRAutoDeleteImagesCustomResourceProvider/Role", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "Handler": { + "id": "Handler", + "path": "aws-ecr-integ-stack/Custom::ECRAutoDeleteImagesCustomResourceProvider/Handler", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResourceProvider", + "version": "0.0.0" + } + }, + "RepositoryURI": { + "id": "RepositoryURI", + "path": "aws-ecr-integ-stack/RepositoryURI", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-ecr-integ-stack/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-ecr-integ-stack/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "cdk-integ-auto-delete-images": { + "id": "cdk-integ-auto-delete-images", + "path": "cdk-integ-auto-delete-images", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "cdk-integ-auto-delete-images/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "cdk-integ-auto-delete-images/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.270" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "cdk-integ-auto-delete-images/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "cdk-integ-auto-delete-images/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "cdk-integ-auto-delete-images/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.270" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.ts b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.ts new file mode 100644 index 0000000000000..1d57788631713 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.ts @@ -0,0 +1,20 @@ +import * as cdk from '@aws-cdk/core'; +import { IntegTest } from '@aws-cdk/integ-tests'; +import * as ecr from '../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-ecr-integ-stack'); + +const repo = new ecr.Repository(stack, 'Repo', { + repositoryName: 'delete-even-if-containing-images', + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteImages: true, +}); + +new cdk.CfnOutput(stack, 'RepositoryURI', { + value: repo.repositoryUri, +}); + +new IntegTest(app, 'cdk-integ-auto-delete-images', { + testCases: [stack], +});