From df9727c272e3d6183063c60902e4ba01abee392c Mon Sep 17 00:00:00 2001 From: Kendra Neil <53584728+TheRealAmazonKendra@users.noreply.github.com> Date: Wed, 5 Jun 2024 19:27:16 -0700 Subject: [PATCH] ruining colin's life --- .../lib/custom-resources-framework/README.md | 2 +- .../callable-expr.ts | 35 ++ .../lib/custom-resources-framework/classes.ts | 185 +++---- .../lib/custom-resources-framework/config.ts | 10 +- .../custom-resources-framework/framework.ts | 41 +- .../module-importer.ts | 73 +++ .../lib/custom-resources-framework/modules.ts | 61 ++- .../custom-resource-provider-core.ts | 32 ++ .../node-runtime/custom-resource-provider.ts | 31 ++ .../expected/node-runtime/function.ts | 15 + .../node-runtime/singleton-function.ts | 39 ++ .../custom-resource-provider-core.ts | 2 +- .../custom-resource-provider.ts | 2 +- .../expected/{ => python-runtime}/function.ts | 2 +- .../singleton-function.ts | 2 +- .../singleton-function-eval-nodejs.ts | 2 +- .../framework.test.ts | 127 ++++- .../my-handler/index.py | 0 .../lib/assertions/providers/provider.ts | 14 +- .../test/dns-validated-certificate.test.ts | 10 +- .../aws-cdk-lib/aws-lambda/lib/runtime.ts | 14 +- .../aws-lambda/test/function.test.ts | 193 +++++++ .../aws-cdk-lib/aws-logs/lib/log-retention.ts | 2 +- .../aws-logs/test/log-retention.test.ts | 10 +- .../lib/evaluate-expression.ts | 1 + .../test/evaluate-expression.test.ts | 16 +- .../custom-resource-provider.ts | 15 + .../custom-resource-provider.test.ts | 227 ++++++++- .../export-writer-provider.test.ts | 476 +++++++++++++++++- .../lib/provider-framework/provider.ts | 2 +- .../region-info/build-tools/fact-tables.ts | 8 + .../build-tools/generate-static-data.ts | 3 + packages/aws-cdk-lib/region-info/lib/fact.ts | 5 + 33 files changed, 1468 insertions(+), 189 deletions(-) create mode 100644 packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/callable-expr.ts create mode 100644 packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/module-importer.ts create mode 100644 packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/node-runtime/custom-resource-provider-core.ts create mode 100644 packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/node-runtime/custom-resource-provider.ts create mode 100644 packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/node-runtime/function.ts create mode 100644 packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/node-runtime/singleton-function.ts rename packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/{ => python-runtime}/custom-resource-provider-core.ts (97%) rename packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/{ => python-runtime}/custom-resource-provider.ts (97%) rename packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/{ => python-runtime}/function.ts (91%) rename packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/{ => python-runtime}/singleton-function.ts (96%) create mode 100644 packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/my-handler/index.py 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 index f18afce9a4fb0..9dbf6c0addfb8 100644 --- 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 @@ -12,7 +12,7 @@ This framework allows for the creation of three component types: 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. +- `runtime` - the runtime that is compatible with the framework component's source code. This is an optional property with a default node runtime that will be the latest available node runtime in the `Stack` deployment region. In general, you should not configure this property unless a `runtime` override is absolutely necessary. - `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. diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/callable-expr.ts b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/callable-expr.ts new file mode 100644 index 0000000000000..7702972e2d203 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/callable-expr.ts @@ -0,0 +1,35 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { $E, Expression, IScope, ThingSymbol, expr } from '@cdklabs/typewriter'; + +/** + * A class representing an expression proxy which builds a JavaScript object that + * will mirror the JavaScript operations done to it in an expression tree. + */ +export class CallableExpr { + /** + * Creates a new CallableExpr that can be called with the specified name. + */ + public static fromName(scope: IScope, name: string) { + return new CallableExpr(scope, name); + } + + private readonly expr: Expression; + + private constructor(readonly scope: IScope, private readonly name: string) { + this.expr = $E(expr.sym(new ThingSymbol(name, scope))); + } + + /** + * Calls the expression proxy with the provided arguments. + */ + public call(...args: Expression[]) { + return this.expr.call(...args); + } + + /** + * The name of the expression proxy. + */ + public toString() { + return this.name; + } +} 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 index 19311d5120a70..72da0c2ecd5fe 100644 --- 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 @@ -5,7 +5,6 @@ import { expr, Type, Splat, - ExternalModule, PropertySpec, InterfaceSpec, InterfaceType, @@ -13,16 +12,16 @@ import { MemberVisibility, SuperInitializer, Expression, + ClassSpec, + $T, } from '@cdklabs/typewriter'; import { Runtime } from './config'; import { HandlerFrameworkModule } from './framework'; import { + PATH_MODULE, CONSTRUCTS_MODULE, LAMBDA_MODULE, CORE_MODULE, - CORE_INTERNAL_STACK, - CORE_INTERNAL_CR_PROVIDER, - PATH_MODULE, } from './modules'; import { toLambdaRuntime } from './utils/framework-utils'; @@ -70,14 +69,22 @@ export interface HandlerFrameworkClassProps { readonly codeDirectory: string; /** - * The runtime environment for the framework component. + * The name of the method within your code that framework component calls. */ - readonly runtime: Runtime; + readonly handler: string; /** - * The name of the method within your code that framework component calls. + * The runtime environment for the framework component. + * + * @default - the latest Lambda runtime available in the region. */ - readonly handler: string; + readonly runtime?: Runtime; +} + +interface BuildRuntimePropertyOptions { + readonly runtime?: Runtime; + readonly isCustomResourceProvider?: boolean; + readonly isEvalNodejsProvider?: boolean; } export abstract class HandlerFrameworkClass extends ClassType { @@ -86,8 +93,6 @@ export abstract class HandlerFrameworkClass extends ClassType { */ 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, @@ -95,13 +100,19 @@ export abstract class HandlerFrameworkClass extends ClassType { export: true, }); - this.importExternalModulesInto(scope); + if (scope.isAlphaModule) { + scope.registerImport(LAMBDA_MODULE, { fromLocation: 'aws-cdk-lib/aws-lambda' }); + } else { + scope.registerImport(LAMBDA_MODULE); + } const superProps = new ObjectLiteral([ new Splat(expr.ident('props')), - ['code', expr.directCode(`lambda.Code.fromAsset(path.join(__dirname, '${props.codeDirectory}'))`)], + ['code', $T(LAMBDA_MODULE.Code).fromAsset( + PATH_MODULE.join.call(expr.directCode(`__dirname, '${props.codeDirectory}'`)), + )], ['handler', expr.lit(props.handler)], - ['runtime', expr.directCode(toLambdaRuntime(props.runtime))], + ['runtime', this.buildRuntimeProperty(scope, { runtime: props.runtime })], ]); this.buildConstructor({ constructorPropsType: LAMBDA_MODULE.FunctionOptions, @@ -118,8 +129,6 @@ export abstract class HandlerFrameworkClass extends ClassType { */ 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, @@ -127,9 +136,15 @@ export abstract class HandlerFrameworkClass extends ClassType { export: true, }); + if (scope.isAlphaModule) { + scope.registerImport(LAMBDA_MODULE, { fromLocation: 'aws-cdk-lib/aws-lambda' }); + } else { + scope.registerImport(LAMBDA_MODULE); + } + const isEvalNodejsProvider = this.fqn.includes('eval-nodejs-provider'); - this.importExternalModulesInto(scope); + scope.registerImport(LAMBDA_MODULE); const uuid: PropertySpec = { name: 'uuid', @@ -181,9 +196,11 @@ export abstract class HandlerFrameworkClass extends ClassType { const superProps = new ObjectLiteral([ new Splat(expr.ident('props')), - ['code', expr.directCode(`lambda.Code.fromAsset(path.join(__dirname, '${props.codeDirectory}'))`)], + ['code', $T(LAMBDA_MODULE.Code).fromAsset( + PATH_MODULE.join.call(expr.directCode(`__dirname, '${props.codeDirectory}'`)), + )], ['handler', expr.lit(props.handler)], - ['runtime', expr.directCode(`${isEvalNodejsProvider ? 'props.runtime ?? ' : ''}${toLambdaRuntime(props.runtime)}`)], + ['runtime', this.buildRuntimeProperty(scope, { runtime: props.runtime, isEvalNodejsProvider })], ]); this.buildConstructor({ constructorPropsType: _interface.type, @@ -199,23 +216,34 @@ export abstract class HandlerFrameworkClass extends ClassType { */ 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, + extends: CORE_MODULE.CustomResourceProviderBase, export: true, }); - if (scope.coreInternal) { - this.externalModules.push(...[CORE_INTERNAL_STACK, CORE_INTERNAL_CR_PROVIDER]); + if (scope.isCoreInternal) { + scope.registerImport(CORE_MODULE, { + targets: [CORE_MODULE.Stack], + fromLocation: '../../stack', + }); + scope.registerImport(CORE_MODULE, { + targets: [ + CORE_MODULE.CustomResourceProviderBase, + CORE_MODULE.CustomResourceProviderOptions, + ], + fromLocation: '../../custom-resource-provider', + }); } else { - this.externalModules.push(CORE_MODULE); + scope.registerImport(CORE_MODULE, { + targets: [ + CORE_MODULE.Stack, + CORE_MODULE.CustomResourceProviderBase, + CORE_MODULE.CustomResourceProviderOptions, + ], + }); } - this.importExternalModulesInto(scope); const getOrCreateMethod = this.addMethod({ name: 'getOrCreate', @@ -235,9 +263,7 @@ export abstract class HandlerFrameworkClass extends ClassType { }); getOrCreateMethod.addParameter({ name: 'props', - type: scope.coreInternal - ? CORE_INTERNAL_CR_PROVIDER.CustomResourceProviderOptions - : CORE_MODULE.CustomResourceProviderOptions, + type: CORE_MODULE.CustomResourceProviderOptions, optional: true, }); getOrCreateMethod.addBody( @@ -252,7 +278,7 @@ export abstract class HandlerFrameworkClass extends ClassType { summary: 'Returns a stack-level singleton for the custom resource provider.', }, }); - getOrCreateProviderMethod.addParameter({ + const _scope = getOrCreateProviderMethod.addParameter({ name: 'scope', type: CONSTRUCTS_MODULE.Construct, }); @@ -262,27 +288,26 @@ export abstract class HandlerFrameworkClass extends ClassType { }); getOrCreateProviderMethod.addParameter({ name: 'props', - type: scope.coreInternal - ? CORE_INTERNAL_CR_PROVIDER.CustomResourceProviderOptions - : CORE_MODULE.CustomResourceProviderOptions, + type: 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('stack'), $T(CORE_MODULE.Stack).of(expr.directCode(_scope.spec.name))), 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)], + ['codeDirectory', PATH_MODULE.join.call(expr.directCode(`__dirname, '${props.codeDirectory}'`))], + ['runtimeName', this.buildRuntimeProperty(scope, { + runtime: props.runtime, + isCustomResourceProvider: true, + })], ]); this.buildConstructor({ - constructorPropsType: scope.coreInternal - ? CORE_INTERNAL_CR_PROVIDER.CustomResourceProviderOptions - : CORE_MODULE.CustomResourceProviderOptions, + constructorPropsType: CORE_MODULE.CustomResourceProviderOptions, superProps, constructorVisbility: MemberVisibility.Private, optionalConstructorProps: true, @@ -291,54 +316,12 @@ export abstract class HandlerFrameworkClass extends ClassType { })(); } - /** - * 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; - } - } + protected constructor(scope: HandlerFrameworkModule, spec: ClassSpec) { + super(scope, spec); + scope.registerImport(PATH_MODULE); + scope.registerImport(CONSTRUCTS_MODULE, { + targets: [CONSTRUCTS_MODULE.Construct], + }); } private getOrCreateInterface(scope: HandlerFrameworkModule, spec: InterfaceSpec) { @@ -373,4 +356,30 @@ export abstract class HandlerFrameworkClass extends ClassType { const superInitializerArgs: Expression[] = [scope, id, props.superProps]; init.addBody(new SuperInitializer(...superInitializerArgs)); } + + private buildRuntimeProperty(scope: HandlerFrameworkModule, options: BuildRuntimePropertyOptions = {}) { + const { runtime, isCustomResourceProvider, isEvalNodejsProvider } = options; + + if (runtime) { + return isCustomResourceProvider ? expr.lit(runtime) : expr.directCode(toLambdaRuntime(runtime)); + } + + if (isCustomResourceProvider) { + scope.registerImport(CORE_MODULE, { + targets: [CORE_MODULE.determineLatestNodeRuntimeName], + fromLocation: scope.isCoreInternal + ? '../../custom-resource-provider' + : '../../../core', + }); + } + + const _scope = expr.ident('scope'); + const call = isCustomResourceProvider + ? CORE_MODULE.determineLatestNodeRuntimeName.call(_scope) + : LAMBDA_MODULE.determineLatestNodeRuntime.call(_scope); + + return isEvalNodejsProvider + ? expr.cond(expr.directCode('props.runtime'), expr.directCode('props.runtime'), call) + : call; + } } 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 index 24436234b3678..c61d7686170e1 100644 --- 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 @@ -88,10 +88,16 @@ export type HandlerFrameworkConfig = { [module: string]: { [identifier: string]: export const config: HandlerFrameworkConfig = { 'aws-amplify-alpha': { - 'asset-deployment-handler': [ + 'asset-deployment-provider': [ { - type: ComponentType.NO_OP, + type: ComponentType.FUNCTION, + sourceCode: path.resolve(__dirname, '..', 'aws-amplify-alpha', 'asset-deployment-handler', 'index.ts'), + handler: 'index.onEvent', + }, + { + type: ComponentType.FUNCTION, sourceCode: path.resolve(__dirname, '..', 'aws-amplify-alpha', 'asset-deployment-handler', 'index.ts'), + handler: 'index.isComplete', }, ], }, 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 index b77942905fdc3..09af23071ac0e 100644 --- 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 @@ -1,28 +1,30 @@ /* eslint-disable import/no-extraneous-dependencies */ -import { ExternalModule, InterfaceType, Module, TypeScriptRenderer } from '@cdklabs/typewriter'; +import { InterfaceType, Module, TypeScriptRenderer } from '@cdklabs/typewriter'; import * as fs from 'fs-extra'; import { HandlerFrameworkClass, HandlerFrameworkClassProps } from './classes'; -import { ComponentType, ComponentProps, Runtime } from './config'; +import { ComponentType, ComponentProps } from './config'; +import { ModuleImportOptions, ModuleImporter } from './module-importer'; +import { ImportableModule } from './modules'; 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 importer = new ModuleImporter(); 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; + public readonly isCoreInternal: boolean; + + /** + * Whether the module being generated will be part of an alpha module. + */ + public readonly isAlphaModule: boolean; /** - * Whether the module contains handler framework components. + * Whether the module contains provider framework components. */ public get hasComponents() { return this._hasComponents; @@ -30,7 +32,8 @@ export class HandlerFrameworkModule extends Module { public constructor(fqn: string) { super(fqn); - this.coreInternal = fqn.includes('core'); + this.isCoreInternal = fqn.includes('core'); + this.isAlphaModule = fqn.includes('alpha'); } /** @@ -50,7 +53,7 @@ export class HandlerFrameworkModule extends Module { name, handler, codeDirectory, - runtime: component.runtime ?? HandlerFrameworkModule.DEFAULT_RUNTIME, + runtime: component.runtime, }; switch (component.type) { @@ -73,21 +76,15 @@ export class HandlerFrameworkModule extends Module { * Render module with components into an output file. */ public renderTo(file: string) { + this.importer.importModulesInto(this); 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. + * Register an external module to be imported into this module. */ - public hasExternalModule(module: ExternalModule) { - return this.externalModules.has(module.fqn); + public registerImport(module: ImportableModule, options: ModuleImportOptions = {}) { + this.importer.registerImport(module, options); } /** diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/module-importer.ts b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/module-importer.ts new file mode 100644 index 0000000000000..24ef1ea4903d4 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/module-importer.ts @@ -0,0 +1,73 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { Module, Type } from '@cdklabs/typewriter'; +import { CallableExpr } from './callable-expr'; +import { ImportableModule } from './modules'; + +type Target = CallableExpr | Type; +type ModuleImport = { [fqn: string]: { module: ImportableModule; targets: Set; fromLocation?: string } }; + +/** + * Options used to import an external module. + */ +export interface ModuleImportOptions { + /** + * Targets to import from the external module. Specifying targets for a module will + * result in a selective import. + * + * @default - all targets will be imported from the external module + */ + readonly targets?: Target[]; + + /** + * Override the location the module is imported from. + * + * @default - the module will be imported from its default location + */ + readonly fromLocation?: string; +} + +/** + * A class used to manage external module imports for a target module. + */ +export class ModuleImporter { + private readonly imports: ModuleImport = {}; + + /** + * Register an external module to be imported. + */ + public registerImport(module: ImportableModule, options: ModuleImportOptions = {}) { + const fqn = options.fromLocation ?? module.fqn; + const targets = options.targets ?? []; + const registeredTargets = this.imports[fqn]?.targets ?? new Set(); + + if (this.imports.hasOwnProperty(fqn)) { + if (registeredTargets.size > 0 && targets.length > 0) { + this.imports[fqn].targets = new Set([...registeredTargets, ...targets]); + return; + } + } + this.imports[fqn] = { + module, + targets: new Set([...registeredTargets, ...targets]), + fromLocation: options.fromLocation, + }; + } + + /** + * Import all registered modules into the target module. + */ + public importModulesInto(scope: Module) { + for (const { module, targets, fromLocation } of Object.values(this.imports)) { + this.importModuleInto(scope, module, targets, fromLocation); + } + } + + private importModuleInto(scope: Module, module: ImportableModule, targets: Set, fromLocation?: string) { + if (targets.size > 0) { + const _targets = Array.from(targets).map(target => target.toString()); + module.importSelective(scope, _targets, { fromLocation }); + return; + } + module.import(scope, module.importAs, { fromLocation }); + } +} 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 index 591306c7b9439..932824b2cfaca 100644 --- 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 @@ -1,61 +1,80 @@ /* eslint-disable import/no-extraneous-dependencies */ import { ExternalModule, Type } from '@cdklabs/typewriter'; +import { CallableExpr } from './callable-expr'; + +/** + * A class representing an external module that can be imported into a target module. + */ +export abstract class ImportableModule extends ExternalModule { + /** + * The name to import all targets in the module as. + */ + public abstract importAs: string; +} + +class PathModule extends ImportableModule { + public readonly join = CallableExpr.fromName(this, 'join'); + + public readonly importAs = 'path'; -class PathModule extends ExternalModule { public constructor() { super('path'); } } -class ConstructsModule extends ExternalModule { +class ConstructsModule extends ImportableModule { public readonly Construct = Type.fromName(this, 'Construct'); + public readonly importAs = 'constructs'; + public constructor() { super('constructs'); } } -class CoreModule extends ExternalModule { +class CoreModule extends ImportableModule { public readonly Stack = Type.fromName(this, 'Stack'); public readonly CustomResourceProviderBase = Type.fromName(this, 'CustomResourceProviderBase'); public readonly CustomResourceProviderOptions = Type.fromName(this, 'CustomResourceProviderOptions'); + public readonly determineLatestNodeRuntimeName = CallableExpr.fromName(this, 'determineLatestNodeRuntimeName'); + + public readonly importAs = 'cdk'; + public constructor() { super('../../../core'); } } -class CoreInternalStack extends ExternalModule { - public readonly Stack = Type.fromName(this, 'Stack'); +class LambdaModule extends ImportableModule { + public readonly Function = Type.fromName(this, 'Function'); + public readonly SingletonFunction = Type.fromName(this, 'SingletonFunction'); + public readonly FunctionOptions = Type.fromName(this, 'FunctionOptions'); + public readonly Runtime = Type.fromName(this, 'Runtime'); + public readonly RuntimeFamily = Type.fromName(this, 'RuntimeFamily'); + public readonly Code = Type.fromName(this, 'Code'); - public constructor() { - super('../../stack'); - } -} + public readonly determineLatestNodeRuntime = CallableExpr.fromName(this, 'determineLatestNodeRuntime'); -class CoreInternalCustomResourceProvider extends ExternalModule { - public readonly CustomResourceProviderBase = Type.fromName(this, 'CustomResourceProviderBase'); - public readonly CustomResourceProviderOptions = Type.fromName(this, 'CustomResourceProviderOptions'); + public readonly importAs = 'lambda'; public constructor() { - super('../../custom-resource-provider'); + super('../../../aws-lambda'); } } -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 readonly Runtime = Type.fromName(this, 'Runtime'); +class RegionInfoModule extends ImportableModule { + public readonly FactName = Type.fromName(this, 'FactName'); + + public readonly importAs = 'region'; public constructor() { - super('../../../aws-lambda'); + super('../../../region-info'); } } 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(); +export const REGION_INFO_MODULE = new RegionInfoModule(); diff --git a/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/node-runtime/custom-resource-provider-core.ts b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/node-runtime/custom-resource-provider-core.ts new file mode 100644 index 0000000000000..7d233b55451bd --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/node-runtime/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, determineLatestNodeRuntimeName } 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": determineLatestNodeRuntimeName(scope) + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/node-runtime/custom-resource-provider.ts b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/node-runtime/custom-resource-provider.ts new file mode 100644 index 0000000000000..8c45d66c40e93 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/node-runtime/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, determineLatestNodeRuntimeName } 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": determineLatestNodeRuntimeName(scope) + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/node-runtime/function.ts b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/node-runtime/function.ts new file mode 100644 index 0000000000000..33e1a8f4a8766 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/node-runtime/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.determineLatestNodeRuntime(scope) + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/node-runtime/singleton-function.ts b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/node-runtime/singleton-function.ts new file mode 100644 index 0000000000000..6f5f5e4897fa6 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/node-runtime/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.determineLatestNodeRuntime(scope) + }); + } +} + +/** + * 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/expected/custom-resource-provider-core.ts b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/python-runtime/custom-resource-provider-core.ts similarity index 97% rename from packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/custom-resource-provider-core.ts rename to packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/python-runtime/custom-resource-provider-core.ts index 84353b7b77372..46b3b7bdf532c 100644 --- 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/python-runtime/custom-resource-provider-core.ts @@ -26,7 +26,7 @@ export class TestProvider extends CustomResourceProviderBase { super(scope, id, { ...props, "codeDirectory": path.join(__dirname, 'my-handler'), - "runtimeName": "nodejs18.x" + "runtimeName": "python3.10" }); } } \ 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/python-runtime/custom-resource-provider.ts similarity index 97% rename from packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/custom-resource-provider.ts rename to packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/python-runtime/custom-resource-provider.ts index dfecaf43427eb..0ab4377ec0ebd 100644 --- 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/python-runtime/custom-resource-provider.ts @@ -25,7 +25,7 @@ export class TestProvider extends CustomResourceProviderBase { super(scope, id, { ...props, "codeDirectory": path.join(__dirname, 'my-handler'), - "runtimeName": "nodejs18.x" + "runtimeName": "python3.10" }); } } \ 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/python-runtime/function.ts similarity index 91% rename from packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/function.ts rename to packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/python-runtime/function.ts index 8907e5f2c0540..00f32433d5a87 100644 --- 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/python-runtime/function.ts @@ -9,7 +9,7 @@ export class TestFunction extends lambda.Function { ...props, "code": lambda.Code.fromAsset(path.join(__dirname, 'my-handler')), "handler": "index.handler", - "runtime": lambda.Runtime.NODEJS_18_X + "runtime": lambda.Runtime.PYTHON_3_10 }); } } \ 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/python-runtime/singleton-function.ts similarity index 96% rename from packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/singleton-function.ts rename to packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/python-runtime/singleton-function.ts index 7aa1d645e3f71..c8326cbeece52 100644 --- 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/python-runtime/singleton-function.ts @@ -9,7 +9,7 @@ export class TestSingletonFunction extends lambda.SingletonFunction { ...props, "code": lambda.Code.fromAsset(path.join(__dirname, 'my-handler')), "handler": "index.handler", - "runtime": lambda.Runtime.NODEJS_18_X + "runtime": lambda.Runtime.PYTHON_3_10 }); } } diff --git a/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/singleton-function-eval-nodejs.ts b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/singleton-function-eval-nodejs.ts index d05eb9772f902..8866f7f2301e1 100644 --- a/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/singleton-function-eval-nodejs.ts +++ b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/expected/singleton-function-eval-nodejs.ts @@ -9,7 +9,7 @@ export class EvalNodejsSingletonFunction extends lambda.SingletonFunction { ...props, "code": lambda.Code.fromAsset(path.join(__dirname, 'my-handler')), "handler": "index.handler", - "runtime": props.runtime ?? lambda.Runtime.NODEJS_18_X + "runtime": (props.runtime ? props.runtime : lambda.determineLatestNodeRuntime(scope)) }); } } 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 index 116495b11baa0..db83742859a5b 100644 --- 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 @@ -5,9 +5,8 @@ import { ComponentProps, ComponentType, Runtime } from '../../lib/custom-resourc 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'); +const sourceCodeTs = path.resolve(__dirname, 'my-handler', 'index.ts'); +const sourceCodePy = path.resolve(__dirname, 'my-handler', 'index.py'); describe('framework', () => { let tmpDir: string; @@ -20,17 +19,17 @@ describe('framework', () => { const module = new HandlerFrameworkModule('cdk-testing/test-provider'); const component: ComponentProps = { type: ComponentType.FUNCTION, - sourceCode, + sourceCode: sourceCodeTs, }; - const outfile = calculateOutfile(sourceCode); - module.build(component, path.dirname(outfile).split('/').pop() ?? 'dist'); + const outfile = calculateOutfile(sourceCodeTs); + module.build(component, calculateCodeDirectory(path.dirname(outfile))); // 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'); + const expected = fs.readFileSync(path.resolve(__dirname, 'expected', 'node-runtime', 'function.ts'), 'utf-8'); expect(result).toContain(expected); }); @@ -39,36 +38,36 @@ describe('framework', () => { const module = new HandlerFrameworkModule('cdk-testing/test-provider'); const component: ComponentProps = { type: ComponentType.SINGLETON_FUNCTION, - sourceCode, + sourceCode: sourceCodeTs, }; - const outfile = calculateOutfile(sourceCode); - module.build(component, path.dirname(outfile).split('/').pop() ?? 'dist'); + const outfile = calculateOutfile(sourceCodeTs); + module.build(component, calculateCodeDirectory(path.dirname(outfile))); // 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'); + const expected = fs.readFileSync(path.resolve(__dirname, 'expected', 'node-runtime', 'singleton-function.ts'), 'utf-8'); expect(result).toContain(expected); }); - test('can codegen cdk custom resource provider for core internal', () => { + test('can codegen cdk custom resource provider', () => { // GIVEN const module = new HandlerFrameworkModule('cdk-testing/test-provider'); const component: ComponentProps = { type: ComponentType.CUSTOM_RESOURCE_PROVIDER, - sourceCode, + sourceCode: sourceCodeTs, }; - const outfile = calculateOutfile(sourceCode); - module.build(component, path.dirname(outfile).split('/').pop() ?? 'dist'); + const outfile = calculateOutfile(sourceCodeTs); + module.build(component, calculateCodeDirectory(path.dirname(outfile))); // 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'); + const expected = fs.readFileSync(path.resolve(__dirname, 'expected', 'node-runtime', 'custom-resource-provider.ts'), 'utf-8'); expect(result).toContain(expected); }); @@ -77,17 +76,97 @@ describe('framework', () => { const module = new HandlerFrameworkModule('core/test-provider'); const component: ComponentProps = { type: ComponentType.CUSTOM_RESOURCE_PROVIDER, - sourceCode, + sourceCode: sourceCodeTs, }; - const outfile = calculateOutfile(sourceCode); - module.build(component, path.dirname(outfile).split('/').pop() ?? 'dist'); + const outfile = calculateOutfile(sourceCodeTs); + module.build(component, calculateCodeDirectory(path.dirname(outfile))); + + // 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', 'node-runtime', 'custom-resource-provider-core.ts'), 'utf-8'); + expect(result).toContain(expected); + }); + + test('can codegen cdk function with python runtime', () => { + // GIVEN + const module = new HandlerFrameworkModule('cdk-testing/test-provider'); + const component: ComponentProps = { + type: ComponentType.FUNCTION, + sourceCode: sourceCodePy, + runtime: Runtime.PYTHON_3_10, + }; + const outfile = calculateOutfile(sourceCodePy); + module.build(component, calculateCodeDirectory(path.dirname(outfile))); // 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'); + const expected = fs.readFileSync(path.resolve(__dirname, 'expected', 'python-runtime', 'function.ts'), 'utf-8'); + expect(result).toContain(expected); + }); + + test('can codegen cdk singleton function with python runtime', () => { + // GIVEN + const module = new HandlerFrameworkModule('cdk-testing/test-provider'); + const component: ComponentProps = { + type: ComponentType.SINGLETON_FUNCTION, + sourceCode: sourceCodePy, + runtime: Runtime.PYTHON_3_10, + }; + const outfile = calculateOutfile(sourceCodePy); + module.build(component, calculateCodeDirectory(path.dirname(outfile))); + + // 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', 'python-runtime', 'singleton-function.ts'), 'utf-8'); + expect(result).toContain(expected); + }); + + test('can codegen cdk custom resource provider with python runtime', () => { + // GIVEN + const module = new HandlerFrameworkModule('cdk-testing/test-provider'); + const component: ComponentProps = { + type: ComponentType.CUSTOM_RESOURCE_PROVIDER, + sourceCode: sourceCodePy, + runtime: Runtime.PYTHON_3_10, + }; + const outfile = calculateOutfile(sourceCodePy); + module.build(component, calculateCodeDirectory(path.dirname(outfile))); + + // 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', 'python-runtime', 'custom-resource-provider.ts'), 'utf-8'); + expect(result).toContain(expected); + }); + + test('can codegen cdk custom resource provider with python runtime for core internal', () => { + // GIVEN + const module = new HandlerFrameworkModule('core/test-provider'); + const component: ComponentProps = { + type: ComponentType.CUSTOM_RESOURCE_PROVIDER, + sourceCode: sourceCodePy, + runtime: Runtime.PYTHON_3_10, + }; + const outfile = calculateOutfile(sourceCodePy); + module.build(component, calculateCodeDirectory(path.dirname(outfile))); + + // 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', 'python-runtime', 'custom-resource-provider-core.ts'), 'utf-8'); expect(result).toContain(expected); }); @@ -96,9 +175,9 @@ describe('framework', () => { const module = new HandlerFrameworkModule('cdk-testing/eval-nodejs-provider'); const component: ComponentProps = { type: ComponentType.SINGLETON_FUNCTION, // eval-nodejs-provider is a singleton function - sourceCode, + sourceCode: sourceCodeTs, }; - const outfile = calculateOutfile(sourceCode); + const outfile = calculateOutfile(sourceCodeTs); module.build(component, path.dirname(outfile).split('/').pop() ?? 'dist'); // WHEN @@ -110,3 +189,7 @@ describe('framework', () => { expect(result).toContain(expected); }); }); + +function calculateCodeDirectory(outpath: string) { + return outpath.split('/').pop() ?? 'dist'; +} diff --git a/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/my-handler/index.py b/packages/@aws-cdk/custom-resource-handlers/test/custom-resources-framework/my-handler/index.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/@aws-cdk/integ-tests-alpha/lib/assertions/providers/provider.ts b/packages/@aws-cdk/integ-tests-alpha/lib/assertions/providers/provider.ts index 36d268ad37379..fa49ecfe93e0f 100644 --- a/packages/@aws-cdk/integ-tests-alpha/lib/assertions/providers/provider.ts +++ b/packages/@aws-cdk/integ-tests-alpha/lib/assertions/providers/provider.ts @@ -1,5 +1,15 @@ import * as path from 'path'; -import { Duration, CfnResource, AssetStaging, Stack, FileAssetPackaging, Token, Lazy, Reference } from 'aws-cdk-lib/core'; +import { + Duration, + CfnResource, + AssetStaging, + Stack, + FileAssetPackaging, + Token, + Lazy, + Reference, + determineLatestNodeRuntimeName, +} from 'aws-cdk-lib/core'; import { Construct } from 'constructs'; import { awsSdkToIamAction } from 'aws-cdk-lib/custom-resources/lib/helpers-internal'; import { RetentionDays } from 'aws-cdk-lib/aws-logs'; @@ -84,7 +94,7 @@ class LambdaFunctionProvider extends Construct { }); const functionProperties: any = { - Runtime: 'nodejs18.x', + Runtime: determineLatestNodeRuntimeName(this), Code: { S3Bucket: asset.bucketName, S3Key: asset.objectKey, diff --git a/packages/aws-cdk-lib/aws-certificatemanager/test/dns-validated-certificate.test.ts b/packages/aws-cdk-lib/aws-certificatemanager/test/dns-validated-certificate.test.ts index 6f6a93ee36654..b2308def62b41 100644 --- a/packages/aws-cdk-lib/aws-certificatemanager/test/dns-validated-certificate.test.ts +++ b/packages/aws-cdk-lib/aws-certificatemanager/test/dns-validated-certificate.test.ts @@ -35,7 +35,15 @@ testDeprecated('creates CloudFormation Custom Resource', () => { }); Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Handler: 'index.certificateRequestHandler', - Runtime: 'nodejs18.x', + Runtime: { + 'Fn::FindInMap': [ + 'LatestNodeRuntimeMap', + { + Ref: 'AWS::Region', + }, + 'value', + ], + }, Timeout: 900, }); Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { diff --git a/packages/aws-cdk-lib/aws-lambda/lib/runtime.ts b/packages/aws-cdk-lib/aws-lambda/lib/runtime.ts index 281cd56c0b25e..efc478dc6675a 100644 --- a/packages/aws-cdk-lib/aws-lambda/lib/runtime.ts +++ b/packages/aws-cdk-lib/aws-lambda/lib/runtime.ts @@ -1,4 +1,6 @@ -import { BundlingDockerImage, DockerImage } from '../../core'; +import { Construct } from 'constructs'; +import { BundlingDockerImage, DockerImage, Stack } from '../../core'; +import { FactName } from '../../region-info'; export interface LambdaRuntimeProps { /** @@ -371,3 +373,13 @@ export class Runtime { other.supportsInlineCode === this.supportsInlineCode; } } + +/** + * The latest Lambda node runtime available by AWS region. + */ +export function determineLatestNodeRuntime(scope: Construct): Runtime { + // Runtime regional fact should always return a known runtime string that Runtime can index off, but for type + // safety we also default it here. + const runtimeName = Stack.of(scope).regionalFact(FactName.LATEST_NODE_RUNTIME, Runtime.NODEJS_18_X.name); + return new Runtime(runtimeName, RuntimeFamily.NODEJS, { supportsInlineCode: true, isVariable: true }); +} diff --git a/packages/aws-cdk-lib/aws-lambda/test/function.test.ts b/packages/aws-cdk-lib/aws-lambda/test/function.test.ts index b6e77a56a87a7..d8edf331cecd9 100644 --- a/packages/aws-cdk-lib/aws-lambda/test/function.test.ts +++ b/packages/aws-cdk-lib/aws-lambda/test/function.test.ts @@ -3979,6 +3979,199 @@ describe('VPC configuration', () => { }); }); +describe('latest Lambda node runtime', () => { + test('with region agnostic stack', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new lambda.Function(stack, 'Lambda', { + code: lambda.Code.fromInline('foo'), + handler: 'index.handler', + runtime: lambda.determineLatestNodeRuntime(stack), + }); + + // THEN + Template.fromStack(stack).hasMapping('LatestNodeRuntimeMap', { + 'af-south-1': { + value: 'nodejs20.x', + }, + 'ap-east-1': { + value: 'nodejs20.x', + }, + 'ap-northeast-1': { + value: 'nodejs20.x', + }, + 'ap-northeast-2': { + value: 'nodejs20.x', + }, + 'ap-northeast-3': { + value: 'nodejs20.x', + }, + 'ap-south-1': { + value: 'nodejs20.x', + }, + 'ap-south-2': { + value: 'nodejs20.x', + }, + 'ap-southeast-1': { + value: 'nodejs20.x', + }, + 'ap-southeast-2': { + value: 'nodejs20.x', + }, + 'ap-southeast-3': { + value: 'nodejs20.x', + }, + 'ap-southeast-4': { + value: 'nodejs20.x', + }, + 'ca-central-1': { + value: 'nodejs20.x', + }, + 'cn-north-1': { + value: 'nodejs18.x', + }, + 'cn-northwest-1': { + value: 'nodejs18.x', + }, + 'eu-central-1': { + value: 'nodejs20.x', + }, + 'eu-central-2': { + value: 'nodejs20.x', + }, + 'eu-north-1': { + value: 'nodejs20.x', + }, + 'eu-south-1': { + value: 'nodejs20.x', + }, + 'eu-south-2': { + value: 'nodejs20.x', + }, + 'eu-west-1': { + value: 'nodejs20.x', + }, + 'eu-west-2': { + value: 'nodejs20.x', + }, + 'eu-west-3': { + value: 'nodejs20.x', + }, + 'il-central-1': { + value: 'nodejs20.x', + }, + 'me-central-1': { + value: 'nodejs20.x', + }, + 'me-south-1': { + value: 'nodejs20.x', + }, + 'sa-east-1': { + value: 'nodejs20.x', + }, + 'us-east-1': { + value: 'nodejs20.x', + }, + 'us-east-2': { + value: 'nodejs20.x', + }, + 'us-gov-east-1': { + value: 'nodejs18.x', + }, + 'us-gov-west-1': { + value: 'nodejs18.x', + }, + 'us-iso-east-1': { + value: 'nodejs18.x', + }, + 'us-iso-west-1': { + value: 'nodejs18.x', + }, + 'us-isob-east-1': { + value: 'nodejs18.x', + }, + 'us-west-1': { + value: 'nodejs20.x', + }, + 'us-west-2': { + value: 'nodejs20.x', + }, + }); + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { + Properties: { + Runtime: { + 'Fn::FindInMap': [ + 'LatestNodeRuntimeMap', + { + Ref: 'AWS::Region', + }, + 'value', + ], + }, + }, + }); + }); + + test('with stack in commercial region', () => { + // GIVEN + const stack = new cdk.Stack(undefined, 'Stack', { env: { region: 'us-east-1' } }); + + // WHEN + new lambda.Function(stack, 'Lambda', { + code: lambda.Code.fromInline('foo'), + handler: 'index.handler', + runtime: lambda.determineLatestNodeRuntime(stack), + }); + + // THEN + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { + Properties: { + Runtime: 'nodejs20.x', + }, + }); + }); + + test('with stack in china region', () => { + // GIVEN + const stack = new cdk.Stack(undefined, 'Stack', { env: { region: 'cn-north-1' } }); + + // WHEN + new lambda.Function(stack, 'Lambda', { + code: lambda.Code.fromInline('foo'), + handler: 'index.handler', + runtime: lambda.determineLatestNodeRuntime(stack), + }); + + // THEN + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { + Properties: { + Runtime: 'nodejs18.x', + }, + }); + }); + + test('with stack in govcloud region', () => { + // GIVEN + const stack = new cdk.Stack(undefined, 'Stack', { env: { region: 'us-iso-east-1' } }); + + // WHEN + new lambda.Function(stack, 'Lambda', { + code: lambda.Code.fromInline('foo'), + handler: 'index.handler', + runtime: lambda.determineLatestNodeRuntime(stack), + }); + + // THEN + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { + Properties: { + Runtime: 'nodejs18.x', + }, + }); + }); +}); + function newTestLambda(scope: constructs.Construct) { return new lambda.Function(scope, 'MyLambda', { code: new lambda.InlineCode('foo'), diff --git a/packages/aws-cdk-lib/aws-logs/lib/log-retention.ts b/packages/aws-cdk-lib/aws-logs/lib/log-retention.ts index a1c5432560c8f..86f219ea25058 100644 --- a/packages/aws-cdk-lib/aws-logs/lib/log-retention.ts +++ b/packages/aws-cdk-lib/aws-logs/lib/log-retention.ts @@ -170,7 +170,7 @@ class LogRetentionFunction extends Construct implements cdk.ITaggable { type: 'AWS::Lambda::Function', properties: { Handler: 'index.handler', - Runtime: 'nodejs18.x', + Runtime: cdk.determineLatestNodeRuntimeName(this), Timeout: cdk.Duration.minutes(15).toSeconds(), Code: { S3Bucket: asset.s3BucketName, diff --git a/packages/aws-cdk-lib/aws-logs/test/log-retention.test.ts b/packages/aws-cdk-lib/aws-logs/test/log-retention.test.ts index cb26deba0c4d2..27e34b49b8dbd 100644 --- a/packages/aws-cdk-lib/aws-logs/test/log-retention.test.ts +++ b/packages/aws-cdk-lib/aws-logs/test/log-retention.test.ts @@ -43,7 +43,15 @@ describe('log retention', () => { Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Handler: 'index.handler', - Runtime: 'nodejs18.x', + Runtime: { + 'Fn::FindInMap': [ + 'LatestNodeRuntimeMap', + { + Ref: 'AWS::Region', + }, + 'value', + ], + }, }); Template.fromStack(stack).hasResourceProperties('Custom::LogRetention', { 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 595edb29882f4..785a6d2d78d8a 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 @@ -101,6 +101,7 @@ function createEvalFn(runtime: lambda.Runtime | undefined, scope: Construct) { const lambdaPurpose = 'Eval'; const nodeJsGuids = { + [lambda.Runtime.NODEJS_20_X.name]: '9757c267-6d7c-45c2-af77-37a30d93d2c6', [lambda.Runtime.NODEJS_18_X.name]: '078d40d3-fb4e-4d53-94a7-9c46fc11fe02', [lambda.Runtime.NODEJS_16_X.name]: '2a430b68-eb4b-4026-9232-ee39b71c1db8', [lambda.Runtime.NODEJS_14_X.name]: 'da2d1181-604e-4a45-8694-1a6abd7fe42d', diff --git a/packages/aws-cdk-lib/aws-stepfunctions-tasks/test/evaluate-expression.test.ts b/packages/aws-cdk-lib/aws-stepfunctions-tasks/test/evaluate-expression.test.ts index 7dbf84d4673ab..f0ecb3695ba0d 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions-tasks/test/evaluate-expression.test.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions-tasks/test/evaluate-expression.test.ts @@ -35,7 +35,15 @@ test('Eval with Node.js', () => { }); Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { - Runtime: 'nodejs18.x', + Runtime: { + 'Fn::FindInMap': [ + 'LatestNodeRuntimeMap', + { + Ref: 'AWS::Region', + }, + 'value', + ], + }, }); }); @@ -89,17 +97,17 @@ test('with dash and underscore in path', () => { }); }); -test('With Node.js 18.x', () => { +test('With Node.js 20.x', () => { // WHEN const task = new tasks.EvaluateExpression(stack, 'Task', { expression: '$.a + $.b', - runtime: new Runtime('nodejs18.x', RuntimeFamily.NODEJS), + runtime: new Runtime('nodejs20.x', RuntimeFamily.NODEJS), }); new sfn.StateMachine(stack, 'SM', { definition: task, }); Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { - Runtime: 'nodejs18.x', + Runtime: 'nodejs20.x', }); }); 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 1b0dd0e81a984..3aaa6456262e5 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,6 +1,7 @@ import { Construct } from 'constructs'; import { CustomResourceProviderBase } from './custom-resource-provider-base'; import { CustomResourceProviderOptions } from './shared'; +import { FactName } from '../../../region-info'; import { Stack } from '../stack'; /** @@ -52,6 +53,11 @@ export enum CustomResourceProviderRuntime { * Node.js 18.x */ NODEJS_18_X = 'nodejs18.x', + + /** + * Node.js 20.x + */ + NODEJS_20_X = 'nodejs20.x', } /** @@ -132,5 +138,14 @@ function customResourceProviderRuntimeToString(x: CustomResourceProviderRuntime) return 'nodejs16.x'; case CustomResourceProviderRuntime.NODEJS_18_X: return 'nodejs18.x'; + case CustomResourceProviderRuntime.NODEJS_20_X: + return 'nodejs20.x'; } } + +/** + * The name of the latest Lambda node runtime available by AWS region. + */ +export function determineLatestNodeRuntimeName(scope: Construct): string { + return Stack.of(scope).regionalFact(FactName.LATEST_NODE_RUNTIME, 'nodejs18.x'); +} diff --git a/packages/aws-cdk-lib/core/test/custom-resource-provider/custom-resource-provider.test.ts b/packages/aws-cdk-lib/core/test/custom-resource-provider/custom-resource-provider.test.ts index 2348ae717ad3e..685e757bb9b6e 100644 --- a/packages/aws-cdk-lib/core/test/custom-resource-provider/custom-resource-provider.test.ts +++ b/packages/aws-cdk-lib/core/test/custom-resource-provider/custom-resource-provider.test.ts @@ -1,12 +1,15 @@ import * as fs from 'fs'; import * as path from 'path'; +import { Construct } from 'constructs'; +import { Template } from '../../../assertions'; import * as cxapi from '../../../cx-api'; -import { App, AssetStaging, CustomResourceProvider, CustomResourceProviderRuntime, DockerImageAssetLocation, DockerImageAssetSource, Duration, FileAssetLocation, FileAssetSource, ISynthesisSession, Size, Stack, CfnResource } from '../../lib'; +import { App, AssetStaging, CustomResourceProvider, DockerImageAssetLocation, DockerImageAssetSource, Duration, FileAssetLocation, FileAssetSource, ISynthesisSession, Size, Stack, CfnResource, determineLatestNodeRuntimeName, CustomResourceProviderBase, CustomResourceProviderBaseProps, CustomResourceProviderOptions, CustomResourceProviderRuntime } from '../../lib'; import { CUSTOMIZE_ROLES_CONTEXT_KEY } from '../../lib/helpers-internal'; import { toCloudFormation } from '../util'; const TEST_HANDLER = `${__dirname}/mock-provider`; -const STANDARD_PROVIDER = CustomResourceProviderRuntime.NODEJS_18_X; +// current node runtime available in ALL AWS regions +const DEFAULT_PROVIDER_RUNTIME = CustomResourceProviderRuntime.NODEJS_18_X; describe('custom resource provider', () => { describe('customize roles', () => { @@ -27,7 +30,7 @@ describe('custom resource provider', () => { // WHEN const cr = CustomResourceProvider.getOrCreateProvider(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: STANDARD_PROVIDER, + runtime: DEFAULT_PROVIDER_RUNTIME, }); cr.addToRolePolicy({ Action: 's3:GetBucket', @@ -104,7 +107,7 @@ describe('custom resource provider', () => { // WHEN const cr = CustomResourceProvider.getOrCreateProvider(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: STANDARD_PROVIDER, + runtime: DEFAULT_PROVIDER_RUNTIME, }); cr.addToRolePolicy({ Action: 's3:GetBucket', @@ -165,7 +168,7 @@ describe('custom resource provider', () => { // WHEN CustomResourceProvider.getOrCreate(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: STANDARD_PROVIDER, + runtime: DEFAULT_PROVIDER_RUNTIME, }); // THEN @@ -251,7 +254,7 @@ describe('custom resource provider', () => { 'Arn', ], }, - Runtime: STANDARD_PROVIDER, + Runtime: DEFAULT_PROVIDER_RUNTIME, }, DependsOn: [ 'CustomMyResourceTypeCustomResourceProviderRoleBD5E655F', @@ -285,7 +288,7 @@ describe('custom resource provider', () => { // WHEN CustomResourceProvider.getOrCreate(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: STANDARD_PROVIDER, + runtime: DEFAULT_PROVIDER_RUNTIME, }); // Then @@ -327,7 +330,7 @@ describe('custom resource provider', () => { // WHEN CustomResourceProvider.getOrCreate(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: STANDARD_PROVIDER, + runtime: DEFAULT_PROVIDER_RUNTIME, }); // THEN -- no exception @@ -344,7 +347,7 @@ describe('custom resource provider', () => { // WHEN CustomResourceProvider.getOrCreate(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: STANDARD_PROVIDER, + runtime: DEFAULT_PROVIDER_RUNTIME, policyStatements: [ { statement1: 123 }, { statement2: { foo: 111 } }, @@ -371,7 +374,7 @@ describe('custom resource provider', () => { // WHEN const provider = CustomResourceProvider.getOrCreateProvider(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: STANDARD_PROVIDER, + runtime: DEFAULT_PROVIDER_RUNTIME, policyStatements: [ { statement1: 123 }, { statement2: { foo: 111 } }, @@ -398,7 +401,7 @@ describe('custom resource provider', () => { // WHEN CustomResourceProvider.getOrCreate(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: STANDARD_PROVIDER, + runtime: DEFAULT_PROVIDER_RUNTIME, memorySize: Size.gibibytes(2), timeout: Duration.minutes(5), description: 'veni vidi vici', @@ -420,7 +423,7 @@ describe('custom resource provider', () => { // WHEN CustomResourceProvider.getOrCreate(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: STANDARD_PROVIDER, + runtime: DEFAULT_PROVIDER_RUNTIME, environment: { B: 'b', A: 'a', @@ -446,7 +449,7 @@ describe('custom resource provider', () => { // WHEN const cr = CustomResourceProvider.getOrCreateProvider(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: STANDARD_PROVIDER, + runtime: DEFAULT_PROVIDER_RUNTIME, }); // THEN @@ -459,3 +462,201 @@ describe('custom resource provider', () => { }); }); + +describe('latest Lambda node runtime', () => { + test('with region agnostic stack', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + TestCustomResourceProvider.getOrCreateProvider(stack, 'TestCrProvider'); + + // THEN + Template.fromStack(stack).hasMapping('LatestNodeRuntimeMap', { + 'af-south-1': { + value: 'nodejs20.x', + }, + 'ap-east-1': { + value: 'nodejs20.x', + }, + 'ap-northeast-1': { + value: 'nodejs20.x', + }, + 'ap-northeast-2': { + value: 'nodejs20.x', + }, + 'ap-northeast-3': { + value: 'nodejs20.x', + }, + 'ap-south-1': { + value: 'nodejs20.x', + }, + 'ap-south-2': { + value: 'nodejs20.x', + }, + 'ap-southeast-1': { + value: 'nodejs20.x', + }, + 'ap-southeast-2': { + value: 'nodejs20.x', + }, + 'ap-southeast-3': { + value: 'nodejs20.x', + }, + 'ap-southeast-4': { + value: 'nodejs20.x', + }, + 'ca-central-1': { + value: 'nodejs20.x', + }, + 'cn-north-1': { + value: 'nodejs18.x', + }, + 'cn-northwest-1': { + value: 'nodejs18.x', + }, + 'eu-central-1': { + value: 'nodejs20.x', + }, + 'eu-central-2': { + value: 'nodejs20.x', + }, + 'eu-north-1': { + value: 'nodejs20.x', + }, + 'eu-south-1': { + value: 'nodejs20.x', + }, + 'eu-south-2': { + value: 'nodejs20.x', + }, + 'eu-west-1': { + value: 'nodejs20.x', + }, + 'eu-west-2': { + value: 'nodejs20.x', + }, + 'eu-west-3': { + value: 'nodejs20.x', + }, + 'il-central-1': { + value: 'nodejs20.x', + }, + 'me-central-1': { + value: 'nodejs20.x', + }, + 'me-south-1': { + value: 'nodejs20.x', + }, + 'sa-east-1': { + value: 'nodejs20.x', + }, + 'us-east-1': { + value: 'nodejs20.x', + }, + 'us-east-2': { + value: 'nodejs20.x', + }, + 'us-gov-east-1': { + value: 'nodejs18.x', + }, + 'us-gov-west-1': { + value: 'nodejs18.x', + }, + 'us-iso-east-1': { + value: 'nodejs18.x', + }, + 'us-iso-west-1': { + value: 'nodejs18.x', + }, + 'us-isob-east-1': { + value: 'nodejs18.x', + }, + 'us-west-1': { + value: 'nodejs20.x', + }, + 'us-west-2': { + value: 'nodejs20.x', + }, + }); + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { + Properties: { + Runtime: { + 'Fn::FindInMap': [ + 'LatestNodeRuntimeMap', + { + Ref: 'AWS::Region', + }, + 'value', + ], + }, + }, + }); + }); + + test('with stack in commercial region', () => { + // GIVEN + const stack = new Stack(undefined, 'Stack', { env: { region: 'us-east-1' } }); + + // WHEN + TestCustomResourceProvider.getOrCreateProvider(stack, 'TestCrProvider'); + + // THEN + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { + Properties: { + Runtime: 'nodejs20.x', + }, + }); + }); + + test('with stack in china region', () => { + // GIVEN + const stack = new Stack(undefined, 'Stack', { env: { region: 'cn-north-1' } }); + + // WHEN + TestCustomResourceProvider.getOrCreateProvider(stack, 'TestCrProvider'); + + // THEN + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { + Properties: { + Runtime: 'nodejs18.x', + }, + }); + }); + + test('with stack in govcloud region', () => { + // GIVEN + const stack = new Stack(undefined, 'Stack', { env: { region: 'us-iso-east-1' } }); + + // WHEN + TestCustomResourceProvider.getOrCreateProvider(stack, 'TestCrProvider'); + + // THEN + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { + Properties: { + Runtime: 'nodejs18.x', + }, + }); + }); +}); + +/** + * A class used to simulate and test a code generated custom resource provider. + */ +class TestCustomResourceProvider extends CustomResourceProviderBase { + public static getOrCreateProvider(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions) { + const id = `${uniqueid}CustomResourceProvider`; + const stack = Stack.of(scope); + const provider = stack.node.tryFindChild(id) as TestCustomResourceProvider + ?? new TestCustomResourceProvider(stack, id, props); + return provider; + } + + private constructor(scope: Construct, id: string, props?: CustomResourceProviderOptions) { + super(scope, id, { + ...props, + runtimeName: determineLatestNodeRuntimeName(scope), + codeDirectory: TEST_HANDLER, + }); + } +} diff --git a/packages/aws-cdk-lib/core/test/custom-resource-provider/export-writer-provider.test.ts b/packages/aws-cdk-lib/core/test/custom-resource-provider/export-writer-provider.test.ts index b27726f4daa79..315eba063ff82 100644 --- a/packages/aws-cdk-lib/core/test/custom-resource-provider/export-writer-provider.test.ts +++ b/packages/aws-cdk-lib/core/test/custom-resource-provider/export-writer-provider.test.ts @@ -31,6 +31,115 @@ describe('export writer provider', () => { ], }); expect(cfn).toEqual({ + Mappings: { + LatestNodeRuntimeMap: { + 'af-south-1': { + value: 'nodejs20.x', + }, + 'ap-east-1': { + value: 'nodejs20.x', + }, + 'ap-northeast-1': { + value: 'nodejs20.x', + }, + 'ap-northeast-2': { + value: 'nodejs20.x', + }, + 'ap-northeast-3': { + value: 'nodejs20.x', + }, + 'ap-south-1': { + value: 'nodejs20.x', + }, + 'ap-south-2': { + value: 'nodejs20.x', + }, + 'ap-southeast-1': { + value: 'nodejs20.x', + }, + 'ap-southeast-2': { + value: 'nodejs20.x', + }, + 'ap-southeast-3': { + value: 'nodejs20.x', + }, + 'ap-southeast-4': { + value: 'nodejs20.x', + }, + 'ca-central-1': { + value: 'nodejs20.x', + }, + 'cn-north-1': { + value: 'nodejs18.x', + }, + 'cn-northwest-1': { + value: 'nodejs18.x', + }, + 'eu-central-1': { + value: 'nodejs20.x', + }, + 'eu-central-2': { + value: 'nodejs20.x', + }, + 'eu-north-1': { + value: 'nodejs20.x', + }, + 'eu-south-1': { + value: 'nodejs20.x', + }, + 'eu-south-2': { + value: 'nodejs20.x', + }, + 'eu-west-1': { + value: 'nodejs20.x', + }, + 'eu-west-2': { + value: 'nodejs20.x', + }, + 'eu-west-3': { + value: 'nodejs20.x', + }, + 'il-central-1': { + value: 'nodejs20.x', + }, + 'me-central-1': { + value: 'nodejs20.x', + }, + 'me-south-1': { + value: 'nodejs20.x', + }, + 'sa-east-1': { + value: 'nodejs20.x', + }, + 'us-east-1': { + value: 'nodejs20.x', + }, + 'us-east-2': { + value: 'nodejs20.x', + }, + 'us-gov-east-1': { + value: 'nodejs18.x', + }, + 'us-gov-west-1': { + value: 'nodejs18.x', + }, + 'us-iso-east-1': { + value: 'nodejs18.x', + }, + 'us-iso-west-1': { + value: 'nodejs18.x', + }, + 'us-isob-east-1': { + value: 'nodejs18.x', + }, + 'us-west-1': { + value: 'nodejs20.x', + }, + 'us-west-2': { + value: 'nodejs20.x', + }, + }, + }, Resources: { MyResource: { Type: 'Custom::MyResource', @@ -134,7 +243,15 @@ describe('export writer provider', () => { 'Arn', ], }, - Runtime: 'nodejs18.x', + Runtime: { + 'Fn::FindInMap': [ + 'LatestNodeRuntimeMap', + { + Ref: 'AWS::Region', + }, + 'value', + ], + }, }, DependsOn: [ 'CustomCrossRegionExportWriterCustomResourceProviderRoleC951B1E1', @@ -143,6 +260,115 @@ describe('export writer provider', () => { }, }); expect(stack2Cfn).toEqual({ + Mappings: { + LatestNodeRuntimeMap: { + 'af-south-1': { + value: 'nodejs20.x', + }, + 'ap-east-1': { + value: 'nodejs20.x', + }, + 'ap-northeast-1': { + value: 'nodejs20.x', + }, + 'ap-northeast-2': { + value: 'nodejs20.x', + }, + 'ap-northeast-3': { + value: 'nodejs20.x', + }, + 'ap-south-1': { + value: 'nodejs20.x', + }, + 'ap-south-2': { + value: 'nodejs20.x', + }, + 'ap-southeast-1': { + value: 'nodejs20.x', + }, + 'ap-southeast-2': { + value: 'nodejs20.x', + }, + 'ap-southeast-3': { + value: 'nodejs20.x', + }, + 'ap-southeast-4': { + value: 'nodejs20.x', + }, + 'ca-central-1': { + value: 'nodejs20.x', + }, + 'cn-north-1': { + value: 'nodejs18.x', + }, + 'cn-northwest-1': { + value: 'nodejs18.x', + }, + 'eu-central-1': { + value: 'nodejs20.x', + }, + 'eu-central-2': { + value: 'nodejs20.x', + }, + 'eu-north-1': { + value: 'nodejs20.x', + }, + 'eu-south-1': { + value: 'nodejs20.x', + }, + 'eu-south-2': { + value: 'nodejs20.x', + }, + 'eu-west-1': { + value: 'nodejs20.x', + }, + 'eu-west-2': { + value: 'nodejs20.x', + }, + 'eu-west-3': { + value: 'nodejs20.x', + }, + 'il-central-1': { + value: 'nodejs20.x', + }, + 'me-central-1': { + value: 'nodejs20.x', + }, + 'me-south-1': { + value: 'nodejs20.x', + }, + 'sa-east-1': { + value: 'nodejs20.x', + }, + 'us-east-1': { + value: 'nodejs20.x', + }, + 'us-east-2': { + value: 'nodejs20.x', + }, + 'us-gov-east-1': { + value: 'nodejs18.x', + }, + 'us-gov-west-1': { + value: 'nodejs18.x', + }, + 'us-iso-east-1': { + value: 'nodejs18.x', + }, + 'us-iso-west-1': { + value: 'nodejs18.x', + }, + 'us-isob-east-1': { + value: 'nodejs18.x', + }, + 'us-west-1': { + value: 'nodejs20.x', + }, + 'us-west-2': { + value: 'nodejs20.x', + }, + }, + }, Resources: { CustomCrossRegionExportReaderCustomResourceProviderHandler46647B68: { DependsOn: [ @@ -163,7 +389,15 @@ describe('export writer provider', () => { 'Arn', ], }, - Runtime: 'nodejs18.x', + Runtime: { + 'Fn::FindInMap': [ + 'LatestNodeRuntimeMap', + { + Ref: 'AWS::Region', + }, + 'value', + ], + }, Timeout: 900, }, Type: 'AWS::Lambda::Function', @@ -391,6 +625,115 @@ describe('export writer provider', () => { 'Fn::GetAtt': ['ExportsReader8B249524', '/cdk/exports/MyResourceName'], }); expect(cfn).toEqual({ + Mappings: { + LatestNodeRuntimeMap: { + 'af-south-1': { + value: 'nodejs20.x', + }, + 'ap-east-1': { + value: 'nodejs20.x', + }, + 'ap-northeast-1': { + value: 'nodejs20.x', + }, + 'ap-northeast-2': { + value: 'nodejs20.x', + }, + 'ap-northeast-3': { + value: 'nodejs20.x', + }, + 'ap-south-1': { + value: 'nodejs20.x', + }, + 'ap-south-2': { + value: 'nodejs20.x', + }, + 'ap-southeast-1': { + value: 'nodejs20.x', + }, + 'ap-southeast-2': { + value: 'nodejs20.x', + }, + 'ap-southeast-3': { + value: 'nodejs20.x', + }, + 'ap-southeast-4': { + value: 'nodejs20.x', + }, + 'ca-central-1': { + value: 'nodejs20.x', + }, + 'cn-north-1': { + value: 'nodejs18.x', + }, + 'cn-northwest-1': { + value: 'nodejs18.x', + }, + 'eu-central-1': { + value: 'nodejs20.x', + }, + 'eu-central-2': { + value: 'nodejs20.x', + }, + 'eu-north-1': { + value: 'nodejs20.x', + }, + 'eu-south-1': { + value: 'nodejs20.x', + }, + 'eu-south-2': { + value: 'nodejs20.x', + }, + 'eu-west-1': { + value: 'nodejs20.x', + }, + 'eu-west-2': { + value: 'nodejs20.x', + }, + 'eu-west-3': { + value: 'nodejs20.x', + }, + 'il-central-1': { + value: 'nodejs20.x', + }, + 'me-central-1': { + value: 'nodejs20.x', + }, + 'me-south-1': { + value: 'nodejs20.x', + }, + 'sa-east-1': { + value: 'nodejs20.x', + }, + 'us-east-1': { + value: 'nodejs20.x', + }, + 'us-east-2': { + value: 'nodejs20.x', + }, + 'us-gov-east-1': { + value: 'nodejs18.x', + }, + 'us-gov-west-1': { + value: 'nodejs18.x', + }, + 'us-iso-east-1': { + value: 'nodejs18.x', + }, + 'us-iso-west-1': { + value: 'nodejs18.x', + }, + 'us-isob-east-1': { + value: 'nodejs18.x', + }, + 'us-west-1': { + value: 'nodejs20.x', + }, + 'us-west-2': { + value: 'nodejs20.x', + }, + }, + }, Resources: { MyResource: { Type: 'Custom::MyResource', @@ -495,7 +838,15 @@ describe('export writer provider', () => { 'Arn', ], }, - Runtime: 'nodejs18.x', + Runtime: { + 'Fn::FindInMap': [ + 'LatestNodeRuntimeMap', + { + Ref: 'AWS::Region', + }, + 'value', + ], + }, }, DependsOn: [ 'CustomCrossRegionExportWriterCustomResourceProviderRoleC951B1E1', @@ -504,6 +855,115 @@ describe('export writer provider', () => { }, }); expect(stack2Cfn).toEqual({ + Mappings: { + LatestNodeRuntimeMap: { + 'af-south-1': { + value: 'nodejs20.x', + }, + 'ap-east-1': { + value: 'nodejs20.x', + }, + 'ap-northeast-1': { + value: 'nodejs20.x', + }, + 'ap-northeast-2': { + value: 'nodejs20.x', + }, + 'ap-northeast-3': { + value: 'nodejs20.x', + }, + 'ap-south-1': { + value: 'nodejs20.x', + }, + 'ap-south-2': { + value: 'nodejs20.x', + }, + 'ap-southeast-1': { + value: 'nodejs20.x', + }, + 'ap-southeast-2': { + value: 'nodejs20.x', + }, + 'ap-southeast-3': { + value: 'nodejs20.x', + }, + 'ap-southeast-4': { + value: 'nodejs20.x', + }, + 'ca-central-1': { + value: 'nodejs20.x', + }, + 'cn-north-1': { + value: 'nodejs18.x', + }, + 'cn-northwest-1': { + value: 'nodejs18.x', + }, + 'eu-central-1': { + value: 'nodejs20.x', + }, + 'eu-central-2': { + value: 'nodejs20.x', + }, + 'eu-north-1': { + value: 'nodejs20.x', + }, + 'eu-south-1': { + value: 'nodejs20.x', + }, + 'eu-south-2': { + value: 'nodejs20.x', + }, + 'eu-west-1': { + value: 'nodejs20.x', + }, + 'eu-west-2': { + value: 'nodejs20.x', + }, + 'eu-west-3': { + value: 'nodejs20.x', + }, + 'il-central-1': { + value: 'nodejs20.x', + }, + 'me-central-1': { + value: 'nodejs20.x', + }, + 'me-south-1': { + value: 'nodejs20.x', + }, + 'sa-east-1': { + value: 'nodejs20.x', + }, + 'us-east-1': { + value: 'nodejs20.x', + }, + 'us-east-2': { + value: 'nodejs20.x', + }, + 'us-gov-east-1': { + value: 'nodejs18.x', + }, + 'us-gov-west-1': { + value: 'nodejs18.x', + }, + 'us-iso-east-1': { + value: 'nodejs18.x', + }, + 'us-iso-west-1': { + value: 'nodejs18.x', + }, + 'us-isob-east-1': { + value: 'nodejs18.x', + }, + 'us-west-1': { + value: 'nodejs20.x', + }, + 'us-west-2': { + value: 'nodejs20.x', + }, + }, + }, Resources: { CustomCrossRegionExportReaderCustomResourceProviderHandler46647B68: { DependsOn: [ @@ -524,7 +984,15 @@ describe('export writer provider', () => { 'Arn', ], }, - Runtime: 'nodejs18.x', + Runtime: { + 'Fn::FindInMap': [ + 'LatestNodeRuntimeMap', + { + Ref: 'AWS::Region', + }, + 'value', + ], + }, Timeout: 900, }, Type: 'AWS::Lambda::Function', diff --git a/packages/aws-cdk-lib/custom-resources/lib/provider-framework/provider.ts b/packages/aws-cdk-lib/custom-resources/lib/provider-framework/provider.ts index a460d89f21241..f0e5257a33b0c 100644 --- a/packages/aws-cdk-lib/custom-resources/lib/provider-framework/provider.ts +++ b/packages/aws-cdk-lib/custom-resources/lib/provider-framework/provider.ts @@ -256,7 +256,7 @@ export class Provider extends Construct implements ICustomResourceProvider { exclude: ['*.ts'], }), description: `AWS CDK resource provider framework - ${entrypoint} (${this.node.path})`.slice(0, 256), - runtime: lambda.Runtime.NODEJS_18_X, + runtime: lambda.determineLatestNodeRuntime(this), handler: `framework.${entrypoint}`, timeout: FRAMEWORK_HANDLER_TIMEOUT, // props.logRetention is deprecated, make sure we only set it if it is actually provided diff --git a/packages/aws-cdk-lib/region-info/build-tools/fact-tables.ts b/packages/aws-cdk-lib/region-info/build-tools/fact-tables.ts index ea9d9251b36d5..5423d65996327 100644 --- a/packages/aws-cdk-lib/region-info/build-tools/fact-tables.ts +++ b/packages/aws-cdk-lib/region-info/build-tools/fact-tables.ts @@ -3453,3 +3453,11 @@ export const PARTITION_SAML_SIGN_ON_URL: Record = { [Partition.UsIso]: 'https://signin.c2shome.ic.gov/saml', [Partition.UsIsoB]: 'https://signin.sc2shome.sgov.gov/saml', }; + +export const LATEST_NODE_RUNTIME_MAP: Record = { + [Partition.Default]: 'nodejs20.x', + [Partition.Cn]: 'nodejs18.x', + [Partition.UsGov]: 'nodejs18.x', + [Partition.UsIso]: 'nodejs18.x', + [Partition.UsIsoB]: 'nodejs18.x', +}; diff --git a/packages/aws-cdk-lib/region-info/build-tools/generate-static-data.ts b/packages/aws-cdk-lib/region-info/build-tools/generate-static-data.ts index 03f8c5d712710..9adba22c9b92d 100644 --- a/packages/aws-cdk-lib/region-info/build-tools/generate-static-data.ts +++ b/packages/aws-cdk-lib/region-info/build-tools/generate-static-data.ts @@ -13,6 +13,7 @@ import { PARAMS_AND_SECRETS_LAMBDA_LAYER_ARNS, APPCONFIG_LAMBDA_LAYER_ARNS, PARTITION_SAML_SIGN_ON_URL, + LATEST_NODE_RUNTIME_MAP, } from './fact-tables'; import { AWS_CDK_METADATA } from './metadata'; import { @@ -87,6 +88,8 @@ export async function main(): Promise { registerFact(region, 'SAML_SIGN_ON_URL', PARTITION_SAML_SIGN_ON_URL[partition]); + registerFact(region, 'LATEST_NODE_RUNTIME', LATEST_NODE_RUNTIME_MAP[partition]); + const firehoseCidrBlock = FIREHOSE_CIDR_BLOCKS[region]; if (firehoseCidrBlock) { registerFact(region, 'FIREHOSE_CIDR_BLOCK', `${FIREHOSE_CIDR_BLOCKS[region]}/27`); diff --git a/packages/aws-cdk-lib/region-info/lib/fact.ts b/packages/aws-cdk-lib/region-info/lib/fact.ts index 36671df11c3f1..1657743343c87 100644 --- a/packages/aws-cdk-lib/region-info/lib/fact.ts +++ b/packages/aws-cdk-lib/region-info/lib/fact.ts @@ -195,6 +195,11 @@ export class FactName { */ public static readonly SAML_SIGN_ON_URL = 'samlSignOnUrl'; + /** + * The latest Lambda NodeJS runtime available in a given region. + */ + public static readonly LATEST_NODE_RUNTIME = 'latestNodeRuntime'; + /** * The ARN of CloudWatch Lambda Insights for a version (e.g. 1.0.98.0) */