Skip to content

Commit 560878f

Browse files
authored
updates layer to also use layername:version (#2316)
* updates layer to also use layername:version
1 parent 3cf0738 commit 560878f

File tree

4 files changed

+147
-21
lines changed

4 files changed

+147
-21
lines changed

.changeset/silent-files-appear.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@aws-amplify/backend-function': minor
3+
'@aws-amplify/backend': minor
4+
---
5+
6+
updates layer to also use layername:version

packages/backend-function/src/factory.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ import * as path from 'path';
4646
import { FunctionEnvironmentTranslator } from './function_env_translator.js';
4747
import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.js';
4848
import { FunctionLayerArnParser } from './layer_parser.js';
49-
import { convertFunctionSchedulesToRuleSchedules } from './schedule_parser.js';
5049
import { convertLoggingOptionsToCDK } from './logging_options_parser.js';
50+
import { convertFunctionSchedulesToRuleSchedules } from './schedule_parser.js';
5151

5252
const functionStackType = 'function-Lambda';
5353

@@ -148,6 +148,11 @@ export type FunctionProps = {
148148
* layers: {
149149
* "@aws-lambda-powertools/logger": "arn:aws:lambda:<current-region>:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:11"
150150
* },
151+
* or
152+
* @example
153+
* layers: {
154+
* "Sharp": "SharpLayer:1"
155+
* },
151156
* @see [Amplify documentation for Lambda layers](https://docs.amplify.aws/react/build-a-backend/functions/add-lambda-layers)
152157
* @see [AWS documentation for Lambda layers](https://docs.aws.amazon.com/lambda/latest/dg/chapter-layers.html)
153158
*/
@@ -225,8 +230,7 @@ class FunctionFactory implements ConstructFactory<AmplifyFunction> {
225230
): HydratedFunctionProps => {
226231
const name = this.resolveName();
227232
resourceNameValidator?.validate(name);
228-
const parser = new FunctionLayerArnParser();
229-
const layers = parser.parseLayers(this.props.layers ?? {}, name);
233+
230234
return {
231235
name,
232236
entry: this.resolveEntry(),
@@ -236,7 +240,7 @@ class FunctionFactory implements ConstructFactory<AmplifyFunction> {
236240
runtime: this.resolveRuntime(),
237241
schedule: this.resolveSchedule(),
238242
bundling: this.resolveBundling(),
239-
layers,
243+
layers: this.props.layers ?? {},
240244
resourceGroupName: this.props.resourceGroupName ?? 'function',
241245
logging: this.props.logging ?? {},
242246
};
@@ -413,8 +417,18 @@ class FunctionGenerator implements ConstructContainerEntryGenerator {
413417
scope,
414418
backendSecretResolver,
415419
}: GenerateContainerEntryProps) => {
416-
// resolve layers to LayerVersion objects for the NodejsFunction constructor using the scope.
417-
const resolvedLayers = Object.entries(this.props.layers).map(([key, arn]) =>
420+
// Move layer resolution here where we have access to scope
421+
const parser = new FunctionLayerArnParser(
422+
Stack.of(scope).region,
423+
Stack.of(scope).account
424+
);
425+
const resolvedLayerArns = parser.parseLayers(
426+
this.props.layers ?? {},
427+
this.props.name
428+
);
429+
430+
// resolve layers to LayerVersion objects for the NodejsFunction constructor
431+
const resolvedLayers = Object.entries(resolvedLayerArns).map(([key, arn]) =>
418432
LayerVersion.fromLayerVersionArn(
419433
scope,
420434
`${this.props.name}-${key}-layer`,

packages/backend-function/src/layer_parser.test.ts

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ import {
1010
ResourceNameValidator,
1111
} from '@aws-amplify/plugin-types';
1212
import { App, Stack } from 'aws-cdk-lib';
13-
import { Template } from 'aws-cdk-lib/assertions';
13+
import { Match, Template } from 'aws-cdk-lib/assertions';
14+
import fsp from 'fs/promises';
1415
import assert from 'node:assert';
16+
import path from 'node:path';
1517
import { after, beforeEach, describe, it } from 'node:test';
1618
import { defineFunction } from './factory.js';
17-
import path from 'node:path';
18-
import fsp from 'fs/promises';
1919

2020
const createStackAndSetContext = (): Stack => {
2121
const app = new App();
@@ -115,7 +115,7 @@ void describe('AmplifyFunctionFactory - Layers', () => {
115115
(error: AmplifyUserError) => {
116116
assert.strictEqual(
117117
error.message,
118-
`Invalid ARN format for layer: ${invalidLayerArn}`
118+
`Invalid format for layer: ${invalidLayerArn}`
119119
);
120120
assert.ok(error.resolution);
121121
return true;
@@ -184,4 +184,77 @@ void describe('AmplifyFunctionFactory - Layers', () => {
184184
Layers: [duplicateArn],
185185
});
186186
});
187+
188+
void it('accepts and converts name:version format to ARN', () => {
189+
const functionFactory = defineFunction({
190+
entry: './test-assets/default-lambda/handler.ts',
191+
name: 'lambdaWithNameVersionLayer',
192+
layers: {
193+
myLayer: 'my-layer:1',
194+
},
195+
});
196+
const lambda = functionFactory.getInstance(getInstanceProps);
197+
const template = Template.fromStack(Stack.of(lambda.resources.lambda));
198+
199+
template.hasResourceProperties('AWS::Lambda::Function', {
200+
Handler: 'index.handler',
201+
Layers: [
202+
{
203+
'Fn::Join': [
204+
'',
205+
[
206+
'arn:aws:lambda:',
207+
{ Ref: 'AWS::Region' },
208+
':',
209+
{ Ref: 'AWS::AccountId' },
210+
':layer:my-layer:1',
211+
],
212+
],
213+
},
214+
],
215+
});
216+
});
217+
218+
void it('throws an error for invalid name:version format', () => {
219+
const invalidFormat = 'my-layer'; // missing version number
220+
const functionFactory = defineFunction({
221+
entry: './test-assets/default-lambda/handler.ts',
222+
name: 'lambdaWithInvalidNameVersion',
223+
layers: {
224+
myLayer: invalidFormat,
225+
},
226+
});
227+
228+
assert.throws(
229+
() => functionFactory.getInstance(getInstanceProps),
230+
(error: AmplifyUserError) => {
231+
assert.strictEqual(
232+
error.message,
233+
`Invalid format for layer: ${invalidFormat}`
234+
);
235+
assert.ok(error.resolution);
236+
return true;
237+
}
238+
);
239+
});
240+
241+
void it('accepts mixed format of ARNs and name:version', () => {
242+
const fullArn =
243+
'arn:aws:lambda:us-east-1:123456789012:layer:full-arn-layer:1';
244+
const functionFactory = defineFunction({
245+
entry: './test-assets/default-lambda/handler.ts',
246+
name: 'lambdaWithMixedLayerFormats',
247+
layers: {
248+
fullArnLayer: fullArn,
249+
nameVersionLayer: 'name-version-layer:2',
250+
},
251+
});
252+
const lambda = functionFactory.getInstance(getInstanceProps);
253+
const template = Template.fromStack(Stack.of(lambda.resources.lambda));
254+
255+
template.hasResourceProperties('AWS::Lambda::Function', {
256+
Handler: 'index.handler',
257+
Layers: Match.arrayWith([fullArn]),
258+
});
259+
});
187260
});

packages/backend-function/src/layer_parser.ts

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,26 @@ export class FunctionLayerArnParser {
77
private arnPattern = new RegExp(
88
'arn:[a-zA-Z0-9-]+:lambda:[a-zA-Z0-9-]+:\\d{12}:layer:[a-zA-Z0-9-_]+:[0-9]+'
99
);
10+
private nameVersionPattern = new RegExp('^[a-zA-Z0-9-_]+:[0-9]+$');
11+
12+
/**
13+
* Creates a new FunctionLayerArnParser
14+
* @param region - AWS region
15+
* @param account - AWS account ID
16+
*/
17+
constructor(
18+
private readonly region: string,
19+
private readonly account: string
20+
) {}
1021

1122
/**
1223
* Parse the layers for a function
13-
* @param layers - Layers to be attached to the function
24+
* @param layers - Layers to be attached to the function. Each layer can be specified as either:
25+
* - A full ARN (arn:aws:lambda:<region>:<account>:layer:<name>:<version>)
26+
* - A name:version format (e.g., "my-layer:1")
1427
* @param functionName - Name of the function
15-
* @returns Valid layers for the function
16-
* @throws AmplifyUserError if the layer ARN is invalid
28+
* @returns Valid layers for the function with resolved ARNs
29+
* @throws AmplifyUserError if the layer ARN or name:version format is invalid
1730
* @throws AmplifyUserError if the number of layers exceeds the limit
1831
*/
1932
parseLayers(
@@ -23,36 +36,56 @@ export class FunctionLayerArnParser {
2336
const validLayers: Record<string, string> = {};
2437
const uniqueArns = new Set<string>();
2538

26-
for (const [key, arn] of Object.entries(layers)) {
27-
if (!this.isValidLayerArn(arn)) {
28-
throw new AmplifyUserError('InvalidLayerArnFormatError', {
29-
message: `Invalid ARN format for layer: ${arn}`,
30-
resolution: `Update the layer ARN with the expected format: arn:aws:lambda:<current-region>:<account-id>:layer:<layer-name>:<version> for function: ${functionName}`,
39+
for (const [key, value] of Object.entries(layers)) {
40+
let arn: string;
41+
42+
if (this.isValidLayerArn(value)) {
43+
// If it's already a valid ARN, use it as is
44+
arn = value;
45+
} else if (this.isValidNameVersion(value)) {
46+
// If it's in name:version format, construct the ARN using provided region and account
47+
const [name, version] = value.split(':');
48+
arn = `arn:aws:lambda:${this.region}:${this.account}:layer:${name}:${version}`;
49+
} else {
50+
throw new AmplifyUserError('InvalidLayerFormatError', {
51+
message: `Invalid format for layer: ${value}`,
52+
resolution: `Layer must be either a full ARN (arn:aws:lambda:<region>:<account>:layer:<name>:<version>) or name:version format for function: ${functionName}`,
3153
});
3254
}
3355

34-
// Add to validLayers and uniqueArns only if the ARN hasn't been added already
56+
// Ensure we don't add duplicate ARNs
3557
if (!uniqueArns.has(arn)) {
3658
uniqueArns.add(arn);
3759
validLayers[key] = arn;
3860
}
3961
}
4062

41-
// Validate the number of unique layers
4263
this.validateLayerCount(uniqueArns);
43-
4464
return validLayers;
4565
}
4666

4767
/**
4868
* Validate the ARN format for a Lambda Layer
69+
* @param arn - The ARN string to validate
70+
* @returns boolean indicating if the ARN format is valid
4971
*/
5072
private isValidLayerArn(arn: string): boolean {
5173
return this.arnPattern.test(arn);
5274
}
5375

76+
/**
77+
* Validate the name:version format for a Lambda Layer
78+
* @param value - The string to validate in format "name:version"
79+
* @returns boolean indicating if the format is valid
80+
*/
81+
private isValidNameVersion(value: string): boolean {
82+
return this.nameVersionPattern.test(value);
83+
}
84+
5485
/**
5586
* Validate the number of layers attached to a function
87+
* @param uniqueArns - Set of unique layer ARNs
88+
* @throws AmplifyUserError if the number of layers exceeds 5
5689
* @see https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution
5790
*/
5891
private validateLayerCount(uniqueArns: Set<string>): void {

0 commit comments

Comments
 (0)