From 621a410471fcda0e388a7a53bb0e3cdb77be759c Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 3 Jan 2022 14:42:24 +0100 Subject: [PATCH 1/4] feat(custom-resources): NoEcho for sensitive data in provider framework (#18097) The `noEcho` option was available in `submitResponse()` but not exposed. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/custom-resources/README.md | 1 + .../provider-framework/runtime/framework.ts | 6 +- .../lib/provider-framework/types.d.ts | 9 +++ .../test/provider-framework/runtime.test.ts | 55 +++++++++++++++++++ 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/custom-resources/README.md b/packages/@aws-cdk/custom-resources/README.md index 2c15ba9141e17..890255ee7bb12 100644 --- a/packages/@aws-cdk/custom-resources/README.md +++ b/packages/@aws-cdk/custom-resources/README.md @@ -143,6 +143,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. +|`NoEcho`|Boolean|No|Whether to mask the output of the custom resource when retrieved by using the `Fn::GetAtt` function. |*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 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 eda70ea7efb4f..181968611d354 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 @@ -39,7 +39,7 @@ async function onEvent(cfnRequest: AWSLambda.CloudFormationCustomResourceEvent) // determine if this is an async provider based on whether we have an isComplete handler defined. // if it is not defined, then we are basically ready to return a positive response. if (!process.env[consts.USER_IS_COMPLETE_FUNCTION_ARN_ENV]) { - return cfnResponse.submitResponse('SUCCESS', resourceEvent); + return cfnResponse.submitResponse('SUCCESS', resourceEvent, { noEcho: resourceEvent.NoEcho }); } // ok, we are not complete, so kick off the waiter workflow @@ -62,7 +62,7 @@ async function isComplete(event: AWSCDKAsyncCustomResource.IsCompleteRequest) { const isCompleteResult = await invokeUserFunction(consts.USER_IS_COMPLETE_FUNCTION_ARN_ENV, event) as IsCompleteResponse; log('user isComplete returned:', isCompleteResult); - // if we are not complete, reeturn false, and don't send a response back. + // if we are not complete, return false, and don't send a response back. if (!isCompleteResult.IsComplete) { if (isCompleteResult.Data && Object.keys(isCompleteResult.Data).length > 0) { throw new Error('"Data" is not allowed if "IsComplete" is "False"'); @@ -79,7 +79,7 @@ async function isComplete(event: AWSCDKAsyncCustomResource.IsCompleteRequest) { }, }; - await cfnResponse.submitResponse('SUCCESS', response); + await cfnResponse.submitResponse('SUCCESS', response, { noEcho: event.NoEcho }); } // invoked when completion retries are exhaused. 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 33a125a971cca..9a9536eac078b 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 @@ -80,6 +80,15 @@ interface OnEventResponse { * Custom fields returned from OnEvent will be passed to IsComplete. */ readonly [key: string]: any; + + /** + * Whether to mask the output of the custom resource when retrieved + * by using the `Fn::GetAtt` function. If set to `true`, all returned + * values are masked with asterisks (*****). + * + * @default false + */ + readonly NoEcho?: boolean; } /** 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 45cc13a460959..d2af0a4fafd2d 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 @@ -230,6 +230,61 @@ test('if there is no user-defined "isComplete", the waiter will not be triggered expectCloudFormationSuccess({ PhysicalResourceId: MOCK_PHYSICAL_ID }); }); +describe('NoEcho', () => { + test('with onEvent', async () => { + // GIVEN + mocks.onEventImplMock = async () => ({ + Data: { + Very: 'Sensitive', + }, + NoEcho: true, + }); + + // WHEN + await simulateEvent({ + RequestType: 'Create', + }); + + // THEN + expectCloudFormationSuccess({ + Data: { + Very: 'Sensitive', + }, + NoEcho: true, + }); + }); + + test('with isComplete', async () => { + // GIVEN + mocks.onEventImplMock = async () => ({ + Data: { + Very: 'Sensitive', + }, + NoEcho: true, + }); + mocks.isCompleteImplMock = async () => ({ + Data: { + Also: 'Confidential', + }, + IsComplete: true, + }); + + // WHEN + await simulateEvent({ + RequestType: 'Create', + }); + + // THEN + expectCloudFormationSuccess({ + Data: { + Very: 'Sensitive', + Also: 'Confidential', + }, + NoEcho: true, + }); + }); +}); + test('fails if user handler returns a non-object response', async () => { // GIVEN mocks.stringifyPayload = false; From 2868f1d97e2b7bb89029a93be600d687010c057e Mon Sep 17 00:00:00 2001 From: Shaangor Date: Mon, 3 Jan 2022 09:27:24 -0500 Subject: [PATCH 2/4] chore(core): Pass Lambda Context to custom resource handler (#18056) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … passing context object into user handler. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* Fixes #18055 --- .../core/lib/custom-resource-provider/nodejs-entrypoint.ts | 6 +++--- .../test/custom-resource-provider/nodejs-entrypoint.test.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/core/lib/custom-resource-provider/nodejs-entrypoint.ts b/packages/@aws-cdk/core/lib/custom-resource-provider/nodejs-entrypoint.ts index e720e225787eb..a7c2ffef53547 100644 --- a/packages/@aws-cdk/core/lib/custom-resource-provider/nodejs-entrypoint.ts +++ b/packages/@aws-cdk/core/lib/custom-resource-provider/nodejs-entrypoint.ts @@ -13,7 +13,7 @@ const CREATE_FAILED_PHYSICAL_ID_MARKER = 'AWSCDK::CustomResourceProviderFramewor const MISSING_PHYSICAL_ID_MARKER = 'AWSCDK::CustomResourceProviderFramework::MISSING_PHYSICAL_ID'; export type Response = AWSLambda.CloudFormationCustomResourceEvent & HandlerResponse; -export type Handler = (event: AWSLambda.CloudFormationCustomResourceEvent) => Promise; +export type Handler = (event: AWSLambda.CloudFormationCustomResourceEvent, context: AWSLambda.Context) => Promise; export type HandlerResponse = undefined | { Data?: any; PhysicalResourceId?: string; @@ -21,7 +21,7 @@ export type HandlerResponse = undefined | { NoEcho?: boolean; }; -export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) { +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent, context: AWSLambda.Context) { external.log(JSON.stringify(event, undefined, 2)); // ignore DELETE event when the physical resource ID is the marker that @@ -39,7 +39,7 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent // cloudformation (otherwise cfn waits). // eslint-disable-next-line @typescript-eslint/no-require-imports const userHandler: Handler = require(external.userHandlerIndex).handler; - const result = await userHandler(event); + const result = await userHandler(event, context); // validate user response and create the combined event const responseEvent = renderResponse(event, result); diff --git a/packages/@aws-cdk/core/test/custom-resource-provider/nodejs-entrypoint.test.ts b/packages/@aws-cdk/core/test/custom-resource-provider/nodejs-entrypoint.test.ts index 888c38da918cb..e3ce9450c7276 100644 --- a/packages/@aws-cdk/core/test/custom-resource-provider/nodejs-entrypoint.test.ts +++ b/packages/@aws-cdk/core/test/custom-resource-provider/nodejs-entrypoint.test.ts @@ -186,7 +186,7 @@ async function invokeHandler(req: AWSLambda.CloudFormationCustomResourceEvent, u actualResponse = responseBody; }; - await entrypoint.handler(req); + await entrypoint.handler(req, {} as AWSLambda.Context); if (!actualResponse) { throw new Error('no response sent to cloudformation'); } From df03df8b5c97fae6c349822ae97245512571a1dc Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 3 Jan 2022 16:12:39 +0100 Subject: [PATCH 3/4] fix(core): `Duration.toString()` throws an error (#18243) `Duration.toString()` was intended to produce a value that would throw an exception when resolved, but unintentionally was written to always throw immediately (the reason it was throwing is that `Token.asString()` doesn't accept functions, it only accepts data values--`Lazy.string()` should have been used). Instead, we remove the validation completely. `toString()` now produces a meaningless string, and users should avoid using the `Duration` object in a context where it will be implicitly converted to a string. Fixes #18176. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/core/lib/duration.ts | 14 +++++--------- packages/@aws-cdk/core/test/duration.test.ts | 4 +++- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/core/lib/duration.ts b/packages/@aws-cdk/core/lib/duration.ts index 5c7bb804b917c..ebb5e60a19a67 100644 --- a/packages/@aws-cdk/core/lib/duration.ts +++ b/packages/@aws-cdk/core/lib/duration.ts @@ -220,17 +220,13 @@ export class Duration { } /** - * Returns a string representation of this `Duration` that is also a Token that cannot be successfully resolved. This - * protects users against inadvertently stringifying a `Duration` object, when they should have called one of the - * `to*` methods instead. + * Returns a string representation of this `Duration` + * + * This is is never the right function to use when you want to use the `Duration` + * object in a template. Use `toSeconds()`, `toMinutes()`, `toDays()`, etc. instead. */ public toString(): string { - return Token.asString( - () => { - throw new Error('Duration.toString() was used, but .toSeconds, .toMinutes or .toDays should have been called instead'); - }, - { displayHint: `${this.amount} ${this.unit.label}` }, - ); + return `Duration.${this.unit.label}(${this.amount})`; } /** diff --git a/packages/@aws-cdk/core/test/duration.test.ts b/packages/@aws-cdk/core/test/duration.test.ts index c22de1e808ee9..99d54d15f0905 100644 --- a/packages/@aws-cdk/core/test/duration.test.ts +++ b/packages/@aws-cdk/core/test/duration.test.ts @@ -4,8 +4,10 @@ import { Duration, Lazy, Stack, Token } from '../lib'; describe('duration', () => { test('negative amount', () => { expect(() => Duration.seconds(-1)).toThrow(/negative/); + }); - + test('can stringify', () => { + expect(`${Duration.hours(1)}`).toEqual('Duration.hours(1)'); }); test('unresolved amount', () => { From 9b6e237be8635ca037b3cc92ed3b3796b981d283 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 3 Jan 2022 16:59:58 +0100 Subject: [PATCH 4/4] docs(pipelines): add philosophy disclaimer (#18244) People keep on wanting to do their CodeDeploy deployments (to either ASGs or ECS clusters) in CDK Pipelines directly. While this is *possible* using custom steps, it's not how the library is intended to be used. Explain that up top in the README. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/pipelines/README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index ac345c75f3776..e350d4a0862f5 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -11,6 +11,18 @@ A construct library for painless Continuous Delivery of CDK applications. +CDK Pipelines is an *opinionated construct library*. It is purpose-built to +deploy one or more copies of your CDK applications using CloudFormation with a +minimal amount of effort on your part. It is *not* intended to support arbitrary +deployment pipelines, and very specifically it is not built to use CodeDeploy to +applications to instances, or deploy your custom-built ECR images to an ECS +cluster directly: use CDK file assets with CloudFormation Init for instances, or +CDK container assets for ECS clusters instead. + +Give the CDK Pipelines way of doing things a shot first: you might find it does +everything you need. If you want or need more control, we recommend you drop +down to using the `aws-codepipeline` construct library directly. + > This module contains two sets of APIs: an **original** and a **modern** version of CDK Pipelines. The *modern* API has been updated to be easier to work with and customize, and will be the preferred API going forward. The *original* version @@ -728,7 +740,7 @@ Here's an example that adds a Jenkins step: ```ts class MyJenkinsStep extends pipelines.Step implements pipelines.ICodePipelineActionFactory { constructor( - private readonly provider: cpactions.JenkinsProvider, + private readonly provider: cpactions.JenkinsProvider, private readonly input: pipelines.FileSet, ) { super('MyJenkinsStep'); @@ -1392,7 +1404,7 @@ is not able to read the cloud assembly produced by the new framework version. Solution: change the `cliVersion` first, commit, push and deploy, and only then change the framework version. - + We recommend you avoid specifying the `cliVersion` parameter at all. By default the pipeline will use the latest CLI version, which will support all cloud assembly versions.