From 3af87e7297e28dcb85346cea880c730633b8c059 Mon Sep 17 00:00:00 2001 From: Momo Kornher Date: Wed, 13 Jul 2022 13:34:57 +0100 Subject: [PATCH] fix(custom-resources): Custom resource provider framework not passing `ResponseURL` to user function (aws#21065) aws#20889 included a change that broke the custom resource framework by not including the necessary presigned URL. We attempted to fix this with aws#21065 but it didn't resolve the issue and aws#21109 reverted the attempted fix. This changes explicitly includes the presigned URL, as well as adding tests to ensure the URL is passed to the downstream Lambda Function. Closes aws#21058 --- .../provider-framework/runtime/framework.ts | 10 ++--- .../test/provider-framework/runtime.test.ts | 38 +++++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/custom-resources/lib/provider-framework/runtime/framework.ts b/packages/@aws-cdk/custom-resources/lib/provider-framework/runtime/framework.ts index 97b7c4de61e4b..20c0144311b94 100644 --- a/packages/@aws-cdk/custom-resources/lib/provider-framework/runtime/framework.ts +++ b/packages/@aws-cdk/custom-resources/lib/provider-framework/runtime/framework.ts @@ -29,7 +29,7 @@ async function onEvent(cfnRequest: AWSLambda.CloudFormationCustomResourceEvent) cfnRequest.ResourceProperties = cfnRequest.ResourceProperties || { }; - const onEventResult = await invokeUserFunction(consts.USER_ON_EVENT_FUNCTION_ARN_ENV, sanitizedRequest) as OnEventResponse; + const onEventResult = await invokeUserFunction(consts.USER_ON_EVENT_FUNCTION_ARN_ENV, sanitizedRequest, cfnRequest.ResponseURL) as OnEventResponse; log('onEvent returned:', onEventResult); // merge the request and the result from onEvent to form the complete resource event @@ -61,7 +61,7 @@ async function isComplete(event: AWSCDKAsyncCustomResource.IsCompleteRequest) { const sanitizedRequest = { ...event, ResponseURL: '...' } as const; log('isComplete', sanitizedRequest); - const isCompleteResult = await invokeUserFunction(consts.USER_IS_COMPLETE_FUNCTION_ARN_ENV, sanitizedRequest) as IsCompleteResponse; + const isCompleteResult = await invokeUserFunction(consts.USER_IS_COMPLETE_FUNCTION_ARN_ENV, sanitizedRequest, event.ResponseURL) as IsCompleteResponse; log('user isComplete returned:', isCompleteResult); // if we are not complete, return false, and don't send a response back. @@ -96,7 +96,7 @@ async function onTimeout(timeoutEvent: any) { }); } -async function invokeUserFunction(functionArnEnv: string, sanitizedPayload: A) { +async function invokeUserFunction(functionArnEnv: string, sanitizedPayload: A, responseUrl: string) { const functionArn = getEnv(functionArnEnv); log(`executing user function ${functionArn} with payload`, sanitizedPayload); @@ -106,8 +106,8 @@ async function invokeUserFunction(functionArnE const resp = await invokeFunction({ FunctionName: functionArn, - // Strip 'ResponseURL' -- the downstream CR doesn't need it and can only log it by accident - Payload: JSON.stringify({ ...sanitizedPayload, ResponseURL: undefined }), + // Cannot strip 'ResponseURL' here as this would be a breaking change even though the downstream CR doesn't need it + Payload: JSON.stringify({ ...sanitizedPayload, ResponseURL: responseUrl }), }); log('user function response:', resp, typeof(resp)); 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 751e33e6408e0..2d35a9e63eac9 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 @@ -18,7 +18,10 @@ outbound.httpRequest = mocks.httpRequestMock; outbound.invokeFunction = mocks.invokeFunctionMock; outbound.startExecution = mocks.startExecutionMock; +const invokeFunctionSpy = jest.spyOn(outbound, 'invokeFunction'); + beforeEach(() => mocks.setup()); +afterEach(() => invokeFunctionSpy.mockClear()); test('async flow: isComplete returns true only after 3 times', async () => { let isCompleteCalls = 0; @@ -346,6 +349,41 @@ describe('if CREATE fails, the subsequent DELETE will be ignored', () => { }); +describe('ResponseURL is passed to user function', () => { + test('for onEvent', async () => { + // GIVEN + mocks.onEventImplMock = async () => ({ PhysicalResourceId: MOCK_PHYSICAL_ID }); + + // WHEN + await simulateEvent({ + RequestType: 'Create', + }); + + // THEN + expect(invokeFunctionSpy).toHaveBeenCalledTimes(1); + expect(invokeFunctionSpy).toBeCalledWith(expect.objectContaining({ + Payload: expect.stringContaining(`"ResponseURL":"${mocks.MOCK_REQUEST.ResponseURL}"`), + })); + }); + + test('for isComplete', async () => { + // GIVEN + mocks.onEventImplMock = async () => ({ PhysicalResourceId: MOCK_PHYSICAL_ID }); + mocks.isCompleteImplMock = async () => ({ IsComplete: true }); + + // WHEN + await simulateEvent({ + RequestType: 'Create', + }); + + // THEN + expect(invokeFunctionSpy).toHaveBeenCalledTimes(2); + expect(invokeFunctionSpy).toHaveBeenLastCalledWith(expect.objectContaining({ + Payload: expect.stringContaining(`"ResponseURL":"${mocks.MOCK_REQUEST.ResponseURL}"`), + })); + }); +}); + // ----------------------------------------------------------------------------------------------------------------------- /**