From eebe2b2a827f05c9e8f65cad463e7abf42b0095b Mon Sep 17 00:00:00 2001 From: ykethan Date: Thu, 8 Aug 2024 10:43:01 -0400 Subject: [PATCH 01/16] adds referenceLayer to function --- .changeset/calm-buttons-sort.md | 6 + packages/backend-function/src/factory.ts | 64 +++++--- packages/backend-function/src/index.ts | 1 + .../src/reference_layer.test.ts | 144 ++++++++++++++++++ .../backend-function/src/reference_layer.ts | 77 ++++++++++ packages/backend/src/index.ts | 9 +- 6 files changed, 279 insertions(+), 22 deletions(-) create mode 100644 .changeset/calm-buttons-sort.md create mode 100644 packages/backend-function/src/reference_layer.test.ts create mode 100644 packages/backend-function/src/reference_layer.ts diff --git a/.changeset/calm-buttons-sort.md b/.changeset/calm-buttons-sort.md new file mode 100644 index 0000000000..fddb681941 --- /dev/null +++ b/.changeset/calm-buttons-sort.md @@ -0,0 +1,6 @@ +--- +'@aws-amplify/backend-function': minor +'@aws-amplify/backend': minor +--- + +adds referenceLayer to function diff --git a/packages/backend-function/src/factory.ts b/packages/backend-function/src/factory.ts index 3feefb4b78..ff0a457512 100644 --- a/packages/backend-function/src/factory.ts +++ b/packages/backend-function/src/factory.ts @@ -1,3 +1,9 @@ +import { + FunctionOutput, + functionOutputKey, +} from '@aws-amplify/backend-output-schemas'; +import { AttributionMetadataStorage } from '@aws-amplify/backend-output-storage'; +import { AmplifyUserError, TagName } from '@aws-amplify/platform-core'; import { BackendOutputStorageStrategy, BackendSecret, @@ -12,28 +18,23 @@ import { ResourceProvider, SsmEnvironmentEntry, } from '@aws-amplify/plugin-types'; -import { Construct } from 'constructs'; -import { NodejsFunction, OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs'; -import * as path from 'path'; -import { getCallerDirectory } from './get_caller_directory.js'; import { Duration, Stack, Tags } from 'aws-cdk-lib'; -import { CfnFunction, Runtime } from 'aws-cdk-lib/aws-lambda'; -import { createRequire } from 'module'; -import { FunctionEnvironmentTranslator } from './function_env_translator.js'; +import { Rule } from 'aws-cdk-lib/aws-events'; +import * as targets from 'aws-cdk-lib/aws-events-targets'; import { Policy } from 'aws-cdk-lib/aws-iam'; +import { CfnFunction, ILayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'; +import { NodejsFunction, OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { Construct } from 'constructs'; import { readFileSync } from 'fs'; +import { createRequire } from 'module'; +import { fileURLToPath } from 'node:url'; import { EOL } from 'os'; -import { - FunctionOutput, - functionOutputKey, -} from '@aws-amplify/backend-output-schemas'; +import * as path from 'path'; +import { FunctionEnvironmentTranslator } from './function_env_translator.js'; import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.js'; -import { AttributionMetadataStorage } from '@aws-amplify/backend-output-storage'; -import { fileURLToPath } from 'node:url'; -import { AmplifyUserError, TagName } from '@aws-amplify/platform-core'; +import { getCallerDirectory } from './get_caller_directory.js'; +import { LayerReference, resolveLayers } from './reference_layer.js'; import { convertFunctionSchedulesToRuleSchedules } from './schedule_parser.js'; -import * as targets from 'aws-cdk-lib/aws-events-targets'; -import { Rule } from 'aws-cdk-lib/aws-events'; const functionStackType = 'function-Lambda'; @@ -52,6 +53,7 @@ export type TimeInterval = | `every month` | `every year`; export type FunctionSchedule = TimeInterval | CronSchedule; +export type FunctionLayerReferences = Record; /** * Entry point for defining a function in the Amplify ecosystem @@ -121,6 +123,20 @@ export type FunctionProps = { * schedule: "0 9 * * 2" // every Monday at 9am */ schedule?: FunctionSchedule | FunctionSchedule[]; + + /** + * Attach Lambda layers to a function + * @example + * import { referenceFunctionLayer } from "@aws-amplify/backend"; + * + * layers: { + * myModule: referenceFunctionLayer("arn:aws:lambda:::layer:myLayer:1") + * }, + * + * The object is keyed by the module name hosted on your existing layer and a value that references to an existing layer using an ARN. The keys will be externalized and available via your layer at runtime + * Maximum of 5 layers can be attached to a function. Layers must be in the same region as the function. + */ + layers?: FunctionLayerReferences; }; /** @@ -166,6 +182,7 @@ class FunctionFactory implements ConstructFactory { environment: this.props.environment ?? {}, runtime: this.resolveRuntime(), schedule: this.resolveSchedule(), + layers: this.props.layers ?? {}, }; }; @@ -265,7 +282,9 @@ class FunctionFactory implements ConstructFactory { }; } -type HydratedFunctionProps = Required; +type HydratedFunctionProps = Required & { + layers: FunctionLayerReferences; +}; class FunctionGenerator implements ConstructContainerEntryGenerator { readonly resourceGroupName = 'function'; @@ -279,10 +298,15 @@ class FunctionGenerator implements ConstructContainerEntryGenerator { scope, backendSecretResolver, }: GenerateContainerEntryProps) => { + const resolvedLayers = resolveLayers( + this.props.layers, + scope, + this.props.name + ); return new AmplifyFunction( scope, this.props.name, - this.props, + { ...this.props, resolvedLayers }, backendSecretResolver, this.outputStorageStrategy ); @@ -301,7 +325,7 @@ class AmplifyFunction constructor( scope: Construct, id: string, - props: HydratedFunctionProps, + props: HydratedFunctionProps & { resolvedLayers: ILayerVersion[] }, backendSecretResolver: BackendSecretResolver, outputStorageStrategy: BackendOutputStorageStrategy ) { @@ -349,6 +373,7 @@ class AmplifyFunction timeout: Duration.seconds(props.timeoutSeconds), memorySize: props.memoryMB, runtime: nodeVersionMap[props.runtime], + layers: props.resolvedLayers, bundling: { format: OutputFormat.ESM, banner: bannerCode, @@ -359,6 +384,7 @@ class AmplifyFunction }, minify: true, sourceMap: true, + externalModules: Object.keys(props.layers), }, }); } catch (error) { diff --git a/packages/backend-function/src/index.ts b/packages/backend-function/src/index.ts index cb95b1ef8d..a75eef2830 100644 --- a/packages/backend-function/src/index.ts +++ b/packages/backend-function/src/index.ts @@ -1 +1,2 @@ export * from './factory.js'; +export { referenceFunctionLayer } from './reference_layer.js'; diff --git a/packages/backend-function/src/reference_layer.test.ts b/packages/backend-function/src/reference_layer.test.ts new file mode 100644 index 0000000000..9af79a1dde --- /dev/null +++ b/packages/backend-function/src/reference_layer.test.ts @@ -0,0 +1,144 @@ +import { StackMetadataBackendOutputStorageStrategy } from '@aws-amplify/backend-output-storage'; +import { + ConstructContainerStub, + ResourceNameValidatorStub, + StackResolverStub, +} from '@aws-amplify/backend-platform-test-stubs'; +import { + ConstructFactoryGetInstanceProps, + ResourceNameValidator, +} from '@aws-amplify/plugin-types'; +import { App, Stack } from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import assert from 'node:assert'; +import { beforeEach, describe, it } from 'node:test'; +import { defineFunction } from './factory.js'; + +const createStackAndSetContext = (): Stack => { + const app = new App(); + app.node.setContext('amplify-backend-name', 'testEnvName'); + app.node.setContext('amplify-backend-namespace', 'testBackendId'); + app.node.setContext('amplify-backend-type', 'branch'); + const stack = new Stack(app); + return stack; +}; + +void describe('AmplifyFunctionFactory - Layers', () => { + let rootStack: Stack; + let getInstanceProps: ConstructFactoryGetInstanceProps; + let resourceNameValidator: ResourceNameValidator; + + beforeEach(() => { + rootStack = createStackAndSetContext(); + + const constructContainer = new ConstructContainerStub( + new StackResolverStub(rootStack) + ); + + const outputStorageStrategy = new StackMetadataBackendOutputStorageStrategy( + rootStack + ); + + resourceNameValidator = new ResourceNameValidatorStub(); + + getInstanceProps = { + constructContainer, + outputStorageStrategy, + resourceNameValidator, + }; + }); + + void it('sets a valid layer', () => { + const layerArn = 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer:1'; + const functionFactory = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'lambdaWithLayer', + layers: { + myLayer: { + arn: layerArn, + }, + }, + }); + const lambda = functionFactory.getInstance(getInstanceProps); + const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'index.handler', + Layers: [layerArn], + }); + }); + + void it('sets multiple valid layers', () => { + const layerArns = [ + 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-1:1', + 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-2:1', + ]; + const functionFactory = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'lambdaWithMultipleLayers', + layers: { + myLayer1: { + arn: layerArns[0], + }, + myLayer2: { + arn: layerArns[1], + }, + }, + }); + const lambda = functionFactory.getInstance(getInstanceProps); + const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'index.handler', + Layers: layerArns, + }); + }); + + void it('throws an error for an invalid layer ARN', () => { + const invalidLayerArn = 'invalid:arn'; + const functionFactory = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'lambdaWithInvalidLayer', + layers: { + invalidLayer: { + arn: invalidLayerArn, + }, + }, + }); + assert.throws( + () => functionFactory.getInstance(getInstanceProps), + new Error(`Invalid ARN format for layer invalidLayer: ${invalidLayerArn}`) + ); + }); + + void it('throws an error for exceeding the maximum number of layers', () => { + const layerArns = [ + 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-1:1', + 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-2:1', + 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-3:1', + 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-4:1', + 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-5:1', + 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-6:1', + ]; + const layers: Record = layerArns.reduce( + (acc, arn, index) => { + acc[`layer${index + 1}`] = { arn }; + return acc; + }, + {} as Record + ); + + const functionFactory = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'lambdaWithTooManyLayers', + layers, + }); + + assert.throws( + () => functionFactory.getInstance(getInstanceProps), + new Error('A maximum of 5 unique layers can be attached to a function.') + ); + }); +}); diff --git a/packages/backend-function/src/reference_layer.ts b/packages/backend-function/src/reference_layer.ts new file mode 100644 index 0000000000..f06d84ffc0 --- /dev/null +++ b/packages/backend-function/src/reference_layer.ts @@ -0,0 +1,77 @@ +import { ILayerVersion, LayerVersion } from 'aws-cdk-lib/aws-lambda'; +import { Arn } from 'aws-cdk-lib/core'; +import { Construct } from 'constructs'; + +/** + * Defines the ARN regex pattern for a layer. + */ +export const arnPattern = new RegExp( + '^(arn:(aws[a-zA-Z-]*)?:lambda:[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:\\d{12}:layer:[a-zA-Z0-9-_]+:[0-9]+)|(arn:[a-zA-Z0-9-]+:lambda:::awslayer:[a-zA-Z0-9-_]+)$' +); + +/** + * Checks if the provided ARN is valid. + */ +export const isValidLayerArn = (arn: string): boolean => { + return arnPattern.test(arn); +}; + +/** + * Validates the provided layer ARNs and throws an error if any are invalid or if there are more than 5 unique ARNs. + */ +export const validateLayers = ( + layers: Record +): Set => { + // Remove duplicate layer Arn's + const uniqueArns = new Set(Object.values(layers).map((layer) => layer.arn)); + + // Only 5 layers can be attached to a function + if (uniqueArns.size > 5) { + throw new Error( + 'A maximum of 5 unique layers can be attached to a function.' + ); + } + + // Check if all Arn inputs are a valid Layer A + for (const [layerName, layerObj] of Object.entries(layers)) { + if (!isValidLayerArn(layerObj.arn)) { + throw new Error( + `Invalid ARN format for layer ${layerName}: ${layerObj.arn}` + ); + } + } + + return uniqueArns; +}; + +/** + * Type definition for a layer reference. + */ +export type LayerReference = { arn: string }; + +/** + * Creates a reference to a layer from an ARN. + */ +export const referenceFunctionLayer = (arn: string): LayerReference => { + return { arn }; +}; + +/** + * Resolves and returns the layers for an AWS Lambda function. + */ +export const resolveLayers = ( + layers: Record, + scope: Construct, + functionName: string +): ILayerVersion[] => { + validateLayers(layers); + const uniqueArns = validateLayers(layers); + return Array.from(uniqueArns).map((arn) => { + const layerName = Arn.extractResourceName(arn, 'layer'); + return LayerVersion.fromLayerVersionArn( + scope, + `${functionName}-${layerName}-layer`, + arn + ); + }); +}; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 039b5de54c..592055dc9b 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,12 +1,12 @@ -export { defineBackend } from './backend_factory.js'; export * from './backend.js'; +export { defineBackend } from './backend_factory.js'; export * from './secret.js'; // re-export core functionality from category packages // data export { defineData } from '@aws-amplify/backend-data'; -export { type ClientSchema, a } from '@aws-amplify/data-schema'; +export { a, type ClientSchema } from '@aws-amplify/data-schema'; // auth export { defineAuth } from '@aws-amplify/backend-auth'; @@ -15,4 +15,7 @@ export { defineAuth } from '@aws-amplify/backend-auth'; export { defineStorage } from '@aws-amplify/backend-storage'; // function -export { defineFunction } from '@aws-amplify/backend-function'; +export { + defineFunction, + referenceFunctionLayer, +} from '@aws-amplify/backend-function'; From d67f787280a488ed8bb427d41b60c97acc023527 Mon Sep 17 00:00:00 2001 From: ykethan Date: Thu, 8 Aug 2024 11:11:02 -0400 Subject: [PATCH 02/16] api.md and lint --- .eslint_dictionary.json | 1 + packages/backend-function/API.md | 9 +++++++++ packages/backend/API.md | 3 +++ 3 files changed, 13 insertions(+) diff --git a/.eslint_dictionary.json b/.eslint_dictionary.json index ddf21aa36b..2b184811b7 100644 --- a/.eslint_dictionary.json +++ b/.eslint_dictionary.json @@ -12,6 +12,7 @@ "argv", "arn", "arns", + "awslayer", "backends", "birthdate", "bundler", diff --git a/packages/backend-function/API.md b/packages/backend-function/API.md index 86a98d7e1e..46f2621ea4 100644 --- a/packages/backend-function/API.md +++ b/packages/backend-function/API.md @@ -21,6 +21,11 @@ export type CronSchedule = `${string} ${string} ${string} ${string} ${string}` | // @public export const defineFunction: (props?: FunctionProps) => ConstructFactory & ResourceAccessAcceptorFactory & AddEnvironmentFactory>; +// Warning: (ae-forgotten-export) The symbol "LayerReference" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type FunctionLayerReferences = Record; + // @public (undocumented) export type FunctionProps = { name?: string; @@ -30,6 +35,7 @@ export type FunctionProps = { environment?: Record; runtime?: NodeVersion; schedule?: FunctionSchedule | FunctionSchedule[]; + layers?: FunctionLayerReferences; }; // @public (undocumented) @@ -38,6 +44,9 @@ export type FunctionSchedule = TimeInterval | CronSchedule; // @public (undocumented) export type NodeVersion = 16 | 18 | 20; +// @public +export const referenceFunctionLayer: (arn: string) => LayerReference; + // @public (undocumented) export type TimeInterval = `every ${number}m` | `every ${number}h` | `every day` | `every week` | `every month` | `every year`; diff --git a/packages/backend/API.md b/packages/backend/API.md index 1cb83ee09f..3477ad55ee 100644 --- a/packages/backend/API.md +++ b/packages/backend/API.md @@ -26,6 +26,7 @@ import { defineStorage } from '@aws-amplify/backend-storage'; import { FunctionResources } from '@aws-amplify/plugin-types'; import { GenerateContainerEntryProps } from '@aws-amplify/plugin-types'; import { ImportPathVerifier } from '@aws-amplify/plugin-types'; +import { referenceFunctionLayer } from '@aws-amplify/backend-function'; import { ResourceAccessAcceptorFactory } from '@aws-amplify/plugin-types'; import { ResourceProvider } from '@aws-amplify/plugin-types'; import { SsmEnvironmentEntriesGenerator } from '@aws-amplify/plugin-types'; @@ -89,6 +90,8 @@ export { GenerateContainerEntryProps } export { ImportPathVerifier } +export { referenceFunctionLayer } + export { ResourceProvider } // @public From e5a1252258347ec187f04ce2ff815b8df1505fe0 Mon Sep 17 00:00:00 2001 From: ykethan Date: Thu, 8 Aug 2024 11:24:41 -0400 Subject: [PATCH 03/16] api.md --- packages/backend-function/API.md | 7 +++++-- packages/backend-function/src/index.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/backend-function/API.md b/packages/backend-function/API.md index 46f2621ea4..71ad54f6af 100644 --- a/packages/backend-function/API.md +++ b/packages/backend-function/API.md @@ -21,8 +21,6 @@ export type CronSchedule = `${string} ${string} ${string} ${string} ${string}` | // @public export const defineFunction: (props?: FunctionProps) => ConstructFactory & ResourceAccessAcceptorFactory & AddEnvironmentFactory>; -// Warning: (ae-forgotten-export) The symbol "LayerReference" needs to be exported by the entry point index.d.ts -// // @public (undocumented) export type FunctionLayerReferences = Record; @@ -41,6 +39,11 @@ export type FunctionProps = { // @public (undocumented) export type FunctionSchedule = TimeInterval | CronSchedule; +// @public +export type LayerReference = { + arn: string; +}; + // @public (undocumented) export type NodeVersion = 16 | 18 | 20; diff --git a/packages/backend-function/src/index.ts b/packages/backend-function/src/index.ts index a75eef2830..82e86585c9 100644 --- a/packages/backend-function/src/index.ts +++ b/packages/backend-function/src/index.ts @@ -1,2 +1,2 @@ export * from './factory.js'; -export { referenceFunctionLayer } from './reference_layer.js'; +export { LayerReference, referenceFunctionLayer } from './reference_layer.js'; From 5a8621a6b2720b68f70acb0cd0e961d9eab9e889 Mon Sep 17 00:00:00 2001 From: ykethan Date: Thu, 8 Aug 2024 19:26:03 -0400 Subject: [PATCH 04/16] remove referenceLayer --- packages/backend-function/API.md | 10 +-- packages/backend-function/src/factory.ts | 4 +- packages/backend-function/src/index.ts | 1 - .../src/reference_layer.test.ts | 27 +++---- .../backend-function/src/reference_layer.ts | 72 +++++++++++-------- packages/backend/API.md | 3 - packages/backend/src/index.ts | 5 +- 7 files changed, 57 insertions(+), 65 deletions(-) diff --git a/packages/backend-function/API.md b/packages/backend-function/API.md index 71ad54f6af..deed77ae53 100644 --- a/packages/backend-function/API.md +++ b/packages/backend-function/API.md @@ -22,7 +22,7 @@ export type CronSchedule = `${string} ${string} ${string} ${string} ${string}` | export const defineFunction: (props?: FunctionProps) => ConstructFactory & ResourceAccessAcceptorFactory & AddEnvironmentFactory>; // @public (undocumented) -export type FunctionLayerReferences = Record; +export type FunctionLayerReferences = Record; // @public (undocumented) export type FunctionProps = { @@ -39,17 +39,9 @@ export type FunctionProps = { // @public (undocumented) export type FunctionSchedule = TimeInterval | CronSchedule; -// @public -export type LayerReference = { - arn: string; -}; - // @public (undocumented) export type NodeVersion = 16 | 18 | 20; -// @public -export const referenceFunctionLayer: (arn: string) => LayerReference; - // @public (undocumented) export type TimeInterval = `every ${number}m` | `every ${number}h` | `every day` | `every week` | `every month` | `every year`; diff --git a/packages/backend-function/src/factory.ts b/packages/backend-function/src/factory.ts index ff0a457512..4091d43554 100644 --- a/packages/backend-function/src/factory.ts +++ b/packages/backend-function/src/factory.ts @@ -33,7 +33,7 @@ import * as path from 'path'; import { FunctionEnvironmentTranslator } from './function_env_translator.js'; import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.js'; import { getCallerDirectory } from './get_caller_directory.js'; -import { LayerReference, resolveLayers } from './reference_layer.js'; +import { resolveLayers } from './reference_layer.js'; import { convertFunctionSchedulesToRuleSchedules } from './schedule_parser.js'; const functionStackType = 'function-Lambda'; @@ -53,7 +53,7 @@ export type TimeInterval = | `every month` | `every year`; export type FunctionSchedule = TimeInterval | CronSchedule; -export type FunctionLayerReferences = Record; +export type FunctionLayerReferences = Record; /** * Entry point for defining a function in the Amplify ecosystem diff --git a/packages/backend-function/src/index.ts b/packages/backend-function/src/index.ts index 82e86585c9..cb95b1ef8d 100644 --- a/packages/backend-function/src/index.ts +++ b/packages/backend-function/src/index.ts @@ -1,2 +1 @@ export * from './factory.js'; -export { LayerReference, referenceFunctionLayer } from './reference_layer.js'; diff --git a/packages/backend-function/src/reference_layer.test.ts b/packages/backend-function/src/reference_layer.test.ts index 9af79a1dde..1c45479c3e 100644 --- a/packages/backend-function/src/reference_layer.test.ts +++ b/packages/backend-function/src/reference_layer.test.ts @@ -11,6 +11,7 @@ import { import { App, Stack } from 'aws-cdk-lib'; import { Template } from 'aws-cdk-lib/assertions'; import assert from 'node:assert'; +import { EOL } from 'node:os'; import { beforeEach, describe, it } from 'node:test'; import { defineFunction } from './factory.js'; @@ -54,9 +55,7 @@ void describe('AmplifyFunctionFactory - Layers', () => { entry: './test-assets/default-lambda/handler.ts', name: 'lambdaWithLayer', layers: { - myLayer: { - arn: layerArn, - }, + myLayer: layerArn, }, }); const lambda = functionFactory.getInstance(getInstanceProps); @@ -78,12 +77,8 @@ void describe('AmplifyFunctionFactory - Layers', () => { entry: './test-assets/default-lambda/handler.ts', name: 'lambdaWithMultipleLayers', layers: { - myLayer1: { - arn: layerArns[0], - }, - myLayer2: { - arn: layerArns[1], - }, + myLayer1: layerArns[0], + myLayer2: layerArns[1], }, }); const lambda = functionFactory.getInstance(getInstanceProps); @@ -102,14 +97,14 @@ void describe('AmplifyFunctionFactory - Layers', () => { entry: './test-assets/default-lambda/handler.ts', name: 'lambdaWithInvalidLayer', layers: { - invalidLayer: { - arn: invalidLayerArn, - }, + invalidLayer: invalidLayerArn, }, }); assert.throws( () => functionFactory.getInstance(getInstanceProps), - new Error(`Invalid ARN format for layer invalidLayer: ${invalidLayerArn}`) + new Error( + `Invalid ARN format for layer: ${invalidLayerArn} ${EOL} Expected format: arn:aws:lambda:::layer::` + ) ); }); @@ -122,12 +117,12 @@ void describe('AmplifyFunctionFactory - Layers', () => { 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-5:1', 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-6:1', ]; - const layers: Record = layerArns.reduce( + const layers: Record = layerArns.reduce( (acc, arn, index) => { - acc[`layer${index + 1}`] = { arn }; + acc[`layer${index + 1}`] = arn; return acc; }, - {} as Record + {} as Record ); const functionFactory = defineFunction({ diff --git a/packages/backend-function/src/reference_layer.ts b/packages/backend-function/src/reference_layer.ts index f06d84ffc0..d54eabf3e7 100644 --- a/packages/backend-function/src/reference_layer.ts +++ b/packages/backend-function/src/reference_layer.ts @@ -1,6 +1,7 @@ import { ILayerVersion, LayerVersion } from 'aws-cdk-lib/aws-lambda'; import { Arn } from 'aws-cdk-lib/core'; import { Construct } from 'constructs'; +import { EOL } from 'os'; /** * Defines the ARN regex pattern for a layer. @@ -17,61 +18,72 @@ export const isValidLayerArn = (arn: string): boolean => { }; /** - * Validates the provided layer ARNs and throws an error if any are invalid or if there are more than 5 unique ARNs. + * Class to represent and handle Lambda Layer ARNs. */ -export const validateLayers = ( - layers: Record -): Set => { - // Remove duplicate layer Arn's - const uniqueArns = new Set(Object.values(layers).map((layer) => layer.arn)); - - // Only 5 layers can be attached to a function - if (uniqueArns.size > 5) { - throw new Error( - 'A maximum of 5 unique layers can be attached to a function.' - ); - } +export class FunctionLayerArn { + public readonly arn: string; - // Check if all Arn inputs are a valid Layer A - for (const [layerName, layerObj] of Object.entries(layers)) { - if (!isValidLayerArn(layerObj.arn)) { + /** + * Creates an instance of FunctionLayerArn. + */ + constructor(arn: string) { + if (!isValidLayerArn(arn)) { throw new Error( - `Invalid ARN format for layer ${layerName}: ${layerObj.arn}` + `Invalid ARN format for layer: ${arn} ${EOL} Expected format: arn:aws:lambda:::layer::` ); } + this.arn = arn; } - return uniqueArns; -}; + /** + * Converts the FunctionLayerArn to a string. + */ + toString(): string { + return this.arn; + } +} /** - * Type definition for a layer reference. + * Parses a string to create a FunctionLayerArn instance. */ -export type LayerReference = { arn: string }; +export const parseFunctionLayerArn = (arn: string): FunctionLayerArn => { + return new FunctionLayerArn(arn); +}; /** - * Creates a reference to a layer from an ARN. + * Validates the provided layer ARNs and throws an error if any are invalid or if there are more than 5 unique ARNs. */ -export const referenceFunctionLayer = (arn: string): LayerReference => { - return { arn }; +export const validateLayers = ( + layers: Record +): Set => { + const uniqueArns = new Set(Object.values(layers)); + + if (uniqueArns.size > 5) { + throw new Error( + 'A maximum of 5 unique layers can be attached to a function.' + ); + } + + return new Set( + Array.from(uniqueArns).map(parseFunctionLayerArn) + ); }; /** * Resolves and returns the layers for an AWS Lambda function. */ export const resolveLayers = ( - layers: Record, + layers: Record, scope: Construct, functionName: string ): ILayerVersion[] => { - validateLayers(layers); - const uniqueArns = validateLayers(layers); - return Array.from(uniqueArns).map((arn) => { - const layerName = Arn.extractResourceName(arn, 'layer'); + const uniqueLayerArns = validateLayers(layers); + return Array.from(uniqueLayerArns).map((layerArn) => { + const layerName = Arn.extractResourceName(layerArn.toString(), 'layer'); return LayerVersion.fromLayerVersionArn( scope, `${functionName}-${layerName}-layer`, - arn + layerArn.toString() ); }); }; diff --git a/packages/backend/API.md b/packages/backend/API.md index 3477ad55ee..1cb83ee09f 100644 --- a/packages/backend/API.md +++ b/packages/backend/API.md @@ -26,7 +26,6 @@ import { defineStorage } from '@aws-amplify/backend-storage'; import { FunctionResources } from '@aws-amplify/plugin-types'; import { GenerateContainerEntryProps } from '@aws-amplify/plugin-types'; import { ImportPathVerifier } from '@aws-amplify/plugin-types'; -import { referenceFunctionLayer } from '@aws-amplify/backend-function'; import { ResourceAccessAcceptorFactory } from '@aws-amplify/plugin-types'; import { ResourceProvider } from '@aws-amplify/plugin-types'; import { SsmEnvironmentEntriesGenerator } from '@aws-amplify/plugin-types'; @@ -90,8 +89,6 @@ export { GenerateContainerEntryProps } export { ImportPathVerifier } -export { referenceFunctionLayer } - export { ResourceProvider } // @public diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 592055dc9b..27f6e4580f 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -15,7 +15,4 @@ export { defineAuth } from '@aws-amplify/backend-auth'; export { defineStorage } from '@aws-amplify/backend-storage'; // function -export { - defineFunction, - referenceFunctionLayer, -} from '@aws-amplify/backend-function'; +export { defineFunction } from '@aws-amplify/backend-function'; From 2520aebbf2718908588699917c079e544303ff90 Mon Sep 17 00:00:00 2001 From: ykethan Date: Thu, 8 Aug 2024 19:33:44 -0400 Subject: [PATCH 05/16] change set --- .changeset/cyan-ways-sin.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/cyan-ways-sin.md diff --git a/.changeset/cyan-ways-sin.md b/.changeset/cyan-ways-sin.md new file mode 100644 index 0000000000..35fd83121f --- /dev/null +++ b/.changeset/cyan-ways-sin.md @@ -0,0 +1,6 @@ +--- +'@aws-amplify/backend-function': minor +'@aws-amplify/backend': minor +--- + +adds ability to add layers to a function From 539083fdc6407b50644f9076877839623c0cb2ba Mon Sep 17 00:00:00 2001 From: ykethan Date: Fri, 9 Aug 2024 17:32:44 -0400 Subject: [PATCH 06/16] update error to Amplify error type --- .../src/reference_layer.test.ts | 22 ++++++++++++++----- .../backend-function/src/reference_layer.ts | 17 ++++++++------ packages/backend/src/index.ts | 4 ++-- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/backend-function/src/reference_layer.test.ts b/packages/backend-function/src/reference_layer.test.ts index 1c45479c3e..825dcfd0aa 100644 --- a/packages/backend-function/src/reference_layer.test.ts +++ b/packages/backend-function/src/reference_layer.test.ts @@ -4,6 +4,7 @@ import { ResourceNameValidatorStub, StackResolverStub, } from '@aws-amplify/backend-platform-test-stubs'; +import { AmplifyUserError } from '@aws-amplify/platform-core'; import { ConstructFactoryGetInstanceProps, ResourceNameValidator, @@ -11,7 +12,6 @@ import { import { App, Stack } from 'aws-cdk-lib'; import { Template } from 'aws-cdk-lib/assertions'; import assert from 'node:assert'; -import { EOL } from 'node:os'; import { beforeEach, describe, it } from 'node:test'; import { defineFunction } from './factory.js'; @@ -102,9 +102,14 @@ void describe('AmplifyFunctionFactory - Layers', () => { }); assert.throws( () => functionFactory.getInstance(getInstanceProps), - new Error( - `Invalid ARN format for layer: ${invalidLayerArn} ${EOL} Expected format: arn:aws:lambda:::layer::` - ) + (error: AmplifyUserError) => { + assert.strictEqual( + error.message, + `Invalid ARN format for layer: ${invalidLayerArn}` + ); + assert.ok(error.resolution); + return true; + } ); }); @@ -133,7 +138,14 @@ void describe('AmplifyFunctionFactory - Layers', () => { assert.throws( () => functionFactory.getInstance(getInstanceProps), - new Error('A maximum of 5 unique layers can be attached to a function.') + (error: AmplifyUserError) => { + assert.strictEqual( + error.message, + `A maximum of 5 unique layers can be attached to a function.` + ); + assert.ok(error.resolution); + return true; + } ); }); }); diff --git a/packages/backend-function/src/reference_layer.ts b/packages/backend-function/src/reference_layer.ts index d54eabf3e7..fb5a141326 100644 --- a/packages/backend-function/src/reference_layer.ts +++ b/packages/backend-function/src/reference_layer.ts @@ -1,7 +1,7 @@ +import { AmplifyUserError } from '@aws-amplify/platform-core'; import { ILayerVersion, LayerVersion } from 'aws-cdk-lib/aws-lambda'; import { Arn } from 'aws-cdk-lib/core'; import { Construct } from 'constructs'; -import { EOL } from 'os'; /** * Defines the ARN regex pattern for a layer. @@ -28,9 +28,11 @@ export class FunctionLayerArn { */ constructor(arn: string) { if (!isValidLayerArn(arn)) { - throw new Error( - `Invalid ARN format for layer: ${arn} ${EOL} Expected format: arn:aws:lambda:::layer::` - ); + throw new AmplifyUserError('InvalidLayerArnFormatError', { + message: `Invalid ARN format for layer: ${arn}`, + resolution: + 'Update the layer Arn with the expected format: arn:aws:lambda:::layer::', + }); } this.arn = arn; } @@ -59,9 +61,10 @@ export const validateLayers = ( const uniqueArns = new Set(Object.values(layers)); if (uniqueArns.size > 5) { - throw new Error( - 'A maximum of 5 unique layers can be attached to a function.' - ); + throw new AmplifyUserError('MaximumLayersReachedError', { + message: 'A maximum of 5 unique layers can be attached to a function.', + resolution: 'Remove unused layers in your function', + }); } return new Set( diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 27f6e4580f..039b5de54c 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,12 +1,12 @@ -export * from './backend.js'; export { defineBackend } from './backend_factory.js'; +export * from './backend.js'; export * from './secret.js'; // re-export core functionality from category packages // data export { defineData } from '@aws-amplify/backend-data'; -export { a, type ClientSchema } from '@aws-amplify/data-schema'; +export { type ClientSchema, a } from '@aws-amplify/data-schema'; // auth export { defineAuth } from '@aws-amplify/backend-auth'; From aa26f26b8f7a807019792081d7bd1812d9bc70eb Mon Sep 17 00:00:00 2001 From: ykethan Date: Sun, 11 Aug 2024 14:14:59 -0400 Subject: [PATCH 07/16] update jsdocs --- .changeset/calm-buttons-sort.md | 6 ------ .changeset/cyan-ways-sin.md | 6 ------ .changeset/serious-dragons-attack.md | 5 +++++ packages/backend-function/src/factory.ts | 10 ++++------ 4 files changed, 9 insertions(+), 18 deletions(-) delete mode 100644 .changeset/calm-buttons-sort.md delete mode 100644 .changeset/cyan-ways-sin.md create mode 100644 .changeset/serious-dragons-attack.md diff --git a/.changeset/calm-buttons-sort.md b/.changeset/calm-buttons-sort.md deleted file mode 100644 index fddb681941..0000000000 --- a/.changeset/calm-buttons-sort.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@aws-amplify/backend-function': minor -'@aws-amplify/backend': minor ---- - -adds referenceLayer to function diff --git a/.changeset/cyan-ways-sin.md b/.changeset/cyan-ways-sin.md deleted file mode 100644 index 35fd83121f..0000000000 --- a/.changeset/cyan-ways-sin.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@aws-amplify/backend-function': minor -'@aws-amplify/backend': minor ---- - -adds ability to add layers to a function diff --git a/.changeset/serious-dragons-attack.md b/.changeset/serious-dragons-attack.md new file mode 100644 index 0000000000..839daf7664 --- /dev/null +++ b/.changeset/serious-dragons-attack.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-function': minor +--- + +adds layers to defineFunction diff --git a/packages/backend-function/src/factory.ts b/packages/backend-function/src/factory.ts index 4091d43554..ada5142b88 100644 --- a/packages/backend-function/src/factory.ts +++ b/packages/backend-function/src/factory.ts @@ -126,15 +126,13 @@ export type FunctionProps = { /** * Attach Lambda layers to a function + * @see [Lambda layers](https://docs.aws.amazon.com/lambda/latest/dg/chapter-layers.html) * @example - * import { referenceFunctionLayer } from "@aws-amplify/backend"; - * * layers: { - * myModule: referenceFunctionLayer("arn:aws:lambda:::layer:myLayer:1") + * "@aws-lambda-powertools/logger": "arn:aws:lambda::094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:11" * }, - * - * The object is keyed by the module name hosted on your existing layer and a value that references to an existing layer using an ARN. The keys will be externalized and available via your layer at runtime - * Maximum of 5 layers can be attached to a function. Layers must be in the same region as the function. + * @description The object is keyed by the module name hosted on your existing layer and a value that references to an existing layer using an ARN. The keys will be externalized and available via your layer at runtime + * @description Maximum of 5 layers can be attached to a function and must be in the same region as the function. */ layers?: FunctionLayerReferences; }; From 4c20954004346c02365d6cf83fa5503cf6f1d788 Mon Sep 17 00:00:00 2001 From: ykethan Date: Wed, 14 Aug 2024 11:31:46 -0400 Subject: [PATCH 08/16] update jsdoc --- packages/backend-function/src/factory.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/backend-function/src/factory.ts b/packages/backend-function/src/factory.ts index ada5142b88..dbeb35f4e1 100644 --- a/packages/backend-function/src/factory.ts +++ b/packages/backend-function/src/factory.ts @@ -126,13 +126,15 @@ export type FunctionProps = { /** * Attach Lambda layers to a function - * @see [Lambda layers](https://docs.aws.amazon.com/lambda/latest/dg/chapter-layers.html) + * @see [Amplify documentation for Lambda layers](https://docs.amplify.aws/react/build-a-backend/functions/lambda-layers) + * @see [AWS documentation for Lambda layers](https://docs.aws.amazon.com/lambda/latest/dg/chapter-layers.html) + * + * - The object is keyed by the module name hosted on your existing layer and a value that references to an existing layer using an ARN. The keys will be externalized and available via your layer at runtime + * - Maximum of 5 layers can be attached to a function and must be in the same region as the function. * @example * layers: { * "@aws-lambda-powertools/logger": "arn:aws:lambda::094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:11" * }, - * @description The object is keyed by the module name hosted on your existing layer and a value that references to an existing layer using an ARN. The keys will be externalized and available via your layer at runtime - * @description Maximum of 5 layers can be attached to a function and must be in the same region as the function. */ layers?: FunctionLayerReferences; }; From 2904601294700c75b5bf208c0ce4d669034c9008 Mon Sep 17 00:00:00 2001 From: ykethan Date: Wed, 14 Aug 2024 14:13:10 -0400 Subject: [PATCH 09/16] change jsdoc --- packages/backend-function/src/factory.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/backend-function/src/factory.ts b/packages/backend-function/src/factory.ts index dbeb35f4e1..bac86a1006 100644 --- a/packages/backend-function/src/factory.ts +++ b/packages/backend-function/src/factory.ts @@ -126,11 +126,10 @@ export type FunctionProps = { /** * Attach Lambda layers to a function - * @see [Amplify documentation for Lambda layers](https://docs.amplify.aws/react/build-a-backend/functions/lambda-layers) - * @see [AWS documentation for Lambda layers](https://docs.aws.amazon.com/lambda/latest/dg/chapter-layers.html) - * * - The object is keyed by the module name hosted on your existing layer and a value that references to an existing layer using an ARN. The keys will be externalized and available via your layer at runtime * - Maximum of 5 layers can be attached to a function and must be in the same region as the function. + * @see [Amplify documentation for Lambda layers](https://docs.amplify.aws/react/build-a-backend/functions/add-lambda-layers) + * @see [AWS documentation for Lambda layers](https://docs.aws.amazon.com/lambda/latest/dg/chapter-layers.html) * @example * layers: { * "@aws-lambda-powertools/logger": "arn:aws:lambda::094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:11" From ad8eac3aa42052be7a5731b5085fcfedf8afaf57 Mon Sep 17 00:00:00 2001 From: ykethan Date: Mon, 26 Aug 2024 13:16:12 -0400 Subject: [PATCH 10/16] update layers --- packages/backend-function/src/factory.ts | 38 +++++--- ...nce_layer.test.ts => layer_parser.test.ts} | 0 packages/backend-function/src/layer_parser.ts | 60 ++++++++++++ .../backend-function/src/reference_layer.ts | 92 ------------------- 4 files changed, 83 insertions(+), 107 deletions(-) rename packages/backend-function/src/{reference_layer.test.ts => layer_parser.test.ts} (100%) create mode 100644 packages/backend-function/src/layer_parser.ts delete mode 100644 packages/backend-function/src/reference_layer.ts diff --git a/packages/backend-function/src/factory.ts b/packages/backend-function/src/factory.ts index bac86a1006..f1c4602bf7 100644 --- a/packages/backend-function/src/factory.ts +++ b/packages/backend-function/src/factory.ts @@ -22,7 +22,12 @@ import { Duration, Stack, Tags } from 'aws-cdk-lib'; import { Rule } from 'aws-cdk-lib/aws-events'; import * as targets from 'aws-cdk-lib/aws-events-targets'; import { Policy } from 'aws-cdk-lib/aws-iam'; -import { CfnFunction, ILayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'; +import { + CfnFunction, + ILayerVersion, + LayerVersion, + Runtime, +} from 'aws-cdk-lib/aws-lambda'; import { NodejsFunction, OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs'; import { Construct } from 'constructs'; import { readFileSync } from 'fs'; @@ -33,7 +38,7 @@ import * as path from 'path'; import { FunctionEnvironmentTranslator } from './function_env_translator.js'; import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.js'; import { getCallerDirectory } from './get_caller_directory.js'; -import { resolveLayers } from './reference_layer.js'; +import { FunctionLayerArnParser } from './layer_parser.js'; import { convertFunctionSchedulesToRuleSchedules } from './schedule_parser.js'; const functionStackType = 'function-Lambda'; @@ -53,7 +58,6 @@ export type TimeInterval = | `every month` | `every year`; export type FunctionSchedule = TimeInterval | CronSchedule; -export type FunctionLayerReferences = Record; /** * Entry point for defining a function in the Amplify ecosystem @@ -126,16 +130,16 @@ export type FunctionProps = { /** * Attach Lambda layers to a function - * - The object is keyed by the module name hosted on your existing layer and a value that references to an existing layer using an ARN. The keys will be externalized and available via your layer at runtime + * - A Lambda layer is represented by an object of key/value pair where the key is the module name that is exported from your layer and the value is the ARN of the layer. The key (module name) is used to externalize the module dependency so it doesn't get bundled with your lambda function * - Maximum of 5 layers can be attached to a function and must be in the same region as the function. - * @see [Amplify documentation for Lambda layers](https://docs.amplify.aws/react/build-a-backend/functions/add-lambda-layers) - * @see [AWS documentation for Lambda layers](https://docs.aws.amazon.com/lambda/latest/dg/chapter-layers.html) * @example * layers: { * "@aws-lambda-powertools/logger": "arn:aws:lambda::094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:11" * }, + * @see [Amplify documentation for Lambda layers](https://docs.amplify.aws/react/build-a-backend/functions/add-lambda-layers) + * @see [AWS documentation for Lambda layers](https://docs.aws.amazon.com/lambda/latest/dg/chapter-layers.html) */ - layers?: FunctionLayerReferences; + layers?: Record; }; /** @@ -173,6 +177,8 @@ class FunctionFactory implements ConstructFactory { ): HydratedFunctionProps => { const name = this.resolveName(); resourceNameValidator?.validate(name); + const parser = new FunctionLayerArnParser(); + const layers = parser.parseLayers(this.props.layers ?? {}, name); return { name, entry: this.resolveEntry(), @@ -181,7 +187,7 @@ class FunctionFactory implements ConstructFactory { environment: this.props.environment ?? {}, runtime: this.resolveRuntime(), schedule: this.resolveSchedule(), - layers: this.props.layers ?? {}, + layers, }; }; @@ -281,9 +287,7 @@ class FunctionFactory implements ConstructFactory { }; } -type HydratedFunctionProps = Required & { - layers: FunctionLayerReferences; -}; +type HydratedFunctionProps = Required; class FunctionGenerator implements ConstructContainerEntryGenerator { readonly resourceGroupName = 'function'; @@ -297,11 +301,15 @@ class FunctionGenerator implements ConstructContainerEntryGenerator { scope, backendSecretResolver, }: GenerateContainerEntryProps) => { - const resolvedLayers = resolveLayers( - this.props.layers, - scope, - this.props.name + // resolve layers to LayerVersion objects for the NodejsFunction constructor using the scope. + const resolvedLayers = Object.entries(this.props.layers).map(([key, arn]) => + LayerVersion.fromLayerVersionArn( + scope, + `${this.props.name}-${key}-layer`, + arn + ) ); + return new AmplifyFunction( scope, this.props.name, diff --git a/packages/backend-function/src/reference_layer.test.ts b/packages/backend-function/src/layer_parser.test.ts similarity index 100% rename from packages/backend-function/src/reference_layer.test.ts rename to packages/backend-function/src/layer_parser.test.ts diff --git a/packages/backend-function/src/layer_parser.ts b/packages/backend-function/src/layer_parser.ts new file mode 100644 index 0000000000..de51982744 --- /dev/null +++ b/packages/backend-function/src/layer_parser.ts @@ -0,0 +1,60 @@ +import { AmplifyUserError } from '@aws-amplify/platform-core'; + +/** + * Parses Lambda Layer ARNs for a function + */ +export class FunctionLayerArnParser { + private arnPattern = new RegExp( + 'arn:[a-zA-Z0-9-]+:lambda:[a-zA-Z0-9-]+:\\d{12}:layer:[a-zA-Z0-9-_]+:[0-9]+' + ); + + /** + * Parse the layers for a function + * @param layers - Layers to be attached to the function + * @param functionName - Name of the function + * @returns Valid layers for the function + * @throws AmplifyUserError if the layer ARN is invalid + * @throws AmplifyUserError if the number of layers exceeds the limit + */ + parseLayers( + layers: Record, + functionName: string + ): Record { + const validLayers: Record = {}; + + const uniqueArns = new Set(Object.values(layers)); + this.validateLayerCount(uniqueArns); + + for (const [key, arn] of Object.entries(layers)) { + if (!this.isValidLayerArn(arn)) { + throw new AmplifyUserError('InvalidLayerArnFormatError', { + message: `Invalid ARN format for layer: ${arn}`, + resolution: `Update the layer ARN with the expected format: arn:aws:lambda:::layer:: for function: ${functionName}`, + }); + } + validLayers[key] = arn; + } + + return validLayers; + } + + /** + * Validate the ARN format for a Lambda Layer + */ + private isValidLayerArn(arn: string): boolean { + return this.arnPattern.test(arn); + } + + /** + * Validate the number of layers attached to a function + * @see https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution + */ + private validateLayerCount(uniqueArns: Set): void { + if (uniqueArns.size > 5) { + throw new AmplifyUserError('MaximumLayersReachedError', { + message: 'A maximum of 5 unique layers can be attached to a function.', + resolution: 'Remove unused layers in your function', + }); + } + } +} diff --git a/packages/backend-function/src/reference_layer.ts b/packages/backend-function/src/reference_layer.ts deleted file mode 100644 index fb5a141326..0000000000 --- a/packages/backend-function/src/reference_layer.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { AmplifyUserError } from '@aws-amplify/platform-core'; -import { ILayerVersion, LayerVersion } from 'aws-cdk-lib/aws-lambda'; -import { Arn } from 'aws-cdk-lib/core'; -import { Construct } from 'constructs'; - -/** - * Defines the ARN regex pattern for a layer. - */ -export const arnPattern = new RegExp( - '^(arn:(aws[a-zA-Z-]*)?:lambda:[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:\\d{12}:layer:[a-zA-Z0-9-_]+:[0-9]+)|(arn:[a-zA-Z0-9-]+:lambda:::awslayer:[a-zA-Z0-9-_]+)$' -); - -/** - * Checks if the provided ARN is valid. - */ -export const isValidLayerArn = (arn: string): boolean => { - return arnPattern.test(arn); -}; - -/** - * Class to represent and handle Lambda Layer ARNs. - */ -export class FunctionLayerArn { - public readonly arn: string; - - /** - * Creates an instance of FunctionLayerArn. - */ - constructor(arn: string) { - if (!isValidLayerArn(arn)) { - throw new AmplifyUserError('InvalidLayerArnFormatError', { - message: `Invalid ARN format for layer: ${arn}`, - resolution: - 'Update the layer Arn with the expected format: arn:aws:lambda:::layer::', - }); - } - this.arn = arn; - } - - /** - * Converts the FunctionLayerArn to a string. - */ - toString(): string { - return this.arn; - } -} - -/** - * Parses a string to create a FunctionLayerArn instance. - */ -export const parseFunctionLayerArn = (arn: string): FunctionLayerArn => { - return new FunctionLayerArn(arn); -}; - -/** - * Validates the provided layer ARNs and throws an error if any are invalid or if there are more than 5 unique ARNs. - */ -export const validateLayers = ( - layers: Record -): Set => { - const uniqueArns = new Set(Object.values(layers)); - - if (uniqueArns.size > 5) { - throw new AmplifyUserError('MaximumLayersReachedError', { - message: 'A maximum of 5 unique layers can be attached to a function.', - resolution: 'Remove unused layers in your function', - }); - } - - return new Set( - Array.from(uniqueArns).map(parseFunctionLayerArn) - ); -}; - -/** - * Resolves and returns the layers for an AWS Lambda function. - */ -export const resolveLayers = ( - layers: Record, - scope: Construct, - functionName: string -): ILayerVersion[] => { - const uniqueLayerArns = validateLayers(layers); - return Array.from(uniqueLayerArns).map((layerArn) => { - const layerName = Arn.extractResourceName(layerArn.toString(), 'layer'); - return LayerVersion.fromLayerVersionArn( - scope, - `${functionName}-${layerName}-layer`, - layerArn.toString() - ); - }); -}; From 11d92f7ed0efbba7dd53e1de24c7a2a9d0b86c54 Mon Sep 17 00:00:00 2001 From: ykethan Date: Mon, 26 Aug 2024 13:20:40 -0400 Subject: [PATCH 11/16] update layers --- packages/backend-function/API.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/backend-function/API.md b/packages/backend-function/API.md index deed77ae53..0d380c2b49 100644 --- a/packages/backend-function/API.md +++ b/packages/backend-function/API.md @@ -21,9 +21,6 @@ export type CronSchedule = `${string} ${string} ${string} ${string} ${string}` | // @public export const defineFunction: (props?: FunctionProps) => ConstructFactory & ResourceAccessAcceptorFactory & AddEnvironmentFactory>; -// @public (undocumented) -export type FunctionLayerReferences = Record; - // @public (undocumented) export type FunctionProps = { name?: string; @@ -33,7 +30,7 @@ export type FunctionProps = { environment?: Record; runtime?: NodeVersion; schedule?: FunctionSchedule | FunctionSchedule[]; - layers?: FunctionLayerReferences; + layers?: Record; }; // @public (undocumented) From 286757f73174729eb5a46a18f023240d031fb72a Mon Sep 17 00:00:00 2001 From: ykethan Date: Wed, 28 Aug 2024 13:06:44 -0400 Subject: [PATCH 12/16] update to check for unqiue arns and adds a test --- .changeset/serious-dragons-attack.md | 2 +- .../backend-function/src/layer_parser.test.ts | 26 +++++++++++++++++++ packages/backend-function/src/layer_parser.ts | 16 +++++++++--- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/.changeset/serious-dragons-attack.md b/.changeset/serious-dragons-attack.md index 839daf7664..b38b05d24a 100644 --- a/.changeset/serious-dragons-attack.md +++ b/.changeset/serious-dragons-attack.md @@ -2,4 +2,4 @@ '@aws-amplify/backend-function': minor --- -adds layers to defineFunction +adds support to reference existing layers in defineFunction diff --git a/packages/backend-function/src/layer_parser.test.ts b/packages/backend-function/src/layer_parser.test.ts index 825dcfd0aa..4d1e8ea5cc 100644 --- a/packages/backend-function/src/layer_parser.test.ts +++ b/packages/backend-function/src/layer_parser.test.ts @@ -148,4 +148,30 @@ void describe('AmplifyFunctionFactory - Layers', () => { } ); }); + + void it('checks if only unique Arns are being used', () => { + const duplicateArn = + 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer:1'; + const functionFactory = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'lambdaWithDuplicateLayers', + layers: { + layer1: duplicateArn, + layer2: duplicateArn, + layer3: duplicateArn, + layer4: duplicateArn, + layer5: duplicateArn, + layer6: duplicateArn, + }, + }); + + const lambda = functionFactory.getInstance(getInstanceProps); + const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'index.handler', + Layers: [duplicateArn], + }); + }); }); diff --git a/packages/backend-function/src/layer_parser.ts b/packages/backend-function/src/layer_parser.ts index de51982744..2adad682ff 100644 --- a/packages/backend-function/src/layer_parser.ts +++ b/packages/backend-function/src/layer_parser.ts @@ -21,9 +21,7 @@ export class FunctionLayerArnParser { functionName: string ): Record { const validLayers: Record = {}; - - const uniqueArns = new Set(Object.values(layers)); - this.validateLayerCount(uniqueArns); + const uniqueArns = new Map(); for (const [key, arn] of Object.entries(layers)) { if (!this.isValidLayerArn(arn)) { @@ -32,9 +30,19 @@ export class FunctionLayerArnParser { resolution: `Update the layer ARN with the expected format: arn:aws:lambda:::layer:: for function: ${functionName}`, }); } - validLayers[key] = arn; + // Add ARN to the Map with the first encountered key + if (!uniqueArns.has(arn)) { + uniqueArns.set(arn, key); + } } + this.validateLayerCount(new Set(uniqueArns.keys())); + + // Construct validLayers with the unique ARNs and their associated keys + uniqueArns.forEach((key, arn) => { + validLayers[key] = arn; + }); + return validLayers; } From 18d5bbbc658d39f49f8237cabd28bbb5a41d4c3e Mon Sep 17 00:00:00 2001 From: ykethan Date: Wed, 28 Aug 2024 14:45:24 -0400 Subject: [PATCH 13/16] update to use set --- packages/backend-function/src/layer_parser.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/backend-function/src/layer_parser.ts b/packages/backend-function/src/layer_parser.ts index 2adad682ff..f90ba68e21 100644 --- a/packages/backend-function/src/layer_parser.ts +++ b/packages/backend-function/src/layer_parser.ts @@ -21,7 +21,7 @@ export class FunctionLayerArnParser { functionName: string ): Record { const validLayers: Record = {}; - const uniqueArns = new Map(); + const uniqueArns = new Set(); for (const [key, arn] of Object.entries(layers)) { if (!this.isValidLayerArn(arn)) { @@ -30,18 +30,16 @@ export class FunctionLayerArnParser { resolution: `Update the layer ARN with the expected format: arn:aws:lambda:::layer:: for function: ${functionName}`, }); } - // Add ARN to the Map with the first encountered key + + // Add to validLayers and uniqueArns only if the ARN hasn't been added already if (!uniqueArns.has(arn)) { - uniqueArns.set(arn, key); + uniqueArns.add(arn); + validLayers[key] = arn; } } - this.validateLayerCount(new Set(uniqueArns.keys())); - - // Construct validLayers with the unique ARNs and their associated keys - uniqueArns.forEach((key, arn) => { - validLayers[key] = arn; - }); + // Validate the number of unique layers + this.validateLayerCount(uniqueArns); return validLayers; } From ad96a04736cd02f5bea890f4ddc1b7416ede304e Mon Sep 17 00:00:00 2001 From: ykethan Date: Wed, 2 Oct 2024 15:39:35 -0400 Subject: [PATCH 14/16] fix api.md error --- packages/client-config/API.md | 2 +- packages/platform-core/API.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/client-config/API.md b/packages/client-config/API.md index 397419f74f..55c083d3e7 100644 --- a/packages/client-config/API.md +++ b/packages/client-config/API.md @@ -473,7 +473,7 @@ export type CustomClientConfig = { export const DEFAULT_CLIENT_CONFIG_VERSION: ClientConfigVersion; // @public -export const generateClientConfig: (backendIdentifier: DeployedBackendIdentifier, version: T, awsClientProvider?: AWSClientProvider<{ +export const generateClientConfig: (backendIdentifier: DeployedBackendIdentifier, version: T, awsClientProvider?: AWSClientProvider<{ getS3Client: S3Client; getAmplifyClient: AmplifyClient; getCloudFormationClient: CloudFormationClient; diff --git a/packages/platform-core/API.md b/packages/platform-core/API.md index 928d4ada67..8e70408201 100644 --- a/packages/platform-core/API.md +++ b/packages/platform-core/API.md @@ -164,13 +164,13 @@ export const packageJsonSchema: z.ZodObject<{ version: z.ZodOptional; type: z.ZodOptional, z.ZodLiteral<"commonjs">]>>; }, "strip", z.ZodTypeAny, { - name?: string | undefined; - type?: "module" | "commonjs" | undefined; version?: string | undefined; -}, { - name?: string | undefined; type?: "module" | "commonjs" | undefined; + name?: string | undefined; +}, { version?: string | undefined; + type?: "module" | "commonjs" | undefined; + name?: string | undefined; }>; // @public From 097b987d1f13f108abdb934b9318969538e41545 Mon Sep 17 00:00:00 2001 From: ykethan Date: Wed, 2 Oct 2024 16:11:02 -0400 Subject: [PATCH 15/16] rm unrelated api.md changes --- packages/client-config/API.md | 2 +- packages/platform-core/API.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/client-config/API.md b/packages/client-config/API.md index 55c083d3e7..397419f74f 100644 --- a/packages/client-config/API.md +++ b/packages/client-config/API.md @@ -473,7 +473,7 @@ export type CustomClientConfig = { export const DEFAULT_CLIENT_CONFIG_VERSION: ClientConfigVersion; // @public -export const generateClientConfig: (backendIdentifier: DeployedBackendIdentifier, version: T, awsClientProvider?: AWSClientProvider<{ +export const generateClientConfig: (backendIdentifier: DeployedBackendIdentifier, version: T, awsClientProvider?: AWSClientProvider<{ getS3Client: S3Client; getAmplifyClient: AmplifyClient; getCloudFormationClient: CloudFormationClient; diff --git a/packages/platform-core/API.md b/packages/platform-core/API.md index 8e70408201..928d4ada67 100644 --- a/packages/platform-core/API.md +++ b/packages/platform-core/API.md @@ -164,13 +164,13 @@ export const packageJsonSchema: z.ZodObject<{ version: z.ZodOptional; type: z.ZodOptional, z.ZodLiteral<"commonjs">]>>; }, "strip", z.ZodTypeAny, { - version?: string | undefined; - type?: "module" | "commonjs" | undefined; name?: string | undefined; -}, { - version?: string | undefined; type?: "module" | "commonjs" | undefined; + version?: string | undefined; +}, { name?: string | undefined; + type?: "module" | "commonjs" | undefined; + version?: string | undefined; }>; // @public From 9f80bfed903d633035f06875e5e40bf683b5e039 Mon Sep 17 00:00:00 2001 From: ykethan Date: Wed, 2 Oct 2024 16:50:56 -0400 Subject: [PATCH 16/16] fix api.md --- .changeset/strong-flowers-warn.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/strong-flowers-warn.md diff --git a/.changeset/strong-flowers-warn.md b/.changeset/strong-flowers-warn.md new file mode 100644 index 0000000000..58648b8d7c --- /dev/null +++ b/.changeset/strong-flowers-warn.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend': minor +--- + +adds support to reference existing layers in defineFunction