diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts index 9b7859b0c2e0e..3d0c8c25e39a3 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts @@ -3,7 +3,7 @@ import { IsCompleteResponse, OnEventResponse } from '@aws-cdk/custom-resources/lib/provider-framework/types'; // eslint-disable-next-line import/no-extraneous-dependencies import * as aws from 'aws-sdk'; -import { EksClient, ResourceHandler } from './common'; +import { EksClient, ResourceEvent, ResourceHandler } from './common'; const MAX_CLUSTER_NAME_LEN = 100; @@ -19,7 +19,7 @@ export class ClusterResourceHandler extends ResourceHandler { private readonly newProps: aws.EKS.CreateClusterRequest; private readonly oldProps: Partial; - constructor(eks: EksClient, event: AWSLambda.CloudFormationCustomResourceEvent) { + constructor(eks: EksClient, event: ResourceEvent) { super(eks, event); this.newProps = parseProps(this.event.ResourceProperties); @@ -127,15 +127,17 @@ export class ClusterResourceHandler extends ResourceHandler { throw new Error(`Cannot remove cluster version configuration. Current version is ${this.oldProps.version}`); } - await this.updateClusterVersion(this.newProps.version); + return await this.updateClusterVersion(this.newProps.version); } if (updates.updateLogging || updates.updateAccess) { - await this.eks.updateClusterConfig({ + const updateResponse = await this.eks.updateClusterConfig({ name: this.clusterName, logging: this.newProps.logging, resourcesVpcConfig: this.newProps.resourcesVpcConfig, }); + + return { EksUpdateId: updateResponse.update?.id }; } // no updates @@ -144,6 +146,12 @@ export class ClusterResourceHandler extends ResourceHandler { protected async isUpdateComplete() { console.log('isUpdateComplete'); + + // if this is an EKS update, we will monitor the update event itself + if (this.event.EksUpdateId) { + return this.isEksUpdateComplete(this.event.EksUpdateId); + } + return this.isActive(); } @@ -158,7 +166,8 @@ export class ClusterResourceHandler extends ResourceHandler { return; } - await this.eks.updateClusterVersion({ name: this.clusterName, version: newVersion }); + const updateResponse = await this.eks.updateClusterVersion({ name: this.clusterName, version: newVersion }); + return { EksUpdateId: updateResponse.update?.id }; } private async isActive(): Promise { @@ -187,6 +196,33 @@ export class ClusterResourceHandler extends ResourceHandler { } } + private async isEksUpdateComplete(eksUpdateId: string) { + this.log({ isEksUpdateComplete: eksUpdateId }); + + const describeUpdateResponse = await this.eks.describeUpdate({ + name: this.clusterName, + updateId: eksUpdateId, + }); + + this.log({ describeUpdateResponse }); + + if (!describeUpdateResponse.update) { + throw new Error(`unable to describe update with id "${eksUpdateId}"`); + } + + switch (describeUpdateResponse.update.status) { + case 'InProgress': + return { IsComplete: false }; + case 'Successful': + return { IsComplete: true }; + case 'Failed': + case 'Cancelled': + throw new Error(`cluster update id "${eksUpdateId}" failed with errors: ${JSON.stringify(describeUpdateResponse.update.errors)}`); + default: + throw new Error(`unknown status "${describeUpdateResponse.update.status}" for update id "${eksUpdateId}"`); + } + } + private generateClusterName() { const suffix = this.requestId.replace(/-/g, ''); // 32 chars const prefix = this.logicalResourceId.substr(0, MAX_CLUSTER_NAME_LEN - suffix.length - 1); diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/common.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/common.ts index 1349563bf0996..57d3ae20f8cef 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/common.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/common.ts @@ -3,14 +3,25 @@ import { IsCompleteResponse, OnEventResponse } from '@aws-cdk/custom-resources/l // eslint-disable-next-line import/no-extraneous-dependencies import * as aws from 'aws-sdk'; +export interface EksUpdateId { + /** + * If this field is included in an event passed to "IsComplete", it means we + * initiated an EKS update that should be monitored using eks:DescribeUpdate + * instead of just looking at the cluster status. + */ + EksUpdateId?: string +} + +export type ResourceEvent = AWSLambda.CloudFormationCustomResourceEvent & EksUpdateId; + export abstract class ResourceHandler { protected readonly requestId: string; protected readonly logicalResourceId: string; protected readonly requestType: 'Create' | 'Update' | 'Delete'; protected readonly physicalResourceId?: string; - protected readonly event: AWSLambda.CloudFormationCustomResourceEvent; + protected readonly event: ResourceEvent; - constructor(protected readonly eks: EksClient, event: AWSLambda.CloudFormationCustomResourceEvent) { + constructor(protected readonly eks: EksClient, event: ResourceEvent) { this.requestType = event.RequestType; this.requestId = event.RequestId; this.logicalResourceId = event.LogicalResourceId; @@ -55,7 +66,7 @@ export abstract class ResourceHandler { protected abstract async onCreate(): Promise; protected abstract async onDelete(): Promise; - protected abstract async onUpdate(): Promise; + protected abstract async onUpdate(): Promise<(OnEventResponse & EksUpdateId) | void>; protected abstract async isCreateComplete(): Promise; protected abstract async isDeleteComplete(): Promise; protected abstract async isUpdateComplete(): Promise; @@ -68,6 +79,7 @@ export interface EksClient { describeCluster(request: aws.EKS.DescribeClusterRequest): Promise; updateClusterConfig(request: aws.EKS.UpdateClusterConfigRequest): Promise; updateClusterVersion(request: aws.EKS.UpdateClusterVersionRequest): Promise; + describeUpdate(req: aws.EKS.DescribeUpdateRequest): Promise; createFargateProfile(request: aws.EKS.CreateFargateProfileRequest): Promise; describeFargateProfile(request: aws.EKS.DescribeFargateProfileRequest): Promise; deleteFargateProfile(request: aws.EKS.DeleteFargateProfileRequest): Promise; diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/index.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/index.ts index 7e12bc72b411f..f2b796297246a 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/index.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/index.ts @@ -16,6 +16,7 @@ const defaultEksClient: EksClient = { createCluster: req => getEksClient().createCluster(req).promise(), deleteCluster: req => getEksClient().deleteCluster(req).promise(), describeCluster: req => getEksClient().describeCluster(req).promise(), + describeUpdate: req => getEksClient().describeUpdate(req).promise(), updateClusterConfig: req => getEksClient().updateClusterConfig(req).promise(), updateClusterVersion: req => getEksClient().updateClusterVersion(req).promise(), createFargateProfile: req => getEksClient().createFargateProfile(req).promise(), diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts index e25198d5aa8d5..c449af08a407b 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts @@ -81,6 +81,7 @@ export class ClusterResource extends Construct { actions: [ 'eks:CreateCluster', 'eks:DescribeCluster', + 'eks:DescribeUpdate', 'eks:DeleteCluster', 'eks:UpdateClusterVersion', 'eks:UpdateClusterConfig', diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index 11013d34fc9a0..d035e341f1fde 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -1078,6 +1078,6 @@ export enum MachineImageType { const GPU_INSTANCETYPES = ['p2', 'p3', 'g4']; -export function nodeTypeForInstanceType(instanceType: ec2.InstanceType) { +function nodeTypeForInstanceType(instanceType: ec2.InstanceType) { return GPU_INSTANCETYPES.includes(instanceType.toString().substring(0, 2)) ? NodeType.GPU : NodeType.STANDARD; } diff --git a/packages/@aws-cdk/aws-eks/test/cluster-resource-handler-mocks.ts b/packages/@aws-cdk/aws-eks/test/cluster-resource-handler-mocks.ts index 7bcc866024418..c7980e0a89cf4 100644 --- a/packages/@aws-cdk/aws-eks/test/cluster-resource-handler-mocks.ts +++ b/packages/@aws-cdk/aws-eks/test/cluster-resource-handler-mocks.ts @@ -9,6 +9,7 @@ export let actualRequest: { configureAssumeRoleRequest?: sdk.STS.AssumeRoleRequest; createClusterRequest?: sdk.EKS.CreateClusterRequest; describeClusterRequest?: sdk.EKS.DescribeClusterRequest; + describeUpdateRequest?: sdk.EKS.DescribeUpdateRequest; deleteClusterRequest?: sdk.EKS.DeleteClusterRequest; updateClusterConfigRequest?: sdk.EKS.UpdateClusterConfigRequest; updateClusterVersionRequest?: sdk.EKS.UpdateClusterVersionRequest; @@ -22,6 +23,8 @@ export let actualRequest: { */ export let simulateResponse: { describeClusterResponseMockStatus?: string; + describeUpdateResponseMockStatus?: string; + describeUpdateResponseMockErrors?: sdk.EKS.ErrorDetails; deleteClusterErrorCode?: string; describeClusterExceptionCode?: string; } = { }; @@ -31,6 +34,8 @@ export function reset() { simulateResponse = { }; } +export const MOCK_UPDATE_STATUS_ID = 'MockEksUpdateStatusId'; + export const client: EksClient = { configureAssumeRole: req => { @@ -87,14 +92,34 @@ export const client: EksClient = { }; }, + describeUpdate: async req => { + actualRequest.describeUpdateRequest = req; + + return { + update: { + id: req.updateId, + errors: simulateResponse.describeUpdateResponseMockErrors, + status: simulateResponse.describeUpdateResponseMockStatus, + }, + }; + }, + updateClusterConfig: async req => { actualRequest.updateClusterConfigRequest = req; - return { }; + return { + update: { + id: MOCK_UPDATE_STATUS_ID, + }, + }; }, updateClusterVersion: async req => { actualRequest.updateClusterVersionRequest = req; - return { }; + return { + update: { + id: MOCK_UPDATE_STATUS_ID, + }, + }; }, createFargateProfile: async req => { diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json index 1f88a2c6b34b0..c168d90725028 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json @@ -802,6 +802,7 @@ "Action": [ "eks:CreateCluster", "eks:DescribeCluster", + "eks:DescribeUpdate", "eks:DeleteCluster", "eks:UpdateClusterVersion", "eks:UpdateClusterConfig", @@ -2231,7 +2232,7 @@ }, "/", { - "Ref": "AssetParameters5c7de45abd07f88cf62deefa6399553786f3559084ff34bae66042cdd1987d69S3Bucket835D19A2" + "Ref": "AssetParametersfa73027e9f72f21daca2d67aa5a23e88f87d90536a7e3c36de9adbfb27fa9103S3Bucket4281E0A4" }, "/", { @@ -2241,7 +2242,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5c7de45abd07f88cf62deefa6399553786f3559084ff34bae66042cdd1987d69S3VersionKeyBFF2DA61" + "Ref": "AssetParametersfa73027e9f72f21daca2d67aa5a23e88f87d90536a7e3c36de9adbfb27fa9103S3VersionKey3B54BD32" } ] } @@ -2254,7 +2255,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5c7de45abd07f88cf62deefa6399553786f3559084ff34bae66042cdd1987d69S3VersionKeyBFF2DA61" + "Ref": "AssetParametersfa73027e9f72f21daca2d67aa5a23e88f87d90536a7e3c36de9adbfb27fa9103S3VersionKey3B54BD32" } ] } @@ -2264,11 +2265,11 @@ ] }, "Parameters": { - "referencetoawscdkeksclustertestAssetParameters54c9eae68c19c65a224969094e8447eed31d811384c7e32bdb72fffb4be15ac8S3Bucket57C4C68FRef": { - "Ref": "AssetParameters54c9eae68c19c65a224969094e8447eed31d811384c7e32bdb72fffb4be15ac8S3Bucket7CCCFC30" + "referencetoawscdkeksclustertestAssetParametersc0e453b77d5ccf090915fba7c771380f8370da5cbcc3c7ed757c98addd75b602S3Bucket38D74D5ERef": { + "Ref": "AssetParametersc0e453b77d5ccf090915fba7c771380f8370da5cbcc3c7ed757c98addd75b602S3BucketD006BE3B" }, - "referencetoawscdkeksclustertestAssetParameters54c9eae68c19c65a224969094e8447eed31d811384c7e32bdb72fffb4be15ac8S3VersionKeyBB973CE7Ref": { - "Ref": "AssetParameters54c9eae68c19c65a224969094e8447eed31d811384c7e32bdb72fffb4be15ac8S3VersionKeyA2A28538" + "referencetoawscdkeksclustertestAssetParametersc0e453b77d5ccf090915fba7c771380f8370da5cbcc3c7ed757c98addd75b602S3VersionKey189EBCBARef": { + "Ref": "AssetParametersc0e453b77d5ccf090915fba7c771380f8370da5cbcc3c7ed757c98addd75b602S3VersionKeyEC71339F" }, "referencetoawscdkeksclustertestAssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3BucketC7CBF350Ref": { "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" @@ -2413,17 +2414,17 @@ } }, "Parameters": { - "AssetParameters54c9eae68c19c65a224969094e8447eed31d811384c7e32bdb72fffb4be15ac8S3Bucket7CCCFC30": { + "AssetParametersc0e453b77d5ccf090915fba7c771380f8370da5cbcc3c7ed757c98addd75b602S3BucketD006BE3B": { "Type": "String", - "Description": "S3 bucket for asset \"54c9eae68c19c65a224969094e8447eed31d811384c7e32bdb72fffb4be15ac8\"" + "Description": "S3 bucket for asset \"c0e453b77d5ccf090915fba7c771380f8370da5cbcc3c7ed757c98addd75b602\"" }, - "AssetParameters54c9eae68c19c65a224969094e8447eed31d811384c7e32bdb72fffb4be15ac8S3VersionKeyA2A28538": { + "AssetParametersc0e453b77d5ccf090915fba7c771380f8370da5cbcc3c7ed757c98addd75b602S3VersionKeyEC71339F": { "Type": "String", - "Description": "S3 key for asset version \"54c9eae68c19c65a224969094e8447eed31d811384c7e32bdb72fffb4be15ac8\"" + "Description": "S3 key for asset version \"c0e453b77d5ccf090915fba7c771380f8370da5cbcc3c7ed757c98addd75b602\"" }, - "AssetParameters54c9eae68c19c65a224969094e8447eed31d811384c7e32bdb72fffb4be15ac8ArtifactHashC0D1FA2A": { + "AssetParametersc0e453b77d5ccf090915fba7c771380f8370da5cbcc3c7ed757c98addd75b602ArtifactHash35F5D0CC": { "Type": "String", - "Description": "Artifact hash for asset \"54c9eae68c19c65a224969094e8447eed31d811384c7e32bdb72fffb4be15ac8\"" + "Description": "Artifact hash for asset \"c0e453b77d5ccf090915fba7c771380f8370da5cbcc3c7ed757c98addd75b602\"" }, "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C": { "Type": "String", @@ -2449,17 +2450,17 @@ "Type": "String", "Description": "Artifact hash for asset \"a6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afb\"" }, - "AssetParameters5c7de45abd07f88cf62deefa6399553786f3559084ff34bae66042cdd1987d69S3Bucket835D19A2": { + "AssetParametersfa73027e9f72f21daca2d67aa5a23e88f87d90536a7e3c36de9adbfb27fa9103S3Bucket4281E0A4": { "Type": "String", - "Description": "S3 bucket for asset \"5c7de45abd07f88cf62deefa6399553786f3559084ff34bae66042cdd1987d69\"" + "Description": "S3 bucket for asset \"fa73027e9f72f21daca2d67aa5a23e88f87d90536a7e3c36de9adbfb27fa9103\"" }, - "AssetParameters5c7de45abd07f88cf62deefa6399553786f3559084ff34bae66042cdd1987d69S3VersionKeyBFF2DA61": { + "AssetParametersfa73027e9f72f21daca2d67aa5a23e88f87d90536a7e3c36de9adbfb27fa9103S3VersionKey3B54BD32": { "Type": "String", - "Description": "S3 key for asset version \"5c7de45abd07f88cf62deefa6399553786f3559084ff34bae66042cdd1987d69\"" + "Description": "S3 key for asset version \"fa73027e9f72f21daca2d67aa5a23e88f87d90536a7e3c36de9adbfb27fa9103\"" }, - "AssetParameters5c7de45abd07f88cf62deefa6399553786f3559084ff34bae66042cdd1987d69ArtifactHash0E7708FD": { + "AssetParametersfa73027e9f72f21daca2d67aa5a23e88f87d90536a7e3c36de9adbfb27fa9103ArtifactHash733CC5DF": { "Type": "String", - "Description": "Artifact hash for asset \"5c7de45abd07f88cf62deefa6399553786f3559084ff34bae66042cdd1987d69\"" + "Description": "Artifact hash for asset \"fa73027e9f72f21daca2d67aa5a23e88f87d90536a7e3c36de9adbfb27fa9103\"" }, "AssetParameters36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbcS3Bucket2D824DEF": { "Type": "String", diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts b/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts index 33a7431894572..0759704acead4 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts @@ -353,7 +353,74 @@ export = { }, }, + 'isUpdateComplete with EKS update ID': { + + async 'with "Failed" status'(test: Test) { + const event = mocks.newRequest('Update'); + const isCompleteHandler = new ClusterResourceHandler(mocks.client, { + ...event, + EksUpdateId: 'foobar', + }); + + mocks.simulateResponse.describeUpdateResponseMockStatus = 'Failed'; + mocks.simulateResponse.describeUpdateResponseMockErrors = [ + { + errorMessage: 'errorMessageMock', + errorCode: 'errorCodeMock', + resourceIds: [ + 'foo', 'bar', + ], + }, + ]; + + let error; + try { + await isCompleteHandler.isComplete(); + } catch (e) { + error = e; + } + test.ok(error); + test.deepEqual(mocks.actualRequest.describeUpdateRequest, { name: 'physical-resource-id', updateId: 'foobar' }); + test.equal(error.message, 'cluster update id "foobar" failed with errors: [{"errorMessage":"errorMessageMock","errorCode":"errorCodeMock","resourceIds":["foo","bar"]}]'); + test.done(); + }, + + async 'with "InProgress" status, returns IsComplete=false'(test: Test) { + const event = mocks.newRequest('Update'); + const isCompleteHandler = new ClusterResourceHandler(mocks.client, { + ...event, + EksUpdateId: 'foobar', + }); + + mocks.simulateResponse.describeUpdateResponseMockStatus = 'InProgress'; + + const response = await isCompleteHandler.isComplete(); + + test.deepEqual(mocks.actualRequest.describeUpdateRequest, { name: 'physical-resource-id', updateId: 'foobar' }); + test.equal(response.IsComplete, false); + test.done(); + }, + + async 'with "Successful" status, returns IsComplete=true'(test: Test) { + const event = mocks.newRequest('Update'); + const isCompleteHandler = new ClusterResourceHandler(mocks.client, { + ...event, + EksUpdateId: 'foobar', + }); + + mocks.simulateResponse.describeUpdateResponseMockStatus = 'Successful'; + + const response = await isCompleteHandler.isComplete(); + + test.deepEqual(mocks.actualRequest.describeUpdateRequest, { name: 'physical-resource-id', updateId: 'foobar' }); + test.equal(response.IsComplete, true); + test.done(); + }, + + }, + 'in-place': { + 'version change': { async 'from undefined to a specific value'(test: Test) { const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { @@ -362,7 +429,7 @@ export = { version: undefined, })); const resp = await handler.onEvent(); - test.equal(resp, undefined); + test.deepEqual(resp, { EksUpdateId: mocks.MOCK_UPDATE_STATUS_ID }); test.deepEqual(mocks.actualRequest.updateClusterVersionRequest!, { name: 'physical-resource-id', version: '12.34', @@ -377,8 +444,9 @@ export = { }, { version: '1.1', })); + const resp = await handler.onEvent(); - test.equal(resp, undefined); + test.deepEqual(resp, { EksUpdateId: mocks.MOCK_UPDATE_STATUS_ID }); test.deepEqual(mocks.actualRequest.updateClusterVersionRequest!, { name: 'physical-resource-id', version: '2.0', diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts index eb2007c70e627..7e67d9f0c8632 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -831,6 +831,7 @@ export = { Action: [ 'eks:CreateCluster', 'eks:DescribeCluster', + 'eks:DescribeUpdate', 'eks:DeleteCluster', 'eks:UpdateClusterVersion', 'eks:UpdateClusterConfig', @@ -941,6 +942,7 @@ export = { Action: [ 'eks:CreateCluster', 'eks:DescribeCluster', + 'eks:DescribeUpdate', 'eks:DeleteCluster', 'eks:UpdateClusterVersion', 'eks:UpdateClusterConfig', diff --git a/packages/@aws-cdk/aws-eks/test/test.fargate-resource-provider.ts b/packages/@aws-cdk/aws-eks/test/test.fargate-resource-provider.ts index 873b1b499cdbe..5c119ae7a9c4e 100644 --- a/packages/@aws-cdk/aws-eks/test/test.fargate-resource-provider.ts +++ b/packages/@aws-cdk/aws-eks/test/test.fargate-resource-provider.ts @@ -287,6 +287,7 @@ function newEksClientMock() { createCluster: sinon.fake.throws('not implemented'), deleteCluster: sinon.fake.throws('not implemented'), describeCluster: sinon.fake.throws('not implemented'), + describeUpdate: sinon.fake.throws('not implemented'), updateClusterConfig: sinon.fake.throws('not implemented'), updateClusterVersion: sinon.fake.throws('not implemented'), configureAssumeRole: sinon.fake(), diff --git a/packages/@aws-cdk/custom-resources/README.md b/packages/@aws-cdk/custom-resources/README.md index d30028903575d..7d73424d9d12e 100644 --- a/packages/@aws-cdk/custom-resources/README.md +++ b/packages/@aws-cdk/custom-resources/README.md @@ -137,6 +137,7 @@ The return value from `onEvent` must be a JSON object with the following fields: |-----|----|--------|----------- |`PhysicalResourceId`|String|No|The allocated/assigned physical ID of the resource. If omitted for `Create` events, the event's `RequestId` will be used. For `Update`, the current physical ID will be used. If a different value is returned, CloudFormation will follow with a subsequent `Delete` for the previous ID (resource replacement). For `Delete`, it will always return the current physical resource ID, and if the user returns a different one, an error will occur. |`Data`|JSON|No|Resource attributes, which can later be retrieved through `Fn::GetAtt` on the custom resource object. +|*any*|*any*|No|Any other field included in the response will be passed through to `isComplete`. This can sometimes be useful to pass state between the handlers. [Custom Resource Provider Request]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-requests.html#crpg-ref-request-fields @@ -158,10 +159,10 @@ with the message "Operation timed out". If an error is thrown, the framework will submit a "FAILED" response to AWS CloudFormation. -The input event to `isComplete` is similar to -[`onEvent`](#handling-lifecycle-events-onevent), with an additional guarantee -that `PhysicalResourceId` is defines and contains the value returned from -`onEvent` or the described default. At any case, it is guaranteed to exist. +The input event to `isComplete` includes all request fields, combined with all +fields returned from `onEvent`. If `PhysicalResourceId` has not been explicitly +returned from `onEvent`, it's value will be calculated based on the heuristics +described above. The return value must be a JSON object with the following fields: diff --git a/packages/@aws-cdk/custom-resources/lib/provider-framework/types.d.ts b/packages/@aws-cdk/custom-resources/lib/provider-framework/types.d.ts index 058405bf4e928..33a125a971cca 100644 --- a/packages/@aws-cdk/custom-resources/lib/provider-framework/types.d.ts +++ b/packages/@aws-cdk/custom-resources/lib/provider-framework/types.d.ts @@ -75,6 +75,11 @@ interface OnEventResponse { * Resource attributes to return. */ readonly Data?: { [name: string]: any }; + + /** + * Custom fields returned from OnEvent will be passed to IsComplete. + */ + readonly [key: string]: any; } /** diff --git a/packages/@aws-cdk/custom-resources/test/provider-framework/integ.provider.expected.json b/packages/@aws-cdk/custom-resources/test/provider-framework/integ.provider.expected.json index a2307af6ac1ab..9907ab690dd70 100644 --- a/packages/@aws-cdk/custom-resources/test/provider-framework/integ.provider.expected.json +++ b/packages/@aws-cdk/custom-resources/test/provider-framework/integ.provider.expected.json @@ -340,7 +340,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersdb961fc9d087616ad76339bd5135f518cea24001f866a17067a1024235128511S3Bucket776FD46E" + "Ref": "AssetParameters4bafad8d010ba693e235b77d2c6decfc2ac79a8208d4477cbb36d31caf7189e8S3Bucket0DB889DF" }, "S3Key": { "Fn::Join": [ @@ -353,7 +353,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersdb961fc9d087616ad76339bd5135f518cea24001f866a17067a1024235128511S3VersionKeyA70347F9" + "Ref": "AssetParameters4bafad8d010ba693e235b77d2c6decfc2ac79a8208d4477cbb36d31caf7189e8S3VersionKey67FE4034" } ] } @@ -366,7 +366,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersdb961fc9d087616ad76339bd5135f518cea24001f866a17067a1024235128511S3VersionKeyA70347F9" + "Ref": "AssetParameters4bafad8d010ba693e235b77d2c6decfc2ac79a8208d4477cbb36d31caf7189e8S3VersionKey67FE4034" } ] } @@ -450,7 +450,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersdb961fc9d087616ad76339bd5135f518cea24001f866a17067a1024235128511S3Bucket776FD46E" + "Ref": "AssetParameters4bafad8d010ba693e235b77d2c6decfc2ac79a8208d4477cbb36d31caf7189e8S3Bucket0DB889DF" }, "S3Key": { "Fn::Join": [ @@ -463,7 +463,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersdb961fc9d087616ad76339bd5135f518cea24001f866a17067a1024235128511S3VersionKeyA70347F9" + "Ref": "AssetParameters4bafad8d010ba693e235b77d2c6decfc2ac79a8208d4477cbb36d31caf7189e8S3VersionKey67FE4034" } ] } @@ -476,7 +476,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersdb961fc9d087616ad76339bd5135f518cea24001f866a17067a1024235128511S3VersionKeyA70347F9" + "Ref": "AssetParameters4bafad8d010ba693e235b77d2c6decfc2ac79a8208d4477cbb36d31caf7189e8S3VersionKey67FE4034" } ] } @@ -1054,17 +1054,17 @@ "Type": "String", "Description": "Artifact hash for asset \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" }, - "AssetParametersdb961fc9d087616ad76339bd5135f518cea24001f866a17067a1024235128511S3Bucket776FD46E": { + "AssetParameters4bafad8d010ba693e235b77d2c6decfc2ac79a8208d4477cbb36d31caf7189e8S3Bucket0DB889DF": { "Type": "String", - "Description": "S3 bucket for asset \"db961fc9d087616ad76339bd5135f518cea24001f866a17067a1024235128511\"" + "Description": "S3 bucket for asset \"4bafad8d010ba693e235b77d2c6decfc2ac79a8208d4477cbb36d31caf7189e8\"" }, - "AssetParametersdb961fc9d087616ad76339bd5135f518cea24001f866a17067a1024235128511S3VersionKeyA70347F9": { + "AssetParameters4bafad8d010ba693e235b77d2c6decfc2ac79a8208d4477cbb36d31caf7189e8S3VersionKey67FE4034": { "Type": "String", - "Description": "S3 key for asset version \"db961fc9d087616ad76339bd5135f518cea24001f866a17067a1024235128511\"" + "Description": "S3 key for asset version \"4bafad8d010ba693e235b77d2c6decfc2ac79a8208d4477cbb36d31caf7189e8\"" }, - "AssetParametersdb961fc9d087616ad76339bd5135f518cea24001f866a17067a1024235128511ArtifactHashB3EA6E4A": { + "AssetParameters4bafad8d010ba693e235b77d2c6decfc2ac79a8208d4477cbb36d31caf7189e8ArtifactHash6C17CFC2": { "Type": "String", - "Description": "Artifact hash for asset \"db961fc9d087616ad76339bd5135f518cea24001f866a17067a1024235128511\"" + "Description": "Artifact hash for asset \"4bafad8d010ba693e235b77d2c6decfc2ac79a8208d4477cbb36d31caf7189e8\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/custom-resources/test/provider-framework/integration-test-fixtures/s3-assert-handler/index.py b/packages/@aws-cdk/custom-resources/test/provider-framework/integration-test-fixtures/s3-assert-handler/index.py index a5e9321c895ea..0f99bdca49aa9 100644 --- a/packages/@aws-cdk/custom-resources/test/provider-framework/integration-test-fixtures/s3-assert-handler/index.py +++ b/packages/@aws-cdk/custom-resources/test/provider-framework/integration-test-fixtures/s3-assert-handler/index.py @@ -4,10 +4,17 @@ def on_event(event, ctx): print(event) + return { + 'ArbitraryField': 12345 + } def is_complete(event, ctx): print(event) + # verify result from on_event is passed through + if event.get('ArbitraryField', None) != 12345: + raise 'Error: expecting "event" to include "ArbitraryField" with value 12345' + # nothing to assert if this resource is being deleted if event['RequestType'] == 'Delete': return { 'IsComplete': True } diff --git a/packages/@aws-cdk/custom-resources/test/provider-framework/runtime.test.ts b/packages/@aws-cdk/custom-resources/test/provider-framework/runtime.test.ts index 0f1679955ed06..3898246fab6c4 100644 --- a/packages/@aws-cdk/custom-resources/test/provider-framework/runtime.test.ts +++ b/packages/@aws-cdk/custom-resources/test/provider-framework/runtime.test.ts @@ -32,11 +32,13 @@ test('async flow: isComplete returns true only after 3 times', async () => { return { PhysicalResourceId: MOCK_PHYSICAL_ID, Data: MOCK_ATTRS, + ArbitraryField: 1234, }; }; mocks.isCompleteImplMock = async event => { isCompleteCalls++; + expect((event as any).ArbitraryField).toEqual(1234); // any field is passed through expect(event.PhysicalResourceId).toEqual(MOCK_PHYSICAL_ID); // physical ID returned from onEvent is passed to "isComplete" expect(event.Data).toStrictEqual(MOCK_ATTRS); // attributes are propagated between the calls