From 1a9c30e55e58203bd0a61de82711cf10f1e04851 Mon Sep 17 00:00:00 2001 From: Colin Francis <131073567+colifran@users.noreply.github.com> Date: Fri, 15 Dec 2023 16:05:25 -0800 Subject: [PATCH] feat: implement code generated handler framework (#28251) This PR introduces an internal handler framework used to code generate constructs that extend a lambda `Function`, lambda `SingletonFunction`, or core `CustomResourceProvider` construct and prohibit the user from directly configuring the `handler`, `runtime`, `code`, and `codeDirectory` properties. In doing this, we are able to establish best practices, runtime enforcement, and consistency across all handlers we build and vend within the aws-cdk. As expected, no integ tests were changed as a result of this PR. To verify that the code generated custom resource providers are working correctly I force ran three integ tests all targeted at an individual custom resource provider: 1. integ.global.ts to test replica provider and the code generated construct extending `Function` 2. integ.bucket-auto-delete-objects.ts to test auto delete objects provider and the code generated construct extending `CustomResourceProvider` 3. integ.aws-api.ts to test aws api provider and the code generated construct `SingletonFunction` All of these integ tests passed successfully. Closes #27303 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/custom-resources-framework/README.md | 83 ++++ .../lib/custom-resources-framework/classes.ts | 356 +++++++++++++++++ .../lib/custom-resources-framework/config.ts | 357 ++++++++++++++++++ .../custom-resources-framework/framework.ts | 106 ++++++ .../lib/custom-resources-framework/modules.ts | 60 +++ .../utils/framework-utils.ts | 26 ++ .../custom-resource-handlers/package.json | 3 +- .../scripts/generate.ts | 133 +++++++ .../scripts/minify-and-bundle-sources.ts | 115 ------ .../expected/custom-resource-provider-core.ts | 32 ++ .../expected/custom-resource-provider.ts | 31 ++ .../expected/function.ts | 15 + .../expected/singleton-function.ts | 39 ++ .../framework.test.ts | 93 +++++ .../my-handler/index.ts | 0 .../utils/framework-utils.test.ts | 66 ++++ .../custom-resource-handlers/tsconfig.json | 2 + .../lib/dns-validated-certificate.ts | 8 +- .../lib/experimental/edge-function.ts | 8 +- .../aws-dynamodb/lib/replica-provider.ts | 14 +- packages/aws-cdk-lib/aws-ec2/lib/vpc.ts | 8 +- .../aws-cdk-lib/aws-ecr/lib/repository.ts | 8 +- .../aws-eks/lib/cluster-resource-provider.ts | 14 +- .../aws-eks/lib/kubectl-provider.ts | 8 +- .../aws-events-targets/lib/aws-api.ts | 7 +- .../aws-cdk-lib/aws-iam/lib/oidc-provider.ts | 8 +- .../aws-cdk-lib/aws-route53/lib/record-set.ts | 15 +- .../lib/bucket-deployment.ts | 7 +- packages/aws-cdk-lib/aws-s3/lib/bucket.ts | 8 +- .../aws-cdk-lib/aws-ses/lib/receipt-rule.ts | 8 +- .../lib/emrcontainers/start-job-run.ts | 8 +- .../lib/evaluate-expression.ts | 9 +- .../aws-cdk-lib/aws-synthetics/lib/canary.ts | 6 +- .../export-reader-provider.ts | 7 +- .../export-writer-provider.ts | 16 +- .../custom-resource-provider-base.ts | 281 ++++++++++++++ .../custom-resource-provider.ts | 339 +---------------- .../lib/custom-resource-provider/index.ts | 4 +- .../lib/custom-resource-provider/shared.ts | 72 ++++ .../core/lib/private/cfn-utils-provider.ts | 8 +- .../aws-custom-resource.ts | 8 +- .../lib/private/application-security-check.ts | 7 +- .../airlift-custom-resource-handlers.sh | 62 +-- packages/aws-cdk-lib/triggers/lib/trigger.ts | 9 +- 44 files changed, 1855 insertions(+), 619 deletions(-) create mode 100644 packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/README.md create mode 100644 packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/classes.ts create mode 100644 packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/config.ts create mode 100644 packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/framework.ts create mode 100644 packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/modules.ts create mode 100644 packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/utils/framework-utils.ts create mode 100644 packages/@aws-cdk/custom-resource-handlers/scripts/generate.ts delete mode 100644 packages/@aws-cdk/custom-resource-handlers/scripts/minify-and-bundle-sources.ts create mode 100644 packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/custom-resource-provider-core.ts create mode 100644 packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/custom-resource-provider.ts create mode 100644 packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/function.ts create mode 100644 packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/singleton-function.ts create mode 100644 packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/framework.test.ts create mode 100644 packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/my-handler/index.ts create mode 100644 packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/utils/framework-utils.test.ts create mode 100644 packages/aws-cdk-lib/core/lib/custom-resource-provider/custom-resource-provider-base.ts create mode 100644 packages/aws-cdk-lib/core/lib/custom-resource-provider/shared.ts diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/README.md b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/README.md new file mode 100644 index 0000000000000..f18afce9a4fb0 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/README.md @@ -0,0 +1,83 @@ +# CDK Handler Framework + +The CDK handler framework is an internal framework used to code generate constructs that extend a lambda `Function`, lambda `SingletonFunction`, or core `CustomResourceProvider` construct and prohibit the user from directly configuring the `handler`, `runtime`, `code`, and `codeDirectory` properties. In doing this, we are able to establish best practices, runtime enforcement, and consistency across all handlers we build and vend within the aws-cdk. + +## CDK Handler Framework Concepts + +This framework allows for the creation of three component types: +1. `ComponentType.FUNCTION` - This is a wrapper around the lambda `Function` construct. It offers the same behavior and performance as a lambda `Function`, but it restricts the consumer from configuring the `handler`, `runtime`, and `code` properties. +2. `ComponentType.SINGLETON_FUNCTION` - This is a wrapper around the lambda `SingletonFunction` construct. It offers the same behavior and performance as a lambda `SingletonFunction`, but it restricts the consumer from configuring the `handler`, `runtime`, and `code` properties. +3. `ComponentType.CUSTOM_RESOURCE_PROVIDER` - This is a wrapper around the core `CustomResourceProvider` construct. It offers the same behavior and performance as a `CustomResourceProvider` and can be instantiated via the `getOrCreate` or `getOrCreateProvider` methods. This component restricts the consumer from configuring the `runtime` and `codeDirectory` properties. + +Code generating one of these three component types requires adding the component properties to the [config](./config.ts) file by providing `ComponentProps`. The `ComponentProps` are responsible for code generating the specified `ComponentType` with the `handler`, `runtime`, `code`, and `codeDirectory` properties set internally. `ComponentProps` includes the following properties: +- `type` - the framework component type to generate. +- `sourceCode` - the source code that will be excuted by the framework component. +- `runtime` - the runtime that is compatible with the framework component's source code. This is an optional property with a default node runtime maintained by the framework. +- `handler` - the name of the method with the source code that the framework component will call. This is an optional property and the default is `index.handler`. +- `minifyAndBundle` - whether the source code should be minified and bundled. This an optional property and the default is `true`. This should only be set to `false` for python files or for typescript/javascript files with a require import. + +The [config](./config.ts) file is structured with the top level mapping to an aws-cdk module, i.e., aws-s3, aws-dynamodb, etc. Each service can contain one or more component modules. Component modules are containers for handler framework components and will be rendered as a code generated file. Each component module can contain one or more `ComponentProps` objects. The following example shows a more structural breakdown of how the [config](./config.ts) file is configured: + +```ts +const config = { + 'aws-s3': { // the aws-cdk-lib module + 'replica-provider': [ // the component module + // handler framework component defined as a `ComponentProps` object + { + // the handler framework component type + type: ComponentType.FUNCTION, + // the source code that the component will use + sourceCode: path.resolve(__dirname, '..', 'aws-dynamodb', 'replica-handler', 'index.ts'), + // the handler in the source code that the component will execute + handler: 'index.onEventHandler', + }, + ], + }, + 'aws-stepfunctions-tasks': { + // contains multiple component modules + 'eval-nodejs-provider': [ + { + type: ComponentType.SINGLETON_FUNCTION, + sourceCode: path.resolve(__dirname, '..', 'aws-stepfunctions-tasks', 'eval-nodejs-handler', 'index.ts'), + }, + ], + 'role-policy-provider': [ + { + type: ComponentType.SINGLETON_FUNCTION, + sourceCode: path.resolve(__dirname, '..', 'aws-stepfunctions-tasks', 'role-policy-handler', 'index.py'), + runtime: Runtime.PYTHON_3_9, + // prevent minify and bundle since the source code is a python file + minifyAndBundle: false, + }, + ], + }, +}; +``` + +Code generation for the component modules is triggered when this package - `@aws-cdk/custom-resource-handlers` - is built. Importantly, this framework is also responsible for minifying and bundling the custom resource providers' source code and dependencies. A flag named `minifyAndBundle` can be configured as part of the `ComponentProps` to prevent minifying and bundling the source code for a specific provider. This flag is only needed for python files or for typescript/javascript files containing require imports. + +Once built, all generated code and bundled source code will be written to `@aws-cdk/custom-resource-handlers/dist`. The top level field in the [config](./config.ts) file defining individual aws-cdk modules will be used to create specific directories within `@aws-cdk/custom-resource-handlers/dist` and each component module will be a separate code generated file within these directories named `.generated.ts`. As an example, the sample [config](./config.ts) file above would create the following file structure: + +|--- @aws-cdk +| |--- custom-resource-handlers +| | |--- dist +| | | |--- aws-s3 +| | | | |--- replica-handler +| | | | | |--- index.js +| | | | |--- replica-provider.generated.ts +| | | |--- aws-stepfunctions-tasks +| | | | |--- eval-nodejs-handler +| | | | | |--- index.js +| | | | |--- role-policy-handler +| | | | | |--- index.py +| | | | |--- eval-nodejs-provider.generated.ts +| | | | |--- role-policy-provider.generated.ts + +The code generated handler framework components are consumable from `aws-cdk-lib/custom-resource-handlers/dist` once `aws-cdk-lib` is built. The file structure of `aws-cdk-lib/custom-resource-handlers/dist` will have the same structure as `@aws-cdk/custom-resource-handlers/dist` with the exception of `core`. To prevent circular dependencies, all handler framework components defined in `core`and any associated source code will be consumable from `aws-cdk-lib/core/dist/core`. + +## Creating a Handler Framework Component + +Creating a new handler framework component involves three steps: +1. Add the source code to `@aws-cdk/custom-resource-handlers/lib/` +2. Update the [config](./config.ts) file by specifying all required `ComponentProps`. +3. At this point you can directly build `@aws-cdk/custom-resource-handlers` with `yarn build` to view the generated component in `@aws-cdk/custom-resource-handlers/dist`. Alternatively, you can build `aws-cdk-lib` with `npx lerna run build --scope=aws-cdk-lib --skip-nx-cache` to make the generated component available for use within `aws-cdk-lib` diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/classes.ts b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/classes.ts new file mode 100644 index 0000000000000..ed9e24231a699 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/classes.ts @@ -0,0 +1,356 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { + ClassType, + stmt, + expr, + Type, + Splat, + ExternalModule, + PropertySpec, + InterfaceSpec, + InterfaceType, + ObjectLiteral, + MemberVisibility, + SuperInitializer, + Expression, +} from '@cdklabs/typewriter'; +import { Runtime } from './config'; +import { HandlerFrameworkModule } from './framework'; +import { + CONSTRUCTS_MODULE, + LAMBDA_MODULE, + CORE_MODULE, + CORE_INTERNAL_STACK, + CORE_INTERNAL_CR_PROVIDER, + PATH_MODULE, +} from './modules'; +import { toLambdaRuntime } from './utils/framework-utils'; + +/** + * Initialization properties for a class constructor. + */ +interface ConstructorBuildProps { + /** + * The props type used to create an instance of this class. + */ + readonly constructorPropsType: Type; + + /** + * Properties to pass up to the parent class. + */ + readonly superProps: ObjectLiteral; + + /** + * Whether the class constructor props are optional. + * + * @default false + */ + readonly optionalConstructorProps?: boolean; + + /** + * Visbility for the constructor. + * + * @default MemberVisbility.Public + */ + readonly constructorVisbility?: MemberVisibility; +} + +/** + * Initialization properties used to build a `HandlerFrameworkClass` instance. + */ +export interface HandlerFrameworkClassProps { + /** + * The name of the framework component class. + */ + readonly name: string; + + /** + * A local file system directory with the framework component's code. + */ + readonly codeDirectory: string; + + /** + * The runtime environment for the framework component. + */ + readonly runtime: Runtime; + + /** + * The name of the method within your code that framework component calls. + */ + readonly handler: string; +} + +export abstract class HandlerFrameworkClass extends ClassType { + /** + * Builds a code generated Lambda function class. + */ + public static buildFunction(scope: HandlerFrameworkModule, props: HandlerFrameworkClassProps): HandlerFrameworkClass { + return new (class Function extends HandlerFrameworkClass { + protected readonly externalModules = [PATH_MODULE, CONSTRUCTS_MODULE, LAMBDA_MODULE]; + + public constructor() { + super(scope, { + name: props.name, + extends: LAMBDA_MODULE.Function, + export: true, + }); + + this.importExternalModulesInto(scope); + + const superProps = new ObjectLiteral([ + new Splat(expr.ident('props')), + ['code', expr.directCode(`lambda.Code.fromAsset(path.join(__dirname, '${props.codeDirectory}'))`)], + ['handler', expr.lit(props.handler)], + ['runtime', expr.directCode(toLambdaRuntime(props.runtime))], + ]); + this.buildConstructor({ + constructorPropsType: LAMBDA_MODULE.FunctionOptions, + superProps, + optionalConstructorProps: true, + constructorVisbility: MemberVisibility.Public, + }); + } + })(); + } + + /** + * Builds a code generated Lambda singleton function class. + */ + public static buildSingletonFunction(scope: HandlerFrameworkModule, props: HandlerFrameworkClassProps): HandlerFrameworkClass { + return new (class SingletonFunction extends HandlerFrameworkClass { + protected readonly externalModules = [PATH_MODULE, CONSTRUCTS_MODULE, LAMBDA_MODULE]; + + public constructor() { + super(scope, { + name: props.name, + extends: LAMBDA_MODULE.SingletonFunction, + export: true, + }); + + this.importExternalModulesInto(scope); + + const uuid: PropertySpec = { + name: 'uuid', + type: Type.STRING, + immutable: true, + docs: { + summary: 'A unique identifier to identify this Lambda.\n\nThe identifier should be unique across all custom resource providers.\nWe recommend generating a UUID per provider.', + }, + }; + const lambdaPurpose: PropertySpec = { + name: 'lambdaPurpose', + type: Type.STRING, + immutable: true, + optional: true, + docs: { + summary: 'A descriptive name for the purpose of this Lambda.\n\nIf the Lambda does not have a physical name, this string will be\nreflected in its generated name. The combination of lambdaPurpose\nand uuid must be unique.', + docTags: { + default: 'SingletonLambda', + }, + }, + }; + const _interface = this.getOrCreateInterface(scope, { + name: `${this.name}Props`, + export: true, + extends: [LAMBDA_MODULE.FunctionOptions], + properties: [uuid, lambdaPurpose], + docs: { + summary: `Initialization properties for ${this.name}`, + }, + }); + + const superProps = new ObjectLiteral([ + new Splat(expr.ident('props')), + ['code', expr.directCode(`lambda.Code.fromAsset(path.join(__dirname, '${props.codeDirectory}'))`)], + ['handler', expr.lit(props.handler)], + ['runtime', expr.directCode(toLambdaRuntime(props.runtime))], + ]); + this.buildConstructor({ + constructorPropsType: _interface.type, + superProps, + constructorVisbility: MemberVisibility.Public, + }); + } + })(); + } + + /** + * Builds a code generated custom resource provider class. + */ + public static buildCustomResourceProvider(scope: HandlerFrameworkModule, props: HandlerFrameworkClassProps): HandlerFrameworkClass { + return new (class CustomResourceProvider extends HandlerFrameworkClass { + protected readonly externalModules: ExternalModule[] = [PATH_MODULE, CONSTRUCTS_MODULE]; + + public constructor() { + super(scope, { + name: props.name, + extends: scope.coreInternal + ? CORE_INTERNAL_CR_PROVIDER.CustomResourceProviderBase + : CORE_MODULE.CustomResourceProviderBase, + export: true, + }); + + if (scope.coreInternal) { + this.externalModules.push(...[CORE_INTERNAL_STACK, CORE_INTERNAL_CR_PROVIDER]); + } else { + this.externalModules.push(CORE_MODULE); + } + this.importExternalModulesInto(scope); + + const getOrCreateMethod = this.addMethod({ + name: 'getOrCreate', + static: true, + returnType: Type.STRING, + docs: { + summary: 'Returns a stack-level singleton ARN (service token) for the custom resource provider.', + }, + }); + getOrCreateMethod.addParameter({ + name: 'scope', + type: CONSTRUCTS_MODULE.Construct, + }); + getOrCreateMethod.addParameter({ + name: 'uniqueid', + type: Type.STRING, + }); + getOrCreateMethod.addParameter({ + name: 'props', + type: scope.coreInternal + ? CORE_INTERNAL_CR_PROVIDER.CustomResourceProviderOptions + : CORE_MODULE.CustomResourceProviderOptions, + optional: true, + }); + getOrCreateMethod.addBody( + stmt.ret(expr.directCode('this.getOrCreateProvider(scope, uniqueid, props).serviceToken')), + ); + + const getOrCreateProviderMethod = this.addMethod({ + name: 'getOrCreateProvider', + static: true, + returnType: this.type, + docs: { + summary: 'Returns a stack-level singleton for the custom resource provider.', + }, + }); + getOrCreateProviderMethod.addParameter({ + name: 'scope', + type: CONSTRUCTS_MODULE.Construct, + }); + getOrCreateProviderMethod.addParameter({ + name: 'uniqueid', + type: Type.STRING, + }); + getOrCreateProviderMethod.addParameter({ + name: 'props', + type: scope.coreInternal + ? CORE_INTERNAL_CR_PROVIDER.CustomResourceProviderOptions + : CORE_MODULE.CustomResourceProviderOptions, + optional: true, + }); + getOrCreateProviderMethod.addBody( + stmt.constVar(expr.ident('id'), expr.directCode('`${uniqueid}CustomResourceProvider`')), + stmt.constVar(expr.ident('stack'), expr.directCode('Stack.of(scope)')), + stmt.constVar(expr.ident('existing'), expr.directCode(`stack.node.tryFindChild(id) as ${this.type}`)), + stmt.ret(expr.directCode(`existing ?? new ${this.name}(stack, id, props)`)), + ); + + const superProps = new ObjectLiteral([ + new Splat(expr.ident('props')), + ['codeDirectory', expr.directCode(`path.join(__dirname, '${props.codeDirectory}')`)], + ['runtimeName', expr.lit(props.runtime)], + ]); + this.buildConstructor({ + constructorPropsType: scope.coreInternal + ? CORE_INTERNAL_CR_PROVIDER.CustomResourceProviderOptions + : CORE_MODULE.CustomResourceProviderOptions, + superProps, + constructorVisbility: MemberVisibility.Private, + optionalConstructorProps: true, + }); + } + })(); + } + + /** + * External modules that this class depends on. + */ + protected abstract readonly externalModules: ExternalModule[]; + + private importExternalModulesInto(scope: HandlerFrameworkModule) { + for (const module of this.externalModules) { + if (!scope.hasExternalModule(module)) { + scope.addExternalModule(module); + this.importExternalModuleInto(scope, module); + } + } + } + + private importExternalModuleInto(scope: HandlerFrameworkModule, module: ExternalModule) { + switch (module.fqn) { + case PATH_MODULE.fqn: { + PATH_MODULE.import(scope, 'path'); + return; + } + case CONSTRUCTS_MODULE.fqn: { + CONSTRUCTS_MODULE.importSelective(scope, ['Construct']); + return; + } + case CORE_MODULE.fqn: { + CORE_MODULE.importSelective(scope, [ + 'Stack', + 'CustomResourceProviderBase', + 'CustomResourceProviderOptions', + ]); + return; + } + case CORE_INTERNAL_CR_PROVIDER.fqn: { + CORE_INTERNAL_CR_PROVIDER.importSelective(scope, [ + 'CustomResourceProviderBase', + 'CustomResourceProviderOptions', + ]); + return; + } + case CORE_INTERNAL_STACK.fqn: { + CORE_INTERNAL_STACK.importSelective(scope, ['Stack']); + return; + } + case LAMBDA_MODULE.fqn: { + LAMBDA_MODULE.import(scope, 'lambda'); + return; + } + } + } + + private getOrCreateInterface(scope: HandlerFrameworkModule, spec: InterfaceSpec) { + const existing = scope.getInterface(spec.name); + if (existing) { + return existing; + } + + const _interface = new InterfaceType(scope, { ...spec }); + scope.registerInterface(_interface); + return _interface; + } + + private buildConstructor(props: ConstructorBuildProps) { + const init = this.addInitializer({ + visibility: props.constructorVisbility, + }); + const scope = init.addParameter({ + name: 'scope', + type: CONSTRUCTS_MODULE.Construct, + }); + const id = init.addParameter({ + name: 'id', + type: Type.STRING, + }); + init.addParameter({ + name: 'props', + type: props.constructorPropsType, + optional: props.optionalConstructorProps, + }); + + const superInitializerArgs: Expression[] = [scope, id, props.superProps]; + init.addBody(new SuperInitializer(...superInitializerArgs)); + } +} diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/config.ts b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/config.ts new file mode 100644 index 0000000000000..f17c002cc8217 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/config.ts @@ -0,0 +1,357 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import * as path from 'path'; + +/** + * Handler framework runtimes used for code generation. + */ +export enum Runtime { + /** + * The NodeJs 18.x runtime + */ + NODEJS_18_X = 'nodejs18.x', + + /** + * The Python 3.9 runtime + */ + PYTHON_3_9 = 'python3.9', + + /** + * The Python 3.10 runtime + */ + PYTHON_3_10 = 'python3.10', +} + +/** + * Handler framework component types. + */ +export enum ComponentType { + /** + * Code generated Lambda function + */ + FUNCTION = 'Function', + + /** + * Code generate a Lambda singleton function + */ + SINGLETON_FUNCTION = 'SingletonFunction', + + /** + * Code generate a custom resource provider. + */ + CUSTOM_RESOURCE_PROVIDER = 'CustomResourceProvider', + + /** + * Do not create a handler framework component. + * + * Note: This is used to just move source code for airlifting. + */ + NO_OP = 'NoOp', +} + +/** + * Properites used to generate individual framework components. + */ +export interface ComponentProps { + /** + * The framework component type to generate. + */ + readonly type: ComponentType; + + /** + * The source code that will be executed by the framework component. + */ + readonly sourceCode: string; + + /** + * The runtime that is compatible with the framework component's source code. + * + * @default Runtime.NODEJS_18_X + */ + readonly runtime?: Runtime; + + /** + * The name of the method within your code that the framework component calls. + * + * @default 'index.handler' + */ + readonly handler?: string; + + /** + * Whether the source code should be minified and bundled. + * + * @default true + */ + readonly minifyAndBundle?: boolean; +} + +export type HandlerFrameworkConfig = { [module: string]: { [identifier: string]: ComponentProps[] } }; + +export const config: HandlerFrameworkConfig = { + 'aws-amplify-alpha': { + 'asset-deployment-handler': [ + { + type: ComponentType.NO_OP, + sourceCode: path.resolve(__dirname, '..', 'aws-amplify-alpha', 'asset-deployment-handler', 'index.ts'), + }, + ], + }, + 'aws-certificatemanager': { + 'certificate-request-provider': [ + { + type: ComponentType.FUNCTION, + sourceCode: path.resolve(__dirname, '..', 'aws-certificatemanager', 'dns-validated-certificate-handler', 'index.js'), + handler: 'index.certificateRequestHandler', + }, + ], + }, + 'aws-cloudfront': { + 'cross-region-string-param-reader-provider': [ + { + type: ComponentType.CUSTOM_RESOURCE_PROVIDER, + sourceCode: path.resolve(__dirname, '..', 'aws-cloudfront', 'edge-function', 'index.js'), + }, + ], + }, + 'aws-dynamodb': { + 'replica-provider': [ + { + type: ComponentType.FUNCTION, + sourceCode: path.resolve(__dirname, '..', 'aws-dynamodb', 'replica-handler', 'index.ts'), + handler: 'index.onEventHandler', + }, + { + type: ComponentType.FUNCTION, + sourceCode: path.resolve(__dirname, '..', 'aws-dynamodb', 'replica-handler', 'index.ts'), + handler: 'index.isCompleteHandler', + }, + ], + }, + 'aws-ec2': { + 'restrict-default-sg-provider': [ + { + type: ComponentType.CUSTOM_RESOURCE_PROVIDER, + sourceCode: path.resolve(__dirname, '..', 'aws-ec2', 'restrict-default-security-group-handler', 'index.ts'), + }, + ], + }, + 'aws-ecr': { + 'auto-delete-images-provider': [ + { + type: ComponentType.CUSTOM_RESOURCE_PROVIDER, + sourceCode: path.resolve(__dirname, '..', 'aws-ecr', 'auto-delete-images-handler', 'index.ts'), + }, + ], + }, + 'aws-ecs': { + 'drain-hook-provider': [ + { + type: ComponentType.FUNCTION, + sourceCode: path.resolve(__dirname, '..', 'aws-ecs', 'lambda-source', 'index.py'), + runtime: Runtime.PYTHON_3_9, + handler: 'index.lambda_handler', + minifyAndBundle: false, + }, + ], + }, + 'aws-eks': { + 'cluster-resource-provider': [ + { + type: ComponentType.FUNCTION, + sourceCode: path.resolve(__dirname, '..', 'aws-eks', 'cluster-resource-handler', 'index.ts'), + handler: 'index.onEvent', + }, + { + type: ComponentType.FUNCTION, + sourceCode: path.resolve(__dirname, '..', 'aws-eks', 'cluster-resource-handler', 'index.ts'), + handler: 'index.isComplete', + }, + ], + 'kubectl-provider': [ + { + type: ComponentType.FUNCTION, + sourceCode: path.resolve(__dirname, '..', 'aws-eks', 'kubectl-handler', 'index.py'), + runtime: Runtime.PYTHON_3_10, + minifyAndBundle: false, + }, + { + type: ComponentType.NO_OP, + sourceCode: path.resolve(__dirname, '..', 'aws-eks', 'kubectl-handler', 'apply', '__init__.py'), + runtime: Runtime.PYTHON_3_10, + minifyAndBundle: false, + }, + { + type: ComponentType.NO_OP, + sourceCode: path.resolve(__dirname, '..', 'aws-eks', 'kubectl-handler', 'get', '__init__.py'), + runtime: Runtime.PYTHON_3_10, + minifyAndBundle: false, + }, + { + type: ComponentType.NO_OP, + sourceCode: path.resolve(__dirname, '..', 'aws-eks', 'kubectl-handler', 'helm', '__init__.py'), + runtime: Runtime.PYTHON_3_10, + minifyAndBundle: false, + }, + { + type: ComponentType.NO_OP, + sourceCode: path.resolve(__dirname, '..', 'aws-eks', 'kubectl-handler', 'patch', '__init__.py'), + runtime: Runtime.PYTHON_3_10, + minifyAndBundle: false, + }, + ], + }, + 'aws-events-targets': { + 'aws-api-provider': [ + { + type: ComponentType.SINGLETON_FUNCTION, + sourceCode: path.resolve(__dirname, '..', 'aws-events-targets', 'aws-api-handler', 'index.ts'), + }, + ], + }, + 'aws-iam': { + 'oidc-provider': [ + { + type: ComponentType.CUSTOM_RESOURCE_PROVIDER, + sourceCode: path.resolve(__dirname, '..', 'aws-iam', 'oidc-handler', 'index.ts'), + }, + ], + }, + 'aws-logs': { + 'log-retention': [ + { + type: ComponentType.NO_OP, + sourceCode: path.resolve(__dirname, '..', 'aws-logs', 'log-retention-handler', 'index.ts'), + }, + ], + }, + 'aws-redshift-alpha': { + 'cluster-reboot-provider': [ + { + type: ComponentType.SINGLETON_FUNCTION, + sourceCode: path.resolve(__dirname, '..', 'aws-redshift-alpha', 'cluster-parameter-change-reboot-handler', 'index.ts'), + }, + ], + }, + 'aws-route53': { + 'cross-account-zone-delegation-provider': [ + { + type: ComponentType.CUSTOM_RESOURCE_PROVIDER, + sourceCode: path.resolve(__dirname, '..', 'aws-route53', 'cross-account-zone-delegation-handler', 'index.ts'), + }, + ], + 'delete-existing-record-set-provider': [ + { + type: ComponentType.CUSTOM_RESOURCE_PROVIDER, + sourceCode: path.resolve(__dirname, '..', 'aws-route53', 'delete-existing-record-set-handler', 'index.ts'), + }, + ], + }, + 'aws-s3': { + 'auto-delete-objects-provider': [ + { + type: ComponentType.CUSTOM_RESOURCE_PROVIDER, + sourceCode: path.resolve(__dirname, '..', 'aws-s3', 'auto-delete-objects-handler', 'index.ts'), + }, + ], + 'notifications-resource-handler': [ + { + type: ComponentType.NO_OP, + sourceCode: path.resolve(__dirname, '..', 'aws-s3', 'notifications-resource-handler', 'index.py'), + runtime: Runtime.PYTHON_3_9, + minifyAndBundle: false, + }, + ], + }, + 'aws-s3-deployment': { + 'bucket-deployment-provider': [ + { + type: ComponentType.SINGLETON_FUNCTION, + sourceCode: path.resolve(__dirname, '..', 'aws-s3-deployment', 'bucket-deployment-handler', 'index.py'), + runtime: Runtime.PYTHON_3_9, + minifyAndBundle: false, + }, + ], + }, + 'aws-ses': { + 'drop-spam-provider': [ + { + type: ComponentType.SINGLETON_FUNCTION, + sourceCode: path.resolve(__dirname, '..', 'aws-ses', 'drop-spam-handler', 'index.ts'), + }, + ], + }, + 'aws-stepfunctions-tasks': { + 'eval-nodejs-provider': [ + { + type: ComponentType.SINGLETON_FUNCTION, + sourceCode: path.resolve(__dirname, '..', 'aws-stepfunctions-tasks', 'eval-nodejs-handler', 'index.ts'), + }, + ], + 'role-policy-provider': [ + { + type: ComponentType.SINGLETON_FUNCTION, + sourceCode: path.resolve(__dirname, '..', 'aws-stepfunctions-tasks', 'role-policy-handler', 'index.py'), + runtime: Runtime.PYTHON_3_9, + minifyAndBundle: false, + }, + ], + }, + 'aws-synthetics': { + 'auto-delete-underlying-resources-provider': [ + { + type: ComponentType.CUSTOM_RESOURCE_PROVIDER, + sourceCode: path.resolve(__dirname, '..', 'aws-synthetics', 'auto-delete-underlying-resources-handler', 'index.ts'), + }, + ], + }, + 'core': { + 'cfn-utils-provider': [ + { + type: ComponentType.CUSTOM_RESOURCE_PROVIDER, + sourceCode: path.resolve(__dirname, '..', 'core', 'cfn-utils-provider', 'index.ts'), + }, + ], + 'cross-region-ssm-writer-provider': [ + { + type: ComponentType.CUSTOM_RESOURCE_PROVIDER, + sourceCode: path.resolve(__dirname, '..', 'core', 'cross-region-ssm-writer-handler', 'index.ts'), + }, + ], + 'cross-region-ssm-reader-provider': [ + { + type: ComponentType.CUSTOM_RESOURCE_PROVIDER, + sourceCode: path.resolve(__dirname, '..', 'core', 'cross-region-ssm-reader-handler', 'index.ts'), + }, + ], + 'nodejs-entrypoint-provider': [ + { + type: ComponentType.NO_OP, + sourceCode: path.resolve(__dirname, '..', 'core', 'nodejs-entrypoint-handler', 'index.js'), + minifyAndBundle: false, + }, + ], + }, + 'custom-resources': { + 'aws-custom-resource-provider': [ + { + type: ComponentType.SINGLETON_FUNCTION, + sourceCode: path.resolve(__dirname, '..', 'custom-resources', 'aws-custom-resource-handler', 'index.ts'), + }, + ], + }, + 'pipelines': { + 'approve-lambda': [ + { + type: ComponentType.FUNCTION, + sourceCode: path.resolve(__dirname, '..', 'pipelines', 'approve-lambda', 'index.ts'), + }, + ], + }, + 'triggers': { + 'trigger-provider': [ + { + type: ComponentType.CUSTOM_RESOURCE_PROVIDER, + sourceCode: path.resolve(__dirname, '..', 'triggers', 'lambda', 'index.ts'), + }, + ], + }, +}; diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/framework.ts b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/framework.ts new file mode 100644 index 0000000000000..b77942905fdc3 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/framework.ts @@ -0,0 +1,106 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { ExternalModule, InterfaceType, Module, TypeScriptRenderer } from '@cdklabs/typewriter'; +import * as fs from 'fs-extra'; +import { HandlerFrameworkClass, HandlerFrameworkClassProps } from './classes'; +import { ComponentType, ComponentProps, Runtime } from './config'; +import { buildComponentName } from './utils/framework-utils'; + +export class HandlerFrameworkModule extends Module { + /** + * The latest nodejs runtime version available across all AWS regions. + */ + private static readonly DEFAULT_RUNTIME = Runtime.NODEJS_18_X; + + private readonly renderer = new TypeScriptRenderer(); + private readonly externalModules = new Map(); + private readonly _interfaces = new Map(); + private _hasComponents = false; + + /** + * Whether the module being generated will live inside of aws-cdk-lib/core. + */ + public readonly coreInternal: boolean; + + /** + * Whether the module contains handler framework components. + */ + public get hasComponents() { + return this._hasComponents; + } + + public constructor(fqn: string) { + super(fqn); + this.coreInternal = fqn.includes('core'); + } + + /** + * Build a framework component inside of this module. + */ + public build(component: ComponentProps, codeDirectory: string) { + if (component.type === ComponentType.NO_OP) { + return; + } + + this._hasComponents = true; + + const handler = component.handler ?? 'index.handler'; + const name = buildComponentName(this.fqn, component.type, handler); + + const props: HandlerFrameworkClassProps = { + name, + handler, + codeDirectory, + runtime: component.runtime ?? HandlerFrameworkModule.DEFAULT_RUNTIME, + }; + + switch (component.type) { + case ComponentType.FUNCTION: { + HandlerFrameworkClass.buildFunction(this, props); + break; + } + case ComponentType.SINGLETON_FUNCTION: { + HandlerFrameworkClass.buildSingletonFunction(this, props); + break; + } + case ComponentType.CUSTOM_RESOURCE_PROVIDER: { + HandlerFrameworkClass.buildCustomResourceProvider(this, props); + break; + } + } + } + + /** + * Render module with components into an output file. + */ + public renderTo(file: string) { + fs.outputFileSync(file, this.renderer.render(this)); + } + + /** + * Add an external module to be imported. + */ + public addExternalModule(module: ExternalModule) { + this.externalModules.set(module.fqn, true); + } + + /** + * If an external module has been added as an import to this module. + */ + public hasExternalModule(module: ExternalModule) { + return this.externalModules.has(module.fqn); + } + + /** + * Register an interface with this module. + */ + public registerInterface(_interface: InterfaceType) { + this._interfaces.set(_interface.name, _interface); + } + + /** + * Retrieve an interface that has been registered with this module. + */ + public getInterface(name: string) { + return this._interfaces.get(name); + } +} diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/modules.ts b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/modules.ts new file mode 100644 index 0000000000000..9b2f349a06cd7 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/modules.ts @@ -0,0 +1,60 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { ExternalModule, Type } from '@cdklabs/typewriter'; + +class PathModule extends ExternalModule { + public constructor() { + super('path'); + } +} + +class ConstructsModule extends ExternalModule { + public readonly Construct = Type.fromName(this, 'Construct'); + + public constructor() { + super('constructs'); + } +} + +class CoreModule extends ExternalModule { + public readonly Stack = Type.fromName(this, 'Stack'); + public readonly CustomResourceProviderBase = Type.fromName(this, 'CustomResourceProviderBase'); + public readonly CustomResourceProviderOptions = Type.fromName(this, 'CustomResourceProviderOptions'); + + public constructor() { + super('../../../core'); + } +} + +class CoreInternalStack extends ExternalModule { + public readonly Stack = Type.fromName(this, 'Stack'); + + public constructor() { + super('../../stack'); + } +} + +class CoreInternalCustomResourceProvider extends ExternalModule { + public readonly CustomResourceProviderBase = Type.fromName(this, 'CustomResourceProviderBase'); + public readonly CustomResourceProviderOptions = Type.fromName(this, 'CustomResourceProviderOptions'); + + public constructor() { + super('../../custom-resource-provider'); + } +} + +class LambdaModule extends ExternalModule { + public readonly Function = Type.fromName(this, 'Function'); + public readonly SingletonFunction = Type.fromName(this, 'SingletonFunction'); + public readonly FunctionOptions = Type.fromName(this, 'FunctionOptions'); + + public constructor() { + super('../../../aws-lambda'); + } +} + +export const PATH_MODULE = new PathModule(); +export const CONSTRUCTS_MODULE = new ConstructsModule(); +export const CORE_MODULE = new CoreModule(); +export const CORE_INTERNAL_STACK = new CoreInternalStack(); +export const CORE_INTERNAL_CR_PROVIDER = new CoreInternalCustomResourceProvider(); +export const LAMBDA_MODULE = new LambdaModule(); diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/utils/framework-utils.ts b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/utils/framework-utils.ts new file mode 100644 index 0000000000000..50d3e645f4056 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/utils/framework-utils.ts @@ -0,0 +1,26 @@ +import { ComponentType, Runtime } from '../config'; + +export function buildComponentName(fqn: string, type: ComponentType, entrypoint: string) { + const id = fqn.split('/').at(-1)?.replace('-provider', '') ?? ''; + const handler = entrypoint.split('.').at(-1)?.replace(/[_Hh]andler/g, '') ?? ''; + const name = (id.replace( + /-([a-z])/g, (s) => { return s[1].toUpperCase(); }, + )) + (handler.charAt(0).toUpperCase() + handler.slice(1)); + return name.charAt(0).toUpperCase() + + name.slice(1) + + (type === ComponentType.CUSTOM_RESOURCE_PROVIDER ? 'Provider' : type); +} + +export function toLambdaRuntime(runtime: Runtime) { + switch (runtime) { + case Runtime.NODEJS_18_X: { + return 'lambda.Runtime.NODEJS_18_X'; + } + case Runtime.PYTHON_3_9: { + return 'lambda.Runtime.PYTHON_3_9'; + } + case Runtime.PYTHON_3_10: { + return 'lambda.Runtime.PYTHON_3_10'; + } + } +} diff --git a/packages/@aws-cdk/custom-resource-handlers/package.json b/packages/@aws-cdk/custom-resource-handlers/package.json index 49426b43ff2b6..e5d23c88c84d1 100644 --- a/packages/@aws-cdk/custom-resource-handlers/package.json +++ b/packages/@aws-cdk/custom-resource-handlers/package.json @@ -4,7 +4,7 @@ "private": true, "version": "0.0.0", "scripts": { - "build": "tsc -b && node scripts/minify-and-bundle-sources.js", + "build": "tsc -b && node scripts/generate.js", "integ": "integ-runner", "lint": "cdk-lint", "package": "cdk-package", @@ -50,6 +50,7 @@ "aws-sdk-client-mock": "^3.0.0", "aws-sdk-client-mock-jest": "^3.0.0", "aws-sdk-mock": "5.8.0", + "@cdklabs/typewriter": "^0.0.3", "jest": "^29.7.0", "sinon": "^9.2.4", "nock": "^13.4.0", diff --git a/packages/@aws-cdk/custom-resource-handlers/scripts/generate.ts b/packages/@aws-cdk/custom-resource-handlers/scripts/generate.ts new file mode 100644 index 0000000000000..79018a2fea504 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/scripts/generate.ts @@ -0,0 +1,133 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as esbuild from 'esbuild'; +import { config, ComponentProps } from '../lib/custom-resources-framework/config'; +import { HandlerFrameworkModule } from '../lib/custom-resources-framework/framework'; + +const framework: { [fqn: string]: ComponentProps[] } = {}; + +async function main() { + recurse(config, []); + + for (const [fqn, components] of Object.entries(framework)) { + const module = new HandlerFrameworkModule(fqn); + for (const component of components) { + const outfile = calculateOutfile(component.sourceCode); + if (component.minifyAndBundle ?? true) { + await minifyAndBundle(component.sourceCode, outfile); + } else { + fs.mkdirSync(path.dirname(outfile), { recursive: true }); + fs.copyFileSync(component.sourceCode, outfile); + } + const codeDirectory = path.dirname(outfile).split('/').pop() ?? ''; + module.build(component, codeDirectory); + } + + if (module.hasComponents) { + module.renderTo(`dist/${fqn}.generated.ts`); + } + } + + function recurse(_config: any, _path: string[]) { + // base case - this is a framework component array and we will build a module with + // all defined components + if (_config instanceof Array) { + const fqn = _path.join('/'); + framework[fqn] = _config; + return; + } + + for (const key in _config) { + if (_config.hasOwnProperty(key) && typeof _config[key] === 'object') { + _path.push(key); + recurse(_config[key], _path); + _path.pop(); // backtrack + } + } + } +} + +async function minifyAndBundle(infile: string, outfile: string) { + const result = await esbuild.build({ + entryPoints: [infile], + outfile, + external: ['@aws-sdk/*', 'aws-sdk'], + format: 'cjs', + platform: 'node', + target: 'node18', + bundle: true, + minify: true, + minifyWhitespace: true, + minifySyntax: true, + minifyIdentifiers: true, + sourcemap: false, + tsconfig: 'tsconfig.json', + + // These should be checked because they can lead to runtime failures. There are + // false positives, and the esbuild API does not provide a way to suppress them, + // so we need to do some postprocessing. + logOverride: { + 'unsupported-dynamic-import': 'warning', + 'unsupported-require-call': 'warning', + 'indirect-require': 'warning', + }, + logLevel: 'error', + }); + + const failures = [ + ...result.errors, + ...ignoreWarnings(result), + ]; + + if (failures.length > 0) { + const messages = esbuild.formatMessagesSync(failures, { + kind: 'error', + color: true, + }); + // eslint-disable-next-line no-console + console.log(messages.join('\n')); + // eslint-disable-next-line no-console + console.log(`${messages.length} errors. For false positives, put '// esbuild-disable - ' on the line before`); + process.exitCode = 1; + } +} + +export function calculateOutfile(file: string) { + // turn ts extension into js extension + if (file.includes('index.ts')) { + file = path.join(path.dirname(file), path.basename(file, path.extname(file)) + '.js'); + } + + // replace /lib with /dist + const fileContents = file.split(path.sep); + fileContents[fileContents.lastIndexOf('lib')] = 'dist'; + + return fileContents.join(path.sep); +} + +function ignoreWarnings(result: esbuild.BuildResult) { + const ret: esbuild.Message[] = []; + for (const warning of result.warnings) { + let suppressed = false; + if (warning.location?.file) { + const contents = fs.readFileSync(warning.location.file, { encoding: 'utf-8' }); + const lines = contents.split('\n'); + const lineBefore = lines[warning.location.line - 1 - 1]; + + if (lineBefore.includes(`esbuild-disable ${warning.id}`)) { + suppressed = true; + } + } + + if (!suppressed) { + ret.push(warning); + } + } + return ret; +} + +main().catch((e) => { + // eslint-disable-next-line no-console + console.error(e); + process.exitCode = 1; +}); diff --git a/packages/@aws-cdk/custom-resource-handlers/scripts/minify-and-bundle-sources.ts b/packages/@aws-cdk/custom-resource-handlers/scripts/minify-and-bundle-sources.ts deleted file mode 100644 index ed618b4446ae1..0000000000000 --- a/packages/@aws-cdk/custom-resource-handlers/scripts/minify-and-bundle-sources.ts +++ /dev/null @@ -1,115 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import * as esbuild from 'esbuild'; - -const entryPoints: string[] = []; -function recFolderStructure(fileOrDir: string) { - if (fs.statSync(fileOrDir).isDirectory()) { - const items = fs.readdirSync(fileOrDir); - for (const i of items) { - recFolderStructure(path.join(fileOrDir, i)); - } - } else { - if (!fileOrDir.includes('nodejs-entrypoint-handler/index.ts') && - ['index.ts', 'index.js', 'index.py', '__init__.py'].some(fileName => fileOrDir.includes(fileName))) { - entryPoints.push(fileOrDir); - } - } -} - -async function main() { - const bindingsDir = path.join(__dirname, '..', 'lib'); - - recFolderStructure(bindingsDir); - - for (const ep of entryPoints) { - // Do not bundle python files. Additionally, do not bundle nodejs-entrypoint-handler due to require import - if (['index.py', '__init__.py', 'nodejs-entrypoint-handler/index.js'].some(fileName => ep.includes(fileName))) { - const outfile = calculateOutfile(ep); - fs.mkdirSync(path.dirname(outfile), { recursive: true }); - fs.copyFileSync(ep, outfile); - } else { - const result = await esbuild.build({ - entryPoints: [ep], - outfile: calculateOutfile(ep), - external: ['@aws-sdk/*', 'aws-sdk'], - format: 'cjs', - platform: 'node', - bundle: true, - minify: true, - minifyWhitespace: true, - minifySyntax: true, - minifyIdentifiers: true, - sourcemap: false, - tsconfig: 'tsconfig.json', - - // These should be checked because they can lead to runtime failures. There are - // false positives, and the esbuild API does not provide a way to suppress them, - // so we need to do some postprocessing. - logOverride: { - 'unsupported-dynamic-import': 'warning', - 'unsupported-require-call': 'warning', - 'indirect-require': 'warning', - }, - logLevel: 'error', - }); - - const failures = [ - ...result.errors, - ...ignoreWarnings(result), - ]; - - if (failures.length > 0) { - const messages = esbuild.formatMessagesSync(failures, { - kind: 'error', - color: true, - }); - // eslint-disable-next-line no-console - console.log(messages.join('\n')); - // eslint-disable-next-line no-console - console.log(`${messages.length} errors. For false positives, put '// esbuild-disable - ' on the line before`); - process.exitCode = 1; - } - } - } - - function calculateOutfile(file: string) { - // turn ts extension into js extension - if (file.includes('index.ts')) { - file = path.join(path.dirname(file), path.basename(file, path.extname(file)) + '.js'); - } - - // replace /lib with /dist - const fileContents = file.split(path.sep); - fileContents[fileContents.lastIndexOf('lib')] = 'dist'; - - return fileContents.join(path.sep); - } -} - -function ignoreWarnings(result: esbuild.BuildResult) { - const ret: esbuild.Message[] = []; - for (const warning of result.warnings) { - let suppressed = false; - if (warning.location?.file) { - const contents = fs.readFileSync(warning.location.file, { encoding: 'utf-8' }); - const lines = contents.split('\n'); - const lineBefore = lines[warning.location.line - 1 - 1]; - - if (lineBefore.includes(`esbuild-disable ${warning.id}`)) { - suppressed = true; - } - } - - if (!suppressed) { - ret.push(warning); - } - } - return ret; -} - -main().catch((e) => { - // eslint-disable-next-line no-console - console.error(e); - process.exitCode = 1; -}); \ No newline at end of file diff --git a/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/custom-resource-provider-core.ts b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/custom-resource-provider-core.ts new file mode 100644 index 0000000000000..84353b7b77372 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/custom-resource-provider-core.ts @@ -0,0 +1,32 @@ +/* eslint-disable prettier/prettier,max-len */ +import * as path from "path"; +import { Construct } from "constructs"; +import { Stack } from "../../stack"; +import { CustomResourceProviderBase, CustomResourceProviderOptions } from "../../custom-resource-provider"; + +export class TestProvider extends CustomResourceProviderBase { + /** + * Returns a stack-level singleton ARN (service token) for the custom resource provider. + */ + public static getOrCreate(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): string { + return this.getOrCreateProvider(scope, uniqueid, props).serviceToken; + } + + /** + * Returns a stack-level singleton for the custom resource provider. + */ + public static getOrCreateProvider(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): TestProvider { + const id = `${uniqueid}CustomResourceProvider`; + const stack = Stack.of(scope); + const existing = stack.node.tryFindChild(id) as TestProvider; + return existing ?? new TestProvider(stack, id, props); + } + + public constructor(scope: Construct, id: string, props?: CustomResourceProviderOptions) { + super(scope, id, { + ...props, + "codeDirectory": path.join(__dirname, 'my-handler'), + "runtimeName": "nodejs18.x" + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/custom-resource-provider.ts b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/custom-resource-provider.ts new file mode 100644 index 0000000000000..dfecaf43427eb --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/custom-resource-provider.ts @@ -0,0 +1,31 @@ +/* eslint-disable prettier/prettier,max-len */ +import * as path from "path"; +import { Construct } from "constructs"; +import { Stack, CustomResourceProviderBase, CustomResourceProviderOptions } from "../../../core"; + +export class TestProvider extends CustomResourceProviderBase { + /** + * Returns a stack-level singleton ARN (service token) for the custom resource provider. + */ + public static getOrCreate(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): string { + return this.getOrCreateProvider(scope, uniqueid, props).serviceToken; + } + + /** + * Returns a stack-level singleton for the custom resource provider. + */ + public static getOrCreateProvider(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): TestProvider { + const id = `${uniqueid}CustomResourceProvider`; + const stack = Stack.of(scope); + const existing = stack.node.tryFindChild(id) as TestProvider; + return existing ?? new TestProvider(stack, id, props); + } + + public constructor(scope: Construct, id: string, props?: CustomResourceProviderOptions) { + super(scope, id, { + ...props, + "codeDirectory": path.join(__dirname, 'my-handler'), + "runtimeName": "nodejs18.x" + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/function.ts b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/function.ts new file mode 100644 index 0000000000000..8907e5f2c0540 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/function.ts @@ -0,0 +1,15 @@ +/* eslint-disable prettier/prettier,max-len */ +import * as path from "path"; +import { Construct } from "constructs"; +import * as lambda from "../../../aws-lambda"; + +export class TestFunction extends lambda.Function { + public constructor(scope: Construct, id: string, props?: lambda.FunctionOptions) { + super(scope, id, { + ...props, + "code": lambda.Code.fromAsset(path.join(__dirname, 'my-handler')), + "handler": "index.handler", + "runtime": lambda.Runtime.NODEJS_18_X + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/singleton-function.ts b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/singleton-function.ts new file mode 100644 index 0000000000000..7aa1d645e3f71 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/singleton-function.ts @@ -0,0 +1,39 @@ +/* eslint-disable prettier/prettier,max-len */ +import * as path from "path"; +import { Construct } from "constructs"; +import * as lambda from "../../../aws-lambda"; + +export class TestSingletonFunction extends lambda.SingletonFunction { + public constructor(scope: Construct, id: string, props: TestSingletonFunctionProps) { + super(scope, id, { + ...props, + "code": lambda.Code.fromAsset(path.join(__dirname, 'my-handler')), + "handler": "index.handler", + "runtime": lambda.Runtime.NODEJS_18_X + }); + } +} + +/** + * Initialization properties for TestSingletonFunction + */ +export interface TestSingletonFunctionProps extends lambda.FunctionOptions { + /** + * A unique identifier to identify this Lambda. + * + * The identifier should be unique across all custom resource providers. + * We recommend generating a UUID per provider. + */ + readonly uuid: string; + + /** + * A descriptive name for the purpose of this Lambda. + * + * If the Lambda does not have a physical name, this string will be + * reflected in its generated name. The combination of lambdaPurpose + * and uuid must be unique. + * + * @default SingletonLambda + */ + readonly lambdaPurpose?: string; +} \ No newline at end of file diff --git a/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/framework.test.ts b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/framework.test.ts new file mode 100644 index 0000000000000..14e4ee2491f45 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/framework.test.ts @@ -0,0 +1,93 @@ +import * as os from 'os'; +import * as path from 'path'; +import * as fs from 'fs-extra'; +import { ComponentProps, ComponentType, Runtime } from '../../lib/custom-resources-framework/config'; +import { HandlerFrameworkModule } from '../../lib/custom-resources-framework/framework'; +import { calculateOutfile } from '../../scripts/generate'; + +/* eslint-disable no-console */ + +const sourceCode = path.resolve(__dirname, 'my-handler', 'index.ts'); + +describe('framework', () => { + let tmpDir: string; + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-test')); + }); + + test('can codegen cdk function', () => { + // GIVEN + const module = new HandlerFrameworkModule('cdk-testing/test-provider'); + const component: ComponentProps = { + type: ComponentType.FUNCTION, + sourceCode, + }; + const outfile = calculateOutfile(sourceCode); + module.build(component, path.dirname(outfile).split('/').pop() ?? 'dist'); + + // WHEN + module.renderTo(`${tmpDir}/result.ts`); + + // THEN + const result = fs.readFileSync(path.resolve(tmpDir, 'result.ts'), 'utf-8'); + const expected = fs.readFileSync(path.resolve(__dirname, 'expected', 'function.ts'), 'utf-8'); + expect(result).toContain(expected); + }); + + test('can codegen cdk singleton function', () => { + // GIVEN + const module = new HandlerFrameworkModule('cdk-testing/test-provider'); + const component: ComponentProps = { + type: ComponentType.SINGLETON_FUNCTION, + sourceCode, + }; + const outfile = calculateOutfile(sourceCode); + module.build(component, path.dirname(outfile).split('/').pop() ?? 'dist'); + + // WHEN + module.renderTo(`${tmpDir}/result.ts`); + + // THEN + const result = fs.readFileSync(path.resolve(tmpDir, 'result.ts'), 'utf-8'); + const expected = fs.readFileSync(path.resolve(__dirname, 'expected', 'singleton-function.ts'), 'utf-8'); + expect(result).toContain(expected); + }); + + test('can codegen cdk custom resource provider for core internal', () => { + // GIVEN + const module = new HandlerFrameworkModule('cdk-testing/test-provider'); + const component: ComponentProps = { + type: ComponentType.CUSTOM_RESOURCE_PROVIDER, + sourceCode, + }; + const outfile = calculateOutfile(sourceCode); + module.build(component, path.dirname(outfile).split('/').pop() ?? 'dist'); + + // WHEN + module.renderTo(`${tmpDir}/result.ts`); + + // THEN + const result = fs.readFileSync(path.resolve(tmpDir, 'result.ts'), 'utf-8'); + const expected = fs.readFileSync(path.resolve(__dirname, 'expected', 'custom-resource-provider.ts'), 'utf-8'); + expect(result).toContain(expected); + }); + + test('can codegen cdk custom resource provider for core internal', () => { + // GIVEN + const module = new HandlerFrameworkModule('core/test-provider'); + const component: ComponentProps = { + type: ComponentType.CUSTOM_RESOURCE_PROVIDER, + sourceCode, + }; + const outfile = calculateOutfile(sourceCode); + module.build(component, path.dirname(outfile).split('/').pop() ?? 'dist'); + + // WHEN + module.renderTo(`${tmpDir}/result.ts`); + + // THEN + const result = fs.readFileSync(path.resolve(tmpDir, 'result.ts'), 'utf-8'); + const expected = fs.readFileSync(path.resolve(__dirname, 'expected', 'custom-resource-provider-core.ts'), 'utf-8'); + expect(result).toContain(expected); + }); +}); diff --git a/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/my-handler/index.ts b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/my-handler/index.ts new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/utils/framework-utils.test.ts b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/utils/framework-utils.test.ts new file mode 100644 index 0000000000000..e8f804df3253a --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/utils/framework-utils.test.ts @@ -0,0 +1,66 @@ +import { ComponentType, Runtime } from '../../../lib/custom-resources-framework/config'; +import { buildComponentName, toLambdaRuntime } from '../../../lib/custom-resources-framework/utils/framework-utils'; + +describe('to lambda runtime', () => { + test.each([ + [Runtime.NODEJS_18_X, 'lambda.Runtime.NODEJS_18_X'], + [Runtime.PYTHON_3_9, 'lambda.Runtime.PYTHON_3_9'], + [Runtime.PYTHON_3_10, 'lambda.Runtime.PYTHON_3_10'], + ])('to lambda %s runtime', (runtime, expectedRuntime) => { + expect(toLambdaRuntime(runtime)).toEqual(expectedRuntime); + }); +}); + +describe('build compoonent name', () => { + test('build function component name', () => { + // GIVEN + const fqn = 'test/aws-cdk-provider'; + const type = ComponentType.FUNCTION; + const entrypoint = 'index.handler'; + + // WHEN + const name = buildComponentName(fqn, type, entrypoint); + + // THEN + expect(name).toEqual('AwsCdkFunction'); + }); + + test('build singleton function component name', () => { + // GIVEN + const fqn = 'test/aws-cdk-provider'; + const type = ComponentType.SINGLETON_FUNCTION; + const entrypoint = 'index.handler'; + + // WHEN + const name = buildComponentName(fqn, type, entrypoint); + + // THEN + expect(name).toEqual('AwsCdkSingletonFunction'); + }); + + test('build custom resource provider component name', () => { + // GIVEN + const fqn = 'test/aws-cdk-provider'; + const type = ComponentType.CUSTOM_RESOURCE_PROVIDER; + const entrypoint = 'index.handler'; + + // WHEN + const name = buildComponentName(fqn, type, entrypoint); + + // THEN + expect(name).toEqual('AwsCdkProvider'); + }); + + test('with non-default handler', () => { + // GIVEN + const fqn = 'test/aws-cdk-provider'; + const type = ComponentType.FUNCTION; + const entrypoint = 'index.onEventHandler'; + + // WHEN + const name = buildComponentName(fqn, type, entrypoint); + + // THEN + expect(name).toEqual('AwsCdkOnEventFunction'); + }); +}); diff --git a/packages/@aws-cdk/custom-resource-handlers/tsconfig.json b/packages/@aws-cdk/custom-resource-handlers/tsconfig.json index 8930cca79e2bd..66153ae4aeb2f 100644 --- a/packages/@aws-cdk/custom-resource-handlers/tsconfig.json +++ b/packages/@aws-cdk/custom-resource-handlers/tsconfig.json @@ -23,5 +23,7 @@ ], "exclude": [ "**/test/**/*.ts", + "dist", + "runtime-determiner", ], } diff --git a/packages/aws-cdk-lib/aws-certificatemanager/lib/dns-validated-certificate.ts b/packages/aws-cdk-lib/aws-certificatemanager/lib/dns-validated-certificate.ts index d2826d6fd796b..b3167087ae9d6 100644 --- a/packages/aws-cdk-lib/aws-certificatemanager/lib/dns-validated-certificate.ts +++ b/packages/aws-cdk-lib/aws-certificatemanager/lib/dns-validated-certificate.ts @@ -1,12 +1,11 @@ -import * as path from 'path'; import { Construct } from 'constructs'; import { CertificateProps, ICertificate } from './certificate'; import { CertificateBase } from './certificate-base'; import * as iam from '../../aws-iam'; -import * as lambda from '../../aws-lambda'; import * as route53 from '../../aws-route53'; import * as cdk from '../../core'; import { Token } from '../../core'; +import { CertificateRequestCertificateRequestFunction } from '../../custom-resource-handlers/dist/aws-certificatemanager/certificate-request-provider.generated'; /** * Properties to create a DNS validated certificate managed by AWS Certificate Manager @@ -106,10 +105,7 @@ export class DnsValidatedCertificate extends CertificateBase implements ICertifi certificateTransparencyLoggingPreference = props.transparencyLoggingEnabled ? 'ENABLED' : 'DISABLED'; } - const requestorFunction = new lambda.Function(this, 'CertificateRequestorFunction', { - code: lambda.Code.fromAsset(path.resolve(__dirname, '..', '..', 'custom-resource-handlers', 'dist', 'aws-certificatemanager', 'dns-validated-certificate-handler')), - handler: 'index.certificateRequestHandler', - runtime: lambda.Runtime.NODEJS_18_X, + const requestorFunction = new CertificateRequestCertificateRequestFunction(this, 'CertificateRequestorFunction', { timeout: cdk.Duration.minutes(15), role: props.customResourceRole, }); diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/experimental/edge-function.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/experimental/edge-function.ts index 27ac19d11c25b..0dcf4c581598c 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/experimental/edge-function.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/experimental/edge-function.ts @@ -1,4 +1,3 @@ -import * as path from 'path'; import { Construct, Node } from 'constructs'; import * as cloudwatch from '../../../aws-cloudwatch'; import * as ec2 from '../../../aws-ec2'; @@ -8,14 +7,13 @@ import * as ssm from '../../../aws-ssm'; import { CfnResource, CustomResource, - CustomResourceProvider, - CustomResourceProviderRuntime, Lazy, Resource, Stack, Stage, Token, } from '../../../core'; +import { CrossRegionStringParamReaderProvider } from '../../../custom-resource-handlers/dist/aws-cloudfront/cross-region-string-param-reader-provider.generated'; /** * Properties for creating a Lambda@Edge function @@ -199,9 +197,7 @@ export class EdgeFunction extends Resource implements lambda.IVersion { }); const resourceType = 'Custom::CrossRegionStringParameterReader'; - const serviceToken = CustomResourceProvider.getOrCreate(this, resourceType, { - codeDirectory: path.join(__dirname, '..', '..', '..', 'custom-resource-handlers', 'dist', 'aws-cloudfront', 'edge-function'), - runtime: CustomResourceProviderRuntime.NODEJS_18_X, + const serviceToken = CrossRegionStringParamReaderProvider.getOrCreate(this, resourceType, { policyStatements: [{ Effect: 'Allow', Resource: parameterArnPrefix, diff --git a/packages/aws-cdk-lib/aws-dynamodb/lib/replica-provider.ts b/packages/aws-cdk-lib/aws-dynamodb/lib/replica-provider.ts index fc3329ad1c998..7a266668e0d7c 100644 --- a/packages/aws-cdk-lib/aws-dynamodb/lib/replica-provider.ts +++ b/packages/aws-cdk-lib/aws-dynamodb/lib/replica-provider.ts @@ -1,8 +1,8 @@ -import * as path from 'path'; import { Construct } from 'constructs'; import * as iam from '../../aws-iam'; import * as lambda from '../../aws-lambda'; import { Aws, Duration, NestedStack, Stack } from '../../core'; +import { ReplicaOnEventFunction, ReplicaIsCompleteFunction } from '../../custom-resource-handlers/dist/aws-dynamodb/replica-provider.generated'; import * as cr from '../../custom-resources'; /** @@ -55,21 +55,13 @@ export class ReplicaProvider extends NestedStack { private constructor(scope: Construct, id: string, props: ReplicaProviderProps) { super(scope, id); - const code = lambda.Code.fromAsset(path.join(__dirname, '..', '..', 'custom-resource-handlers', 'dist', 'aws-dynamodb', 'replica-handler')); - // Issues UpdateTable API calls - this.onEventHandler = new lambda.Function(this, 'OnEventHandler', { - code, - runtime: lambda.Runtime.NODEJS_18_X, - handler: 'index.onEventHandler', + this.onEventHandler = new ReplicaOnEventFunction(this, 'OnEventHandler', { timeout: Duration.minutes(5), }); // Checks if table is back to `ACTIVE` state - this.isCompleteHandler = new lambda.Function(this, 'IsCompleteHandler', { - code, - runtime: lambda.Runtime.NODEJS_18_X, - handler: 'index.isCompleteHandler', + this.isCompleteHandler = new ReplicaIsCompleteFunction(this, 'IsCompleteHandler', { timeout: Duration.seconds(30), }); diff --git a/packages/aws-cdk-lib/aws-ec2/lib/vpc.ts b/packages/aws-cdk-lib/aws-ec2/lib/vpc.ts index 536a9fb8fd5aa..c5f03e5f21df6 100644 --- a/packages/aws-cdk-lib/aws-ec2/lib/vpc.ts +++ b/packages/aws-cdk-lib/aws-ec2/lib/vpc.ts @@ -1,4 +1,3 @@ -import * as path from 'path'; import { Construct, Dependable, DependencyGroup, IConstruct, IDependable, Node } from 'constructs'; import { ClientVpnEndpoint, ClientVpnEndpointOptions } from './client-vpn-endpoint'; import { @@ -17,8 +16,9 @@ import { EnableVpnGatewayOptions, VpnConnection, VpnConnectionOptions, VpnConnec import * as cxschema from '../../cloud-assembly-schema'; import { Arn, Annotations, ContextProvider, - IResource, Lazy, Resource, Stack, Token, Tags, Names, CustomResourceProvider, CustomResourceProviderRuntime, CustomResource, FeatureFlags, + IResource, Lazy, Resource, Stack, Token, Tags, Names, CustomResource, FeatureFlags, } from '../../core'; +import { RestrictDefaultSgProvider } from '../../custom-resource-handlers/dist/aws-ec2/restrict-default-sg-provider.generated'; import * as cxapi from '../../cx-api'; import { EC2_RESTRICT_DEFAULT_SECURITY_GROUP } from '../../cx-api'; @@ -1698,9 +1698,7 @@ export class Vpc extends VpcBase { private restrictDefaultSecurityGroup(): void { const id = 'Custom::VpcRestrictDefaultSG'; - const provider = CustomResourceProvider.getOrCreateProvider(this, id, { - codeDirectory: path.join(__dirname, '..', '..', 'custom-resource-handlers', 'dist', 'aws-ec2', 'restrict-default-security-group-handler'), - runtime: CustomResourceProviderRuntime.NODEJS_18_X, + const provider = RestrictDefaultSgProvider.getOrCreateProvider(this, id, { description: 'Lambda function for removing all inbound/outbound rules from the VPC default security group', }); provider.addToRolePolicy({ diff --git a/packages/aws-cdk-lib/aws-ecr/lib/repository.ts b/packages/aws-cdk-lib/aws-ecr/lib/repository.ts index ef4fa18d809c9..008753dbd567d 100644 --- a/packages/aws-cdk-lib/aws-ecr/lib/repository.ts +++ b/packages/aws-cdk-lib/aws-ecr/lib/repository.ts @@ -1,5 +1,4 @@ import { EOL } from 'os'; -import * as path from 'path'; import { IConstruct, Construct } from 'constructs'; import { CfnRepository } from './ecr.generated'; import { LifecycleRule, TagStatus } from './lifecycle'; @@ -18,10 +17,9 @@ import { Token, TokenComparison, CustomResource, - CustomResourceProvider, - CustomResourceProviderRuntime, Aws, } from '../../core'; +import { AutoDeleteImagesProvider } from '../../custom-resource-handlers/dist/aws-ecr/auto-delete-images-provider.generated'; const AUTO_DELETE_IMAGES_RESOURCE_TYPE = 'Custom::ECRAutoDeleteImages'; const AUTO_DELETE_IMAGES_TAG = 'aws-cdk:auto-delete-images'; @@ -863,10 +861,8 @@ export class Repository extends RepositoryBase { private enableAutoDeleteImages() { const firstTime = Stack.of(this).node.tryFindChild(`${AUTO_DELETE_IMAGES_RESOURCE_TYPE}CustomResourceProvider`) === undefined; - const provider = CustomResourceProvider.getOrCreateProvider(this, AUTO_DELETE_IMAGES_RESOURCE_TYPE, { - codeDirectory: path.join(__dirname, '..', '..', 'custom-resource-handlers', 'dist', 'aws-ecr', 'auto-delete-images-handler'), + const provider = AutoDeleteImagesProvider.getOrCreateProvider(this, AUTO_DELETE_IMAGES_RESOURCE_TYPE, { useCfnResponseWrapper: false, - runtime: CustomResourceProviderRuntime.NODEJS_18_X, description: `Lambda function for auto-deleting images in ${this.repositoryName} repository.`, }); diff --git a/packages/aws-cdk-lib/aws-eks/lib/cluster-resource-provider.ts b/packages/aws-cdk-lib/aws-eks/lib/cluster-resource-provider.ts index 55a0caa27a6c0..1732dba7aca77 100644 --- a/packages/aws-cdk-lib/aws-eks/lib/cluster-resource-provider.ts +++ b/packages/aws-cdk-lib/aws-eks/lib/cluster-resource-provider.ts @@ -1,13 +1,11 @@ -import * as path from 'path'; import { Construct } from 'constructs'; import * as ec2 from '../../aws-ec2'; import * as lambda from '../../aws-lambda'; import { Duration, NestedStack, Stack } from '../../core'; +import { ClusterResourceOnEventFunction, ClusterResourceIsCompleteFunction } from '../../custom-resource-handlers/dist/aws-eks/cluster-resource-provider.generated'; import * as cr from '../../custom-resources'; import { NodeProxyAgentLayer } from '../../lambda-layer-node-proxy-agent'; -const HANDLER_DIR = path.join(__dirname, '..', '..', 'custom-resource-handlers', 'dist', 'aws-eks', 'cluster-resource-handler'); - export interface ClusterResourceProviderProps { /** @@ -66,15 +64,12 @@ export class ClusterResourceProvider extends NestedStack { // The NPM dependency proxy-agent is required in order to support proxy routing with the AWS JS SDK. const nodeProxyAgentLayer = new NodeProxyAgentLayer(this, 'NodeProxyAgentLayer'); - const onEvent = new lambda.Function(this, 'OnEventHandler', { - code: lambda.Code.fromAsset(HANDLER_DIR), + const onEvent = new ClusterResourceOnEventFunction(this, 'OnEventHandler', { description: 'onEvent handler for EKS cluster resource provider', - runtime: lambda.Runtime.NODEJS_18_X, environment: { AWS_STS_REGIONAL_ENDPOINTS: 'regional', ...props.environment, }, - handler: 'index.onEvent', timeout: Duration.minutes(1), vpc: props.subnets ? props.vpc : undefined, vpcSubnets: props.subnets ? { subnets: props.subnets } : undefined, @@ -83,15 +78,12 @@ export class ClusterResourceProvider extends NestedStack { layers: props.onEventLayer ? [props.onEventLayer] : [nodeProxyAgentLayer], }); - const isComplete = new lambda.Function(this, 'IsCompleteHandler', { - code: lambda.Code.fromAsset(HANDLER_DIR), + const isComplete = new ClusterResourceIsCompleteFunction(this, 'IsCompleteHandler', { description: 'isComplete handler for EKS cluster resource provider', - runtime: lambda.Runtime.NODEJS_18_X, environment: { AWS_STS_REGIONAL_ENDPOINTS: 'regional', ...props.environment, }, - handler: 'index.isComplete', timeout: Duration.minutes(1), vpc: props.subnets ? props.vpc : undefined, vpcSubnets: props.subnets ? { subnets: props.subnets } : undefined, diff --git a/packages/aws-cdk-lib/aws-eks/lib/kubectl-provider.ts b/packages/aws-cdk-lib/aws-eks/lib/kubectl-provider.ts index 3f2d473ce94b9..ceab1298df378 100644 --- a/packages/aws-cdk-lib/aws-eks/lib/kubectl-provider.ts +++ b/packages/aws-cdk-lib/aws-eks/lib/kubectl-provider.ts @@ -1,9 +1,8 @@ -import * as path from 'path'; import { Construct, IConstruct } from 'constructs'; import { ICluster, Cluster } from './cluster'; import * as iam from '../../aws-iam'; -import * as lambda from '../../aws-lambda'; import { Duration, Stack, NestedStack, Names, CfnCondition, Fn, Aws } from '../../core'; +import { KubectlFunction } from '../../custom-resource-handlers/dist/aws-eks/kubectl-provider.generated'; import * as cr from '../../custom-resources'; import { AwsCliLayer } from '../../lambda-layer-awscli'; import { KubectlLayer } from '../../lambda-layer-kubectl'; @@ -133,10 +132,7 @@ export class KubectlProvider extends NestedStack implements IKubectlProvider { const memorySize = cluster.kubectlMemory ? cluster.kubectlMemory.toMebibytes() : 1024; - const handler = new lambda.Function(this, 'Handler', { - code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', 'custom-resource-handlers', 'dist', 'aws-eks', 'kubectl-handler')), - runtime: lambda.Runtime.PYTHON_3_10, - handler: 'index.handler', + const handler = new KubectlFunction(this, 'Handler', { timeout: Duration.minutes(15), description: 'onEvent handler for EKS kubectl resource provider', memorySize, diff --git a/packages/aws-cdk-lib/aws-events-targets/lib/aws-api.ts b/packages/aws-cdk-lib/aws-events-targets/lib/aws-api.ts index 1dd24040ee6a2..1b5beebd9a64e 100644 --- a/packages/aws-cdk-lib/aws-events-targets/lib/aws-api.ts +++ b/packages/aws-cdk-lib/aws-events-targets/lib/aws-api.ts @@ -1,10 +1,10 @@ -import * as path from 'path'; import { metadata } from './sdk-api-metadata.generated'; import { addLambdaPermission } from './util'; import * as events from '../../aws-events'; import * as iam from '../../aws-iam'; import * as lambda from '../../aws-lambda'; import { Annotations, Duration } from '../../core'; +import { AwsApiSingletonFunction } from '../../custom-resource-handlers/dist/aws-events-targets/aws-api-provider.generated'; /** * AWS SDK service metadata. @@ -81,12 +81,9 @@ export class AwsApi implements events.IRuleTarget { * result from an EventBridge event. */ public bind(rule: events.IRule, id?: string): events.RuleTargetConfig { - const handler = new lambda.SingletonFunction(rule as events.Rule, `${rule.node.id}${id}Handler`, { - code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', 'custom-resource-handlers', 'dist', 'aws-events-targets', 'aws-api-handler')), - runtime: lambda.Runtime.NODEJS_18_X, + const handler = new AwsApiSingletonFunction(rule as events.Rule, `${rule.node.id}${id}Handler`, { timeout: Duration.seconds(60), memorySize: 256, - handler: 'index.handler', uuid: 'b4cf1abd-4e4f-4bc6-9944-1af7ccd9ec37', lambdaPurpose: 'AWS', }); diff --git a/packages/aws-cdk-lib/aws-iam/lib/oidc-provider.ts b/packages/aws-cdk-lib/aws-iam/lib/oidc-provider.ts index c7456b5867463..b8c5494038a24 100644 --- a/packages/aws-cdk-lib/aws-iam/lib/oidc-provider.ts +++ b/packages/aws-cdk-lib/aws-iam/lib/oidc-provider.ts @@ -1,14 +1,12 @@ -import * as path from 'path'; import { Construct } from 'constructs'; import { Arn, CustomResource, - CustomResourceProvider, - CustomResourceProviderRuntime, IResource, Resource, Token, } from '../../core'; +import { OidcProvider } from '../../custom-resource-handlers/dist/aws-iam/oidc-provider.generated'; const RESOURCE_TYPE = 'Custom::AWSCDKOpenIdConnectProvider'; @@ -162,9 +160,7 @@ export class OpenIdConnectProvider extends Resource implements IOpenIdConnectPro } private getOrCreateProvider() { - return CustomResourceProvider.getOrCreateProvider(this, RESOURCE_TYPE, { - codeDirectory: path.join(__dirname, '..', '..', 'custom-resource-handlers', 'dist', 'aws-iam', 'oidc-handler'), - runtime: CustomResourceProviderRuntime.NODEJS_18_X, + return OidcProvider.getOrCreateProvider(this, RESOURCE_TYPE, { policyStatements: [ { Effect: 'Allow', diff --git a/packages/aws-cdk-lib/aws-route53/lib/record-set.ts b/packages/aws-cdk-lib/aws-route53/lib/record-set.ts index f9f34f60e7fcd..15b49c7e36cd5 100644 --- a/packages/aws-cdk-lib/aws-route53/lib/record-set.ts +++ b/packages/aws-cdk-lib/aws-route53/lib/record-set.ts @@ -1,4 +1,3 @@ -import * as path from 'path'; import { Construct } from 'constructs'; import { IAliasRecordTarget } from './alias-record-target'; import { GeoLocation } from './geo-location'; @@ -6,7 +5,9 @@ import { IHostedZone } from './hosted-zone-ref'; import { CfnRecordSet } from './route53.generated'; import { determineFullyQualifiedDomainName } from './util'; import * as iam from '../../aws-iam'; -import { CustomResource, CustomResourceProvider, CustomResourceProviderRuntime, Duration, IResource, RemovalPolicy, Resource, Token } from '../../core'; +import { CustomResource, Duration, IResource, RemovalPolicy, Resource, Token } from '../../core'; +import { CrossAccountZoneDelegationProvider } from '../../custom-resource-handlers/dist/aws-route53/cross-account-zone-delegation-provider.generated'; +import { DeleteExistingRecordSetProvider } from '../../custom-resource-handlers/dist/aws-route53/delete-existing-record-set-provider.generated'; const CROSS_ACCOUNT_ZONE_DELEGATION_RESOURCE_TYPE = 'Custom::CrossAccountZoneDelegation'; const DELETE_EXISTING_RECORD_SET_RESOURCE_TYPE = 'Custom::DeleteExistingRecordSet'; @@ -268,16 +269,13 @@ export class RecordSet extends Resource implements IRecordSet { if (props.deleteExisting) { // Delete existing record before creating the new one - const provider = CustomResourceProvider.getOrCreateProvider(this, DELETE_EXISTING_RECORD_SET_RESOURCE_TYPE, { - codeDirectory: path.join(__dirname, '..', '..', 'custom-resource-handlers', 'dist', 'aws-route53', 'delete-existing-record-set-handler'), - runtime: CustomResourceProviderRuntime.NODEJS_18_X, + const provider = DeleteExistingRecordSetProvider.getOrCreateProvider(this, DELETE_EXISTING_RECORD_SET_RESOURCE_TYPE, { policyStatements: [{ // IAM permissions for all providers Effect: 'Allow', Action: 'route53:GetChange', Resource: '*', }], }); - // Add to the singleton policy for this specific provider provider.addToRolePolicy({ Effect: 'Allow', @@ -773,10 +771,7 @@ export class CrossAccountZoneDelegationRecord extends Construct { throw Error('Only one of parentHostedZoneName and parentHostedZoneId is supported'); } - const provider = CustomResourceProvider.getOrCreateProvider(this, CROSS_ACCOUNT_ZONE_DELEGATION_RESOURCE_TYPE, { - codeDirectory: path.join(__dirname, '..', '..', 'custom-resource-handlers', 'dist', 'aws-route53', 'cross-account-zone-delegation-handler'), - runtime: CustomResourceProviderRuntime.NODEJS_18_X, - }); + const provider = CrossAccountZoneDelegationProvider.getOrCreateProvider(this, CROSS_ACCOUNT_ZONE_DELEGATION_RESOURCE_TYPE); const role = iam.Role.fromRoleArn(this, 'cross-account-zone-delegation-handler-role', provider.roleArn); diff --git a/packages/aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.ts b/packages/aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.ts index e9b3fa59039c4..f0f7148f8801d 100644 --- a/packages/aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.ts +++ b/packages/aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.ts @@ -1,6 +1,5 @@ import * as fs from 'fs'; -import * as path from 'path'; import { kebab as toKebabCase } from 'case'; import { Construct } from 'constructs'; import { ISource, SourceConfig, Source } from './source'; @@ -12,6 +11,7 @@ import * as lambda from '../../aws-lambda'; import * as logs from '../../aws-logs'; import * as s3 from '../../aws-s3'; import * as cdk from '../../core'; +import { BucketDeploymentSingletonFunction } from '../../custom-resource-handlers/dist/aws-s3-deployment/bucket-deployment-provider.generated'; import { AwsCliLayer } from '../../lambda-layer-awscli'; // tag key has a limit of 128 characters @@ -316,18 +316,15 @@ export class BucketDeployment extends Construct { } const mountPath = `/mnt${accessPointPath}`; - const handler = new lambda.SingletonFunction(this, 'CustomResourceHandler', { + const handler = new BucketDeploymentSingletonFunction(this, 'CustomResourceHandler', { uuid: this.renderSingletonUuid(props.memoryLimit, props.ephemeralStorageSize, props.vpc), - code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', 'custom-resource-handlers', 'dist', 'aws-s3-deployment', 'bucket-deployment-handler')), layers: [new AwsCliLayer(this, 'AwsCliLayer')], - runtime: lambda.Runtime.PYTHON_3_9, environment: { ...props.useEfs ? { MOUNT_PATH: mountPath } : undefined, // Override the built-in CA bundle from the AWS CLI with the Lambda-curated one // This is necessary to make the CLI work in ADC regions. AWS_CA_BUNDLE: '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem', }, - handler: 'index.handler', lambdaPurpose: 'Custom::CDKBucketDeployment', timeout: cdk.Duration.minutes(15), role: props.role, diff --git a/packages/aws-cdk-lib/aws-s3/lib/bucket.ts b/packages/aws-cdk-lib/aws-s3/lib/bucket.ts index cb14f945fe69b..79e3fabddcbe8 100644 --- a/packages/aws-cdk-lib/aws-s3/lib/bucket.ts +++ b/packages/aws-cdk-lib/aws-s3/lib/bucket.ts @@ -1,5 +1,4 @@ import { EOL } from 'os'; -import * as path from 'path'; import { Construct } from 'constructs'; import { BucketPolicy } from './bucket-policy'; import { IBucketNotificationDestination } from './destination'; @@ -13,7 +12,6 @@ import * as iam from '../../aws-iam'; import * as kms from '../../aws-kms'; import { CustomResource, - CustomResourceProvider, Duration, FeatureFlags, Fn, @@ -27,9 +25,9 @@ import { Token, Tokenization, Annotations, - CustomResourceProviderRuntime, } from '../../core'; import { CfnReference } from '../../core/lib/private/cfn-reference'; +import { AutoDeleteObjectsProvider } from '../../custom-resource-handlers/dist/aws-s3/auto-delete-objects-provider.generated'; import * as cxapi from '../../cx-api'; import * as regionInformation from '../../region-info'; @@ -2466,10 +2464,8 @@ export class Bucket extends BucketBase { } private enableAutoDeleteObjects() { - const provider = CustomResourceProvider.getOrCreateProvider(this, AUTO_DELETE_OBJECTS_RESOURCE_TYPE, { - codeDirectory: path.join(__dirname, '..', '..', 'custom-resource-handlers', 'dist', 'aws-s3', 'auto-delete-objects-handler'), + const provider = AutoDeleteObjectsProvider.getOrCreateProvider(this, AUTO_DELETE_OBJECTS_RESOURCE_TYPE, { useCfnResponseWrapper: false, - runtime: CustomResourceProviderRuntime.NODEJS_18_X, description: `Lambda function for auto-deleting objects in ${this.bucketName} S3 bucket.`, }); diff --git a/packages/aws-cdk-lib/aws-ses/lib/receipt-rule.ts b/packages/aws-cdk-lib/aws-ses/lib/receipt-rule.ts index f0bf13f0008d1..28854d2e7e655 100644 --- a/packages/aws-cdk-lib/aws-ses/lib/receipt-rule.ts +++ b/packages/aws-cdk-lib/aws-ses/lib/receipt-rule.ts @@ -1,11 +1,10 @@ -import * as path from 'path'; import { Construct } from 'constructs'; import { IReceiptRuleAction } from './receipt-rule-action'; import { IReceiptRuleSet } from './receipt-rule-set'; import { CfnReceiptRule } from './ses.generated'; import * as iam from '../../aws-iam'; -import * as lambda from '../../aws-lambda'; import { Aws, IResource, Lazy, Resource } from '../../core'; +import { DropSpamSingletonFunction } from '../../custom-resource-handlers/dist/aws-ses/drop-spam-provider.generated'; /** * A receipt rule. @@ -171,10 +170,7 @@ export class DropSpamReceiptRule extends Construct { constructor(scope: Construct, id: string, props: DropSpamReceiptRuleProps) { super(scope, id); - const fn = new lambda.SingletonFunction(this, 'Function', { - runtime: lambda.Runtime.NODEJS_18_X, - handler: 'index.handler', - code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', 'custom-resource-handlers', 'dist', 'aws-ses', 'drop-spam-handler')), + const fn = new DropSpamSingletonFunction(this, 'Function', { uuid: '224e77f9-a32e-4b4d-ac32-983477abba16', }); diff --git a/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/emrcontainers/start-job-run.ts b/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/emrcontainers/start-job-run.ts index da544e8fd9e0e..7ade86376a2a3 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/emrcontainers/start-job-run.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/emrcontainers/start-job-run.ts @@ -1,12 +1,11 @@ -import * as path from 'path'; import { Construct } from 'constructs'; import * as iam from '../../../aws-iam'; -import * as lambda from '../../../aws-lambda'; import * as logs from '../../../aws-logs'; import * as s3 from '../../../aws-s3'; import * as sfn from '../../../aws-stepfunctions'; import { TaskInput } from '../../../aws-stepfunctions'; import * as cdk from '../../../core'; +import { RolePolicySingletonFunction } from '../../../custom-resource-handlers/dist/aws-stepfunctions-tasks/role-policy-provider.generated'; import * as cr from '../../../custom-resources'; import * as awscli from '../../../lambda-layer-awscli'; import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; @@ -350,11 +349,8 @@ export class EmrContainersStartJobRun extends sfn.TaskStateBase implements iam.I * Commands available through CLI: https://awscli.amazonaws.com/v2/documentation/api/latest/reference/emr-containers/index.html */ const cliLayer = new awscli.AwsCliLayer(this, 'awsclilayer'); - const shellCliLambda = new lambda.SingletonFunction(this, 'Call Update-Role-Trust-Policy', { + const shellCliLambda = new RolePolicySingletonFunction(this, 'Call Update-Role-Trust-Policy', { uuid: '8693BB64-9689-44B6-9AAF-B0CC9EB8757C', - runtime: lambda.Runtime.PYTHON_3_9, - handler: 'index.handler', - code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', '..', 'custom-resource-handlers', 'dist', 'aws-stepfunctions-tasks', 'role-policy-handler')), timeout: cdk.Duration.seconds(30), memorySize: 256, layers: [cliLayer], diff --git a/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/evaluate-expression.ts b/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/evaluate-expression.ts index ea3d55d711454..e6d742a2ed2a9 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/evaluate-expression.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/evaluate-expression.ts @@ -1,8 +1,8 @@ -import * as path from 'path'; import { Construct } from 'constructs'; import * as iam from '../../aws-iam'; import * as lambda from '../../aws-lambda'; import * as sfn from '../../aws-stepfunctions'; +import { EvalNodejsSingletonFunction } from '../../custom-resource-handlers/dist/aws-stepfunctions-tasks/eval-nodejs-provider.generated'; /** * Properties for EvaluateExpression @@ -116,13 +116,8 @@ function createEvalFn(runtime: lambda.Runtime | undefined, scope: Construct) { throw new Error(`The runtime ${runtime?.name} is currently not supported.`); } - return new lambda.SingletonFunction(scope, 'EvalFunction', { - runtime: runtime ?? lambda.Runtime.NODEJS_18_X, + return new EvalNodejsSingletonFunction(scope, 'EvalFunction', { uuid, - handler: 'index.handler', lambdaPurpose, - code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', 'custom-resource-handlers', 'dist', 'aws-stepfunctions-tasks', 'eval-nodejs-handler'), { - exclude: ['*.ts'], - }), }); } diff --git a/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts b/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts index dd33e7b6f0002..2d8317782806b 100644 --- a/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts +++ b/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts @@ -1,5 +1,4 @@ import * as crypto from 'crypto'; -import * as path from 'path'; import { Construct } from 'constructs'; import { Code } from './code'; import { Runtime } from './runtime'; @@ -11,6 +10,7 @@ import * as ec2 from '../../aws-ec2'; import * as iam from '../../aws-iam'; import * as s3 from '../../aws-s3'; import * as cdk from '../../core'; +import { AutoDeleteUnderlyingResourcesProvider } from '../../custom-resource-handlers/dist/aws-synthetics/auto-delete-underlying-resources-provider.generated'; const AUTO_DELETE_UNDERLYING_RESOURCES_RESOURCE_TYPE = 'Custom::SyntheticsAutoDeleteUnderlyingResources'; const AUTO_DELETE_UNDERLYING_RESOURCES_TAG = 'aws-cdk:auto-delete-underlying-resources'; @@ -321,10 +321,8 @@ export class Canary extends cdk.Resource implements ec2.IConnectable { } private cleanupUnderlyingResources() { - const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, AUTO_DELETE_UNDERLYING_RESOURCES_RESOURCE_TYPE, { - codeDirectory: path.join(__dirname, '..', '..', 'custom-resource-handlers', 'dist', 'aws-synthetics', 'auto-delete-underlying-resources-handler'), + const provider = AutoDeleteUnderlyingResourcesProvider.getOrCreateProvider(this, AUTO_DELETE_UNDERLYING_RESOURCES_RESOURCE_TYPE, { useCfnResponseWrapper: false, - runtime: cdk.CustomResourceProviderRuntime.NODEJS_18_X, description: `Lambda function for auto-deleting underlying resources created by ${this.canaryName}.`, policyStatements: [{ Effect: 'Allow', diff --git a/packages/aws-cdk-lib/core/lib/custom-resource-provider/cross-region-export-providers/export-reader-provider.ts b/packages/aws-cdk-lib/core/lib/custom-resource-provider/cross-region-export-providers/export-reader-provider.ts index 2d42125c928db..ca7efe1fe6ead 100644 --- a/packages/aws-cdk-lib/core/lib/custom-resource-provider/cross-region-export-providers/export-reader-provider.ts +++ b/packages/aws-cdk-lib/core/lib/custom-resource-provider/cross-region-export-providers/export-reader-provider.ts @@ -1,12 +1,11 @@ -import * as path from 'path'; import { Construct } from 'constructs'; import { SSM_EXPORT_PATH_PREFIX, ExportReaderCRProps, CrossRegionExports } from './types'; import { CfnResource } from '../../cfn-resource'; import { CustomResource } from '../../custom-resource'; +import { CrossRegionSsmReaderProvider } from '../../dist/core/cross-region-ssm-reader-provider.generated'; import { Lazy } from '../../lazy'; import { Intrinsic } from '../../private/intrinsic'; import { Stack } from '../../stack'; -import { CustomResourceProvider, CustomResourceProviderRuntime } from '../custom-resource-provider'; /** * Properties for an ExportReader @@ -35,9 +34,7 @@ export class ExportReader extends Construct { const stack = Stack.of(this); const resourceType = 'Custom::CrossRegionExportReader'; - const serviceToken = CustomResourceProvider.getOrCreate(this, resourceType, { - codeDirectory: path.join(__dirname, '..', '..', '..', '..', 'custom-resource-handlers', 'dist', 'core', 'cross-region-ssm-reader-handler'), - runtime: CustomResourceProviderRuntime.NODEJS_18_X, + const serviceToken = CrossRegionSsmReaderProvider.getOrCreate(this, resourceType, { policyStatements: [{ Effect: 'Allow', Resource: stack.formatArn({ diff --git a/packages/aws-cdk-lib/core/lib/custom-resource-provider/cross-region-export-providers/export-writer-provider.ts b/packages/aws-cdk-lib/core/lib/custom-resource-provider/cross-region-export-providers/export-writer-provider.ts index 0a55c1aeb8607..6c2d77b9e6bf5 100644 --- a/packages/aws-cdk-lib/core/lib/custom-resource-provider/cross-region-export-providers/export-writer-provider.ts +++ b/packages/aws-cdk-lib/core/lib/custom-resource-provider/cross-region-export-providers/export-writer-provider.ts @@ -1,16 +1,16 @@ -import * as path from 'path'; import { Construct } from 'constructs'; import { ExportReader } from './export-reader-provider'; import { CrossRegionExports, SSM_EXPORT_PATH_PREFIX, ExportWriterCRProps } from './types'; import { CfnDynamicReference, CfnDynamicReferenceService } from '../../cfn-dynamic-reference'; import { CustomResource } from '../../custom-resource'; +import { CrossRegionSsmWriterProvider } from '../../dist/core/cross-region-ssm-writer-provider.generated'; import { Lazy } from '../../lazy'; import { Intrinsic } from '../../private/intrinsic'; import { makeUniqueId } from '../../private/uniqueid'; import { Reference } from '../../reference'; import { Stack } from '../../stack'; import { Token } from '../../token'; -import { CustomResourceProviderRuntime, CustomResourceProvider, CustomResourceProviderProps } from '../custom-resource-provider'; +import { CustomResourceProviderOptions } from '../shared'; /** * Properties for an ExportWriter @@ -28,18 +28,17 @@ export interface ExportWriterProps { * Create our own CustomResourceProvider so that we can add a single policy * with a list of ARNs instead of having to create a separate policy statement per ARN. */ -class CRProvider extends CustomResourceProvider { - public static getOrCreateProvider(scope: Construct, uniqueid: string, props: CustomResourceProviderProps): CRProvider { +class CRProvider extends CrossRegionSsmWriterProvider { + public static getOrCreateProvider(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): CRProvider { const id = `${uniqueid}CustomResourceProvider`; const stack = Stack.of(scope); const provider = stack.node.tryFindChild(id) as CRProvider ?? new CRProvider(stack, id, props); - return provider; } private readonly resourceArns = new Set(); - constructor(scope: Construct, id: string, props: CustomResourceProviderProps) { + constructor(scope: Construct, id: string, props?: CustomResourceProviderOptions) { super(scope, id, props); this.addToRolePolicy({ Effect: 'Allow', @@ -99,10 +98,7 @@ export class ExportWriter extends Construct { const region = props.region ?? stack.region; const resourceType = 'Custom::CrossRegionExportWriter'; - this.provider = CRProvider.getOrCreateProvider(this, resourceType, { - codeDirectory: path.join(__dirname, '..', '..', '..', '..', 'custom-resource-handlers', 'dist', 'core', 'cross-region-ssm-writer-handler'), - runtime: CustomResourceProviderRuntime.NODEJS_18_X, - }); + this.provider = CRProvider.getOrCreateProvider(this, resourceType); this.addRegionToPolicy(region); diff --git a/packages/aws-cdk-lib/core/lib/custom-resource-provider/custom-resource-provider-base.ts b/packages/aws-cdk-lib/core/lib/custom-resource-provider/custom-resource-provider-base.ts new file mode 100644 index 0000000000000..ecd8f72f42d41 --- /dev/null +++ b/packages/aws-cdk-lib/core/lib/custom-resource-provider/custom-resource-provider-base.ts @@ -0,0 +1,281 @@ +import * as path from 'path'; +import { Construct } from 'constructs'; +import * as fs from 'fs-extra'; +import { CustomResourceProviderOptions, INLINE_CUSTOM_RESOURCE_CONTEXT } from './shared'; +import * as cxapi from '../../../cx-api'; +import { AssetStaging } from '../asset-staging'; +import { FileAssetPackaging } from '../assets'; +import { CfnResource } from '../cfn-resource'; +import { Duration } from '../duration'; +import { FileSystem } from '../fs'; +import { PolicySynthesizer, getPrecreatedRoleConfig } from '../helpers-internal'; +import { Lazy } from '../lazy'; +import { Size } from '../size'; +import { Stack } from '../stack'; +import { Token } from '../token'; + +const ENTRYPOINT_FILENAME = '__entrypoint__'; +const ENTRYPOINT_NODEJS_SOURCE = path.join(__dirname, '..', '..', '..', 'custom-resource-handlers', 'dist', 'core', 'nodejs-entrypoint-handler', 'index.js'); + +/** + * Initialization properties for `CustomResourceProviderBase` + */ +export interface CustomResourceProviderBaseProps extends CustomResourceProviderOptions { + /** + * A local file system directory with the provider's code. The code will be + * bundled into a zip asset and wired to the provider's AWS Lambda function. + */ + readonly codeDirectory: string; + + /** + * The AWS Lambda runtime and version name to use for the provider. + */ + readonly runtimeName: string; +} + +/** + * Base class for creating a custom resource provider + */ +export abstract class CustomResourceProviderBase extends Construct { + /** + * The hash of the lambda code backing this provider. Can be used to trigger updates + * on code changes, even when the properties of a custom resource remain unchanged. + */ + public get codeHash(): string { + if (!this._codeHash) { + throw new Error('This custom resource uses inlineCode: true and does not have a codeHash'); + } + return this._codeHash; + } + + private _codeHash?: string; + private policyStatements?: any[]; + private role?: CfnResource; + + /** + * The ARN of the provider's AWS Lambda function which should be used as the `serviceToken` when defining a custom + * resource. + */ + public readonly serviceToken: string; + + /** + * The ARN of the provider's AWS Lambda function role. + */ + public readonly roleArn: string; + + protected constructor(scope: Construct, id: string, props: CustomResourceProviderBaseProps) { + super(scope, id); + + const stack = Stack.of(scope); + + // verify we have an index file there + if (!fs.existsSync(path.join(props.codeDirectory, 'index.js'))) { + throw new Error(`cannot find ${props.codeDirectory}/index.js`); + } + + if (props.policyStatements) { + for (const statement of props.policyStatements) { + this.addToRolePolicy(statement); + } + } + + const { code, codeHandler, metadata } = this.createCodePropAndMetadata(props, stack); + + const config = getPrecreatedRoleConfig(this, `${this.node.path}/Role`); + const assumeRolePolicyDoc = [{ Action: 'sts:AssumeRole', Effect: 'Allow', Principal: { Service: 'lambda.amazonaws.com' } }]; + const managedPolicyArn = 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'; + + // need to initialize this attribute, but there should never be an instance + // where config.enabled=true && config.preventSynthesis=true + this.roleArn = ''; + if (config.enabled) { + // gives policyStatements a chance to resolve + this.node.addValidation({ + validate: () => { + PolicySynthesizer.getOrCreate(this).addRole(`${this.node.path}/Role`, { + missing: !config.precreatedRoleName, + roleName: config.precreatedRoleName ?? id+'Role', + managedPolicies: [{ managedPolicyArn: managedPolicyArn }], + policyStatements: this.policyStatements ?? [], + assumeRolePolicy: assumeRolePolicyDoc as any, + }); + return []; + }, + }); + this.roleArn = Stack.of(this).formatArn({ + region: '', + service: 'iam', + resource: 'role', + resourceName: config.precreatedRoleName, + }); + } + if (!config.preventSynthesis) { + this.role = new CfnResource(this, 'Role', { + type: 'AWS::IAM::Role', + properties: { + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: assumeRolePolicyDoc, + }, + ManagedPolicyArns: [ + { 'Fn::Sub': managedPolicyArn }, + ], + Policies: Lazy.any({ produce: () => this.renderPolicies() }), + }, + }); + this.roleArn = Token.asString(this.role.getAtt('Arn')); + } + + const timeout = props.timeout ?? Duration.minutes(15); + const memory = props.memorySize ?? Size.mebibytes(128); + + const handler = new CfnResource(this, 'Handler', { + type: 'AWS::Lambda::Function', + properties: { + Code: code, + Timeout: timeout.toSeconds(), + MemorySize: memory.toMebibytes(), + Handler: codeHandler, + Role: this.roleArn, + Runtime: props.runtimeName, + Environment: this.renderEnvironmentVariables(props.environment), + Description: props.description ?? undefined, + }, + }); + + if (this.role) { + handler.addDependency(this.role); + } + + if (metadata) { + Object.entries(metadata).forEach(([k, v]) => handler.addMetadata(k, v)); + } + + this.serviceToken = Token.asString(handler.getAtt('Arn')); + } + + /** + * Add an IAM policy statement to the inline policy of the + * provider's lambda function's role. + * + * **Please note**: this is a direct IAM JSON policy blob, *not* a `iam.PolicyStatement` + * object like you will see in the rest of the CDK. + * + * + * @example + * declare const myProvider: CustomResourceProvider; + * + * myProvider.addToRolePolicy({ + * Effect: 'Allow', + * Action: 's3:GetObject', + * Resource: '*', + * }); + */ + public addToRolePolicy(statement: any): void { + if (!this.policyStatements) { + this.policyStatements = []; + } + this.policyStatements.push(statement); + } + + private renderPolicies() { + if (!this.policyStatements) { + return undefined; + } + + const policies = [{ + PolicyName: 'Inline', + PolicyDocument: { + Version: '2012-10-17', + Statement: this.policyStatements, + }, + }]; + + return policies; + } + + private renderEnvironmentVariables(env?: { [key: string]: string }) { + if (!env || Object.keys(env).length === 0) { + return undefined; + } + + env = { ...env }; // Copy + + // Always use regional endpoints + env.AWS_STS_REGIONAL_ENDPOINTS = 'regional'; + + // Sort environment so the hash of the function used to create + // `currentVersion` is not affected by key order (this is how lambda does + // it) + const variables: { [key: string]: string } = {}; + const keys = Object.keys(env).sort(); + + for (const key of keys) { + variables[key] = env[key]; + } + + return { Variables: variables }; + } + + /** + * Returns the code property for the custom resource as well as any metadata. + * If the code is to be uploaded as an asset, the asset gets created in this function. + */ + private createCodePropAndMetadata(props: CustomResourceProviderBaseProps, stack: Stack): { + code: Code, + codeHandler: string, + metadata?: {[key: string]: string}, + } { + let codeHandler = 'index.handler'; + const inlineCode = this.node.tryGetContext(INLINE_CUSTOM_RESOURCE_CONTEXT); + if (!inlineCode) { + const stagingDirectory = FileSystem.mkdtemp('cdk-custom-resource'); + fs.copySync(props.codeDirectory, stagingDirectory, { filter: (src, _dest) => !src.endsWith('.ts') }); + + if (props.useCfnResponseWrapper ?? true) { + fs.copyFileSync(ENTRYPOINT_NODEJS_SOURCE, path.join(stagingDirectory, `${ENTRYPOINT_FILENAME}.js`)); + codeHandler = `${ENTRYPOINT_FILENAME}.handler`; + } + + const staging = new AssetStaging(this, 'Staging', { + sourcePath: stagingDirectory, + }); + + const assetFileName = staging.relativeStagedPath(stack); + + const asset = stack.synthesizer.addFileAsset({ + fileName: assetFileName, + sourceHash: staging.assetHash, + packaging: FileAssetPackaging.ZIP_DIRECTORY, + }); + + this._codeHash = staging.assetHash; + + return { + code: { + S3Bucket: asset.bucketName, + S3Key: asset.objectKey, + }, + codeHandler, + metadata: this.node.tryGetContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT) ? { + [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: assetFileName, + [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'Code', + } : undefined, + }; + } + + return { + code: { + ZipFile: fs.readFileSync(path.join(props.codeDirectory, 'index.js'), 'utf-8'), + }, + codeHandler, + }; + } +} + +export type Code = { + ZipFile: string, +} | { + S3Bucket: string, + S3Key: string, +}; diff --git a/packages/aws-cdk-lib/core/lib/custom-resource-provider/custom-resource-provider.ts b/packages/aws-cdk-lib/core/lib/custom-resource-provider/custom-resource-provider.ts index e2a15e3211263..1b0dd0e81a984 100644 --- a/packages/aws-cdk-lib/core/lib/custom-resource-provider/custom-resource-provider.ts +++ b/packages/aws-cdk-lib/core/lib/custom-resource-provider/custom-resource-provider.ts @@ -1,37 +1,13 @@ -import * as path from 'path'; import { Construct } from 'constructs'; -import * as fs from 'fs-extra'; -import * as cxapi from '../../../cx-api'; -import { AssetStaging } from '../asset-staging'; -import { FileAssetPackaging } from '../assets'; -import { CfnResource } from '../cfn-resource'; -import { Duration } from '../duration'; -import { FileSystem } from '../fs'; -import { PolicySynthesizer, getPrecreatedRoleConfig } from '../helpers-internal'; -import { Lazy } from '../lazy'; -import { Size } from '../size'; +import { CustomResourceProviderBase } from './custom-resource-provider-base'; +import { CustomResourceProviderOptions } from './shared'; import { Stack } from '../stack'; -import { Token } from '../token'; - -const ENTRYPOINT_FILENAME = '__entrypoint__'; -const ENTRYPOINT_NODEJS_SOURCE = path.join(__dirname, '..', '..', '..', 'custom-resource-handlers', 'dist', 'core', 'nodejs-entrypoint-handler', 'index.js'); -export const INLINE_CUSTOM_RESOURCE_CONTEXT = '@aws-cdk/core:inlineCustomResourceIfPossible'; /** * Initialization properties for `CustomResourceProvider`. * */ -export interface CustomResourceProviderProps { - /** - * Whether or not the cloudformation response wrapper (`nodejs-entrypoint.ts`) is used. - * If set to `true`, `nodejs-entrypoint.js` is bundled in the same asset as the custom resource - * and set as the entrypoint. If set to `false`, the custom resource provided is the - * entrypoint. - * - * @default - `true` if `inlineCode: false` and `false` otherwise. - */ - readonly useCfnResponseWrapper?: boolean; - +export interface CustomResourceProviderProps extends CustomResourceProviderOptions { /** * A local file system directory with the provider's code. The code will be * bundled into a zip asset and wired to the provider's AWS Lambda function. @@ -42,59 +18,6 @@ export interface CustomResourceProviderProps { * The AWS Lambda runtime and version to use for the provider. */ readonly runtime: CustomResourceProviderRuntime; - - /** - * A set of IAM policy statements to include in the inline policy of the - * provider's lambda function. - * - * **Please note**: these are direct IAM JSON policy blobs, *not* `iam.PolicyStatement` - * objects like you will see in the rest of the CDK. - * - * @default - no additional inline policy - * - * @example - * const provider = CustomResourceProvider.getOrCreateProvider(this, 'Custom::MyCustomResourceType', { - * codeDirectory: `${__dirname}/my-handler`, - * runtime: CustomResourceProviderRuntime.NODEJS_18_X, - * policyStatements: [ - * { - * Effect: 'Allow', - * Action: 's3:PutObject*', - * Resource: '*', - * } - * ], - * }); - */ - readonly policyStatements?: any[]; - - /** - * AWS Lambda timeout for the provider. - * - * @default Duration.minutes(15) - */ - readonly timeout?: Duration; - - /** - * The amount of memory that your function has access to. Increasing the - * function's memory also increases its CPU allocation. - * - * @default Size.mebibytes(128) - */ - readonly memorySize?: Size; - - /** - * Key-value pairs that are passed to Lambda as Environment - * - * @default - No environment variables. - */ - readonly environment?: { [key: string]: string }; - - /** - * A description of the function. - * - * @default - No description. - */ - readonly description?: string; } /** @@ -154,7 +77,7 @@ export enum CustomResourceProviderRuntime { * in that module a read, regardless of whether you end up using the Provider * class in there or this one. */ -export class CustomResourceProvider extends Construct { +export class CustomResourceProvider extends CustomResourceProviderBase { /** * Returns a stack-level singleton ARN (service token) for the custom resource * provider. @@ -187,255 +110,14 @@ export class CustomResourceProvider extends Construct { const stack = Stack.of(scope); const provider = stack.node.tryFindChild(id) as CustomResourceProvider ?? new CustomResourceProvider(stack, id, props); - return provider; } - /** - * The ARN of the provider's AWS Lambda function which should be used as the - * `serviceToken` when defining a custom resource. - * - * @example - * declare const myProvider: CustomResourceProvider; - * - * new CustomResource(this, 'MyCustomResource', { - * serviceToken: myProvider.serviceToken, - * properties: { - * myPropertyOne: 'one', - * myPropertyTwo: 'two', - * }, - * }); - */ - public readonly serviceToken: string; - - /** - * The ARN of the provider's AWS Lambda function role. - */ - public readonly roleArn: string; - - /** - * The hash of the lambda code backing this provider. Can be used to trigger updates - * on code changes, even when the properties of a custom resource remain unchanged. - */ - public get codeHash(): string { - if (!this._codeHash) { - throw new Error('This custom resource uses inlineCode: true and does not have a codeHash'); - } - return this._codeHash; - } - - private _codeHash?: string; - - private policyStatements?: any[]; - private _role?: CfnResource; - protected constructor(scope: Construct, id: string, props: CustomResourceProviderProps) { - super(scope, id); - - const stack = Stack.of(scope); - - // verify we have an index file there - if (!fs.existsSync(path.join(props.codeDirectory, 'index.js'))) { - throw new Error(`cannot find ${props.codeDirectory}/index.js`); - } - - const { code, codeHandler, metadata } = this.createCodePropAndMetadata(props, stack); - - if (props.policyStatements) { - for (const statement of props.policyStatements) { - this.addToRolePolicy(statement); - } - } - - const config = getPrecreatedRoleConfig(this, `${this.node.path}/Role`); - const assumeRolePolicyDoc = [{ Action: 'sts:AssumeRole', Effect: 'Allow', Principal: { Service: 'lambda.amazonaws.com' } }]; - const managedPolicyArn = 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'; - - // need to initialize this attribute, but there should never be an instance - // where config.enabled=true && config.preventSynthesis=true - this.roleArn = ''; - if (config.enabled) { - // gives policyStatements a chance to resolve - this.node.addValidation({ - validate: () => { - PolicySynthesizer.getOrCreate(this).addRole(`${this.node.path}/Role`, { - missing: !config.precreatedRoleName, - roleName: config.precreatedRoleName ?? id+'Role', - managedPolicies: [{ managedPolicyArn: managedPolicyArn }], - policyStatements: this.policyStatements ?? [], - assumeRolePolicy: assumeRolePolicyDoc as any, - }); - return []; - }, - }); - this.roleArn = Stack.of(this).formatArn({ - region: '', - service: 'iam', - resource: 'role', - resourceName: config.precreatedRoleName, - }); - } - if (!config.preventSynthesis) { - this._role = new CfnResource(this, 'Role', { - type: 'AWS::IAM::Role', - properties: { - AssumeRolePolicyDocument: { - Version: '2012-10-17', - Statement: assumeRolePolicyDoc, - }, - ManagedPolicyArns: [ - { 'Fn::Sub': managedPolicyArn }, - ], - Policies: Lazy.any({ produce: () => this.renderPolicies() }), - }, - }); - this.roleArn = Token.asString(this._role.getAtt('Arn')); - } - - const timeout = props.timeout ?? Duration.minutes(15); - const memory = props.memorySize ?? Size.mebibytes(128); - - const handler = new CfnResource(this, 'Handler', { - type: 'AWS::Lambda::Function', - properties: { - Code: code, - Timeout: timeout.toSeconds(), - MemorySize: memory.toMebibytes(), - Handler: codeHandler, - Role: this.roleArn, - Runtime: customResourceProviderRuntimeToString(props.runtime), - Environment: this.renderEnvironmentVariables(props.environment), - Description: props.description ?? undefined, - }, + super(scope, id, { + ...props, + runtimeName: customResourceProviderRuntimeToString(props.runtime), }); - - if (this._role) { - handler.addDependency(this._role); - } - - if (metadata) { - Object.entries(metadata).forEach(([k, v]) => handler.addMetadata(k, v)); - } - - this.serviceToken = Token.asString(handler.getAtt('Arn')); - } - - /** - * Returns the code property for the custom resource as well as any metadata. - * If the code is to be uploaded as an asset, the asset gets created in this function. - */ - private createCodePropAndMetadata(props: CustomResourceProviderProps, stack: Stack): { - code: Code, - codeHandler: string, - metadata?: {[key: string]: string}, - } { - let codeHandler = 'index.handler'; - const inlineCode = this.node.tryGetContext(INLINE_CUSTOM_RESOURCE_CONTEXT); - if (!inlineCode) { - const stagingDirectory = FileSystem.mkdtemp('cdk-custom-resource'); - fs.copySync(props.codeDirectory, stagingDirectory, { filter: (src, _dest) => !src.endsWith('.ts') }); - - if (props.useCfnResponseWrapper ?? true) { - fs.copyFileSync(ENTRYPOINT_NODEJS_SOURCE, path.join(stagingDirectory, `${ENTRYPOINT_FILENAME}.js`)); - codeHandler = `${ENTRYPOINT_FILENAME}.handler`; - } - - const staging = new AssetStaging(this, 'Staging', { - sourcePath: stagingDirectory, - }); - - const assetFileName = staging.relativeStagedPath(stack); - - const asset = stack.synthesizer.addFileAsset({ - fileName: assetFileName, - sourceHash: staging.assetHash, - packaging: FileAssetPackaging.ZIP_DIRECTORY, - }); - - this._codeHash = staging.assetHash; - - return { - code: { - S3Bucket: asset.bucketName, - S3Key: asset.objectKey, - }, - codeHandler, - metadata: this.node.tryGetContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT) ? { - [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: assetFileName, - [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'Code', - } : undefined, - }; - } - - return { - code: { - ZipFile: fs.readFileSync(path.join(props.codeDirectory, 'index.js'), 'utf-8'), - }, - codeHandler, - }; - } - - /** - * Add an IAM policy statement to the inline policy of the - * provider's lambda function's role. - * - * **Please note**: this is a direct IAM JSON policy blob, *not* a `iam.PolicyStatement` - * object like you will see in the rest of the CDK. - * - * - * @example - * declare const myProvider: CustomResourceProvider; - * - * myProvider.addToRolePolicy({ - * Effect: 'Allow', - * Action: 's3:GetObject', - * Resource: '*', - * }); - */ - public addToRolePolicy(statement: any): void { - if (!this.policyStatements) { - this.policyStatements = []; - } - this.policyStatements.push(statement); - } - - private renderPolicies() { - if (!this.policyStatements) { - return undefined; - } - - const policies = [{ - PolicyName: 'Inline', - PolicyDocument: { - Version: '2012-10-17', - Statement: this.policyStatements, - }, - }]; - - return policies; - } - - private renderEnvironmentVariables(env?: { [key: string]: string }) { - if (!env || Object.keys(env).length === 0) { - return undefined; - } - - env = { ...env }; // Copy - - // Always use regional endpoints - env.AWS_STS_REGIONAL_ENDPOINTS = 'regional'; - - // Sort environment so the hash of the function used to create - // `currentVersion` is not affected by key order (this is how lambda does - // it) - const variables: { [key: string]: string } = {}; - const keys = Object.keys(env).sort(); - - for (const key of keys) { - variables[key] = env[key]; - } - - return { Variables: variables }; } } @@ -452,10 +134,3 @@ function customResourceProviderRuntimeToString(x: CustomResourceProviderRuntime) return 'nodejs18.x'; } } - -type Code = { - ZipFile: string, -} | { - S3Bucket: string, - S3Key: string, -}; diff --git a/packages/aws-cdk-lib/core/lib/custom-resource-provider/index.ts b/packages/aws-cdk-lib/core/lib/custom-resource-provider/index.ts index 9ff36ec201b71..95841d8fa7525 100644 --- a/packages/aws-cdk-lib/core/lib/custom-resource-provider/index.ts +++ b/packages/aws-cdk-lib/core/lib/custom-resource-provider/index.ts @@ -1 +1,3 @@ -export * from './custom-resource-provider'; \ No newline at end of file +export * from './custom-resource-provider-base'; +export * from './custom-resource-provider'; +export * from './shared'; \ No newline at end of file diff --git a/packages/aws-cdk-lib/core/lib/custom-resource-provider/shared.ts b/packages/aws-cdk-lib/core/lib/custom-resource-provider/shared.ts new file mode 100644 index 0000000000000..0f5f13e250afc --- /dev/null +++ b/packages/aws-cdk-lib/core/lib/custom-resource-provider/shared.ts @@ -0,0 +1,72 @@ +import { Duration } from '../duration'; +import { Size } from '../size'; + +export const INLINE_CUSTOM_RESOURCE_CONTEXT = '@aws-cdk/core:inlineCustomResourceIfPossible'; + +/** + * Initialization options for custom resource providers + */ +export interface CustomResourceProviderOptions { + /** + * Whether or not the cloudformation response wrapper (`nodejs-entrypoint.ts`) is used. + * If set to `true`, `nodejs-entrypoint.js` is bundled in the same asset as the custom resource + * and set as the entrypoint. If set to `false`, the custom resource provided is the + * entrypoint. + * + * @default - `true` if `inlineCode: false` and `false` otherwise. + */ + readonly useCfnResponseWrapper?: boolean; + + /** + * A set of IAM policy statements to include in the inline policy of the + * provider's lambda function. + * + * **Please note**: these are direct IAM JSON policy blobs, *not* `iam.PolicyStatement` + * objects like you will see in the rest of the CDK. + * + * @default - no additional inline policy + * + * @example + * const provider = CustomResourceProvider.getOrCreateProvider(this, 'Custom::MyCustomResourceType', { + * codeDirectory: `${__dirname}/my-handler`, + * runtime: CustomResourceProviderRuntime.NODEJS_18_X, + * policyStatements: [ + * { + * Effect: 'Allow', + * Action: 's3:PutObject*', + * Resource: '*', + * } + * ], + * }); + */ + readonly policyStatements?: any[]; + + /** + * AWS Lambda timeout for the provider. + * + * @default Duration.minutes(15) + */ + readonly timeout?: Duration; + + /** + * The amount of memory that your function has access to. Increasing the + * function's memory also increases its CPU allocation. + * + * @default Size.mebibytes(128) + */ + readonly memorySize?: Size; + + /** + * Key-value pairs that are passed to Lambda as Environment + * + * @default - No environment variables. + */ + readonly environment?: { [key: string]: string }; + + /** + * A description of the function. + * + * @default - No description. + */ + readonly description?: string; +} diff --git a/packages/aws-cdk-lib/core/lib/private/cfn-utils-provider.ts b/packages/aws-cdk-lib/core/lib/private/cfn-utils-provider.ts index 16358347185d6..bab751211c138 100644 --- a/packages/aws-cdk-lib/core/lib/private/cfn-utils-provider.ts +++ b/packages/aws-cdk-lib/core/lib/private/cfn-utils-provider.ts @@ -1,18 +1,14 @@ -import * as path from 'path'; import { Construct } from 'constructs'; import { CfnUtilsResourceType } from './cfn-utils-provider/consts'; import { CustomResource } from '../custom-resource'; -import { CustomResourceProvider, CustomResourceProviderRuntime } from '../custom-resource-provider'; +import { CfnUtilsProvider as _CfnUtilsProvider } from '../dist/core/cfn-utils-provider.generated'; /** * A custom resource provider for CFN utilities such as `CfnJson`. */ export class CfnUtilsProvider extends Construct { public static getOrCreate(scope: Construct) { - return CustomResourceProvider.getOrCreate(scope, 'AWSCDKCfnUtilsProvider', { - runtime: CustomResourceProviderRuntime.NODEJS_18_X, - codeDirectory: path.join(__dirname, '..', '..', '..', 'custom-resource-handlers', 'dist', 'core', 'cfn-utils-provider'), - }); + return _CfnUtilsProvider.getOrCreate(scope, 'AWSCDKCfnUtilsProvider'); } } diff --git a/packages/aws-cdk-lib/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts b/packages/aws-cdk-lib/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts index aae56b70556cc..e9c0da466015e 100644 --- a/packages/aws-cdk-lib/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts +++ b/packages/aws-cdk-lib/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts @@ -1,11 +1,10 @@ -import * as path from 'path'; import { Construct } from 'constructs'; import * as ec2 from '../../../aws-ec2'; import * as iam from '../../../aws-iam'; -import * as lambda from '../../../aws-lambda'; import * as logs from '../../../aws-logs'; import * as cdk from '../../../core'; import { Annotations } from '../../../core'; +import { AwsCustomResourceSingletonFunction } from '../../../custom-resource-handlers/dist/custom-resources/aws-custom-resource-provider.generated'; import * as cxapi from '../../../cx-api'; import { awsSdkToIamAction } from '../helpers-internal/sdk-info'; @@ -446,10 +445,7 @@ export class AwsCustomResource extends Construct implements iam.IGrantable { this.props = props; - const provider = new lambda.SingletonFunction(this, 'Provider', { - code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', '..', 'custom-resource-handlers', 'dist', 'custom-resources', 'aws-custom-resource-handler')), - runtime: lambda.Runtime.NODEJS_18_X, - handler: 'index.handler', + const provider = new AwsCustomResourceSingletonFunction(this, 'Provider', { uuid: AwsCustomResource.PROVIDER_FUNCTION_UUID, lambdaPurpose: 'AWS', timeout: props.timeout || cdk.Duration.minutes(2), diff --git a/packages/aws-cdk-lib/pipelines/lib/private/application-security-check.ts b/packages/aws-cdk-lib/pipelines/lib/private/application-security-check.ts index 0308652adf868..b625862d9eea2 100644 --- a/packages/aws-cdk-lib/pipelines/lib/private/application-security-check.ts +++ b/packages/aws-cdk-lib/pipelines/lib/private/application-security-check.ts @@ -1,4 +1,3 @@ -import * as path from 'path'; import { Construct } from 'constructs'; import { CDKP_DEFAULT_CODEBUILD_IMAGE } from './default-codebuild-image'; import * as codebuild from '../../../aws-codebuild'; @@ -6,6 +5,7 @@ import * as cp from '../../../aws-codepipeline'; import * as iam from '../../../aws-iam'; import * as lambda from '../../../aws-lambda'; import { Duration, Tags } from '../../../core'; +import { ApproveLambdaFunction } from '../../../custom-resource-handlers/dist/pipelines/approve-lambda.generated'; /** * Properties for an ApplicationSecurityCheck @@ -57,10 +57,7 @@ export class ApplicationSecurityCheck extends Construct { includeResourceTypes: ['AWS::CodePipeline::Pipeline'], }); - this.preApproveLambda = new lambda.Function(this, 'CDKPipelinesAutoApprove', { - handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_18_X, - code: lambda.Code.fromAsset(path.resolve(__dirname, '..', '..', '..', 'custom-resource-handlers', 'dist', 'pipelines', 'approve-lambda')), + this.preApproveLambda = new ApproveLambdaFunction(this, 'CDKPipelinesAutoApprove', { timeout: Duration.minutes(5), }); diff --git a/packages/aws-cdk-lib/scripts/airlift-custom-resource-handlers.sh b/packages/aws-cdk-lib/scripts/airlift-custom-resource-handlers.sh index 875d5b764401b..86ac92a219c64 100755 --- a/packages/aws-cdk-lib/scripts/airlift-custom-resource-handlers.sh +++ b/packages/aws-cdk-lib/scripts/airlift-custom-resource-handlers.sh @@ -1,36 +1,46 @@ #!/bin/bash scriptdir=$(cd $(dirname $0) && pwd) -customresourcedir=$(node -p "path.dirname(require.resolve('@aws-cdk/custom-resource-handlers/package.json'))") awscdklibdir=${scriptdir}/.. +customresourcedir=$(node -p "path.dirname(require.resolve('@aws-cdk/custom-resource-handlers/package.json'))") -list_stable_custom_resources() { - for file in $customresourcedir/dist/*[^-alpha]/*/index.*; do - echo $file | rev | cut -d "/" -f 2-4 | rev - done +function airlift() { + # core needs to be airlifted directly to core to prevent circular dependencies + if [[ $1 != dist/core/nodejs-entrypoint-handler && ($1 = dist/core || $1 = dist/core/*) ]]; + then + mkdir -p $awscdklibdir/core/lib/$1 + cp $customresourcedir/$2 $awscdklibdir/core/lib/$1 + else + mkdir -p $awscdklibdir/custom-resource-handlers/$1 + cp $customresourcedir/$2 $awscdklibdir/custom-resource-handlers/$1 + fi } -list_dependent_python_modules() { - for file in $customresourcedir/dist/*[^-alpha]/*/*/__init__.py; do - echo $file | rev | cut -d "/" -f 2-5 | rev +function recurse() { + local dir=$1 + + for file in $dir/*; do + if [ -f $file ]; then + case $file in + $customresourcedir/dist/*[^-alpha]/*.generated.ts) + cr=$(echo $file | rev | cut -d "/" -f 2-3 | rev) + airlift $cr $cr/*.generated.ts + ;; + $customresourcedir/dist/*[^-alpha]/*/index.*) + cr=$(echo $file | rev | cut -d "/" -f 2-4 | rev) + airlift $cr $cr/index.* + ;; + $customresourcedir/dist/*[^-alpha]/*/*/__init__.py) + cr=$(echo $file | rev | cut -d "/" -f 2-5 | rev) + airlift $cr $cr/__init__.py + ;; + esac + fi + + if [ -d $file ]; then + recurse $file + fi done } -customresources=$(list_stable_custom_resources) -pythonmodules=$(list_dependent_python_modules) - -echo $customresources -echo $pythonmodules - -cd $awscdklibdir -mkdir -p $awscdklibdir/custom-resource-handlers - -for cr in $customresources; do - mkdir -p $awscdklibdir/custom-resource-handlers/$cr - cp $customresourcedir/$cr/index.* $awscdklibdir/custom-resource-handlers/$cr -done - -for pm in $pythonmodules; do - mkdir -p $awscdklibdir/custom-resource-handlers/$pm - cp $customresourcedir/$pm/__init__.py $awscdklibdir/custom-resource-handlers/$pm -done +recurse $customresourcedir/dist diff --git a/packages/aws-cdk-lib/triggers/lib/trigger.ts b/packages/aws-cdk-lib/triggers/lib/trigger.ts index 3bc03ef35ad07..d3b75e12559e1 100644 --- a/packages/aws-cdk-lib/triggers/lib/trigger.ts +++ b/packages/aws-cdk-lib/triggers/lib/trigger.ts @@ -1,7 +1,7 @@ -import { join } from 'path'; import { Construct, IConstruct, Node } from 'constructs'; import * as lambda from '../../aws-lambda'; -import { CustomResource, CustomResourceProvider, CustomResourceProviderRuntime, Duration } from '../../core'; +import { CustomResource, Duration } from '../../core'; +import { TriggerProvider } from '../../custom-resource-handlers/dist/triggers/trigger-provider.generated'; /** * Interface for triggers. @@ -114,10 +114,7 @@ export class Trigger extends Construct implements ITrigger { constructor(scope: Construct, id: string, props: TriggerProps) { super(scope, id); - const provider = CustomResourceProvider.getOrCreateProvider(this, 'AWSCDK.TriggerCustomResourceProvider', { - runtime: CustomResourceProviderRuntime.NODEJS_18_X, - codeDirectory: join(__dirname, '..', '..', 'custom-resource-handlers', 'dist', 'triggers', 'lambda'), - }); + const provider = TriggerProvider.getOrCreateProvider(this, 'AWSCDK.TriggerCustomResourceProvider'); provider.addToRolePolicy({ Effect: 'Allow',