Skip to content

Commit 3f521c3

Browse files
MarlonJDsobolkAmplifiyer
authored
Feature: custom runtimes to define function (#1602)
* feature(function): add custom provided function support to defineFunction * chore: add changeset * chore(function): fix e2ee path and changeset * chore(function): add new API.md * fix(function): fix e2ee tests for provided function Co-authored-by: Kamil Sobol <[email protected]> * chore: change changeset Co-authored-by: Kamil Sobol <[email protected]> * chore(function): add without docker error message Co-authored-by: Kamil Sobol <[email protected]> * chore(function): add provided function error message Co-authored-by: Kamil Sobol <[email protected]> * chore(function): add missing imports * chore(function): fix lint error * Update packages/backend-function/src/provided_function_factory.ts Co-authored-by: Amplifiyer <[email protected]> * Update packages/backend-function/src/provided_function_factory.ts Co-authored-by: Amplifiyer <[email protected]> * chore(function): fix lint issue --------- Co-authored-by: Kamil Sobol <[email protected]> Co-authored-by: Amplifiyer <[email protected]>
1 parent a04dbb8 commit 3f521c3

File tree

11 files changed

+365
-67
lines changed

11 files changed

+365
-67
lines changed

.changeset/long-berries-greet.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+
add custom provided function support to define function

packages/backend-function/API.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66

77
import { AmplifyResourceGroupName } from '@aws-amplify/plugin-types';
88
import { BackendSecret } from '@aws-amplify/plugin-types';
9+
import { Construct } from 'constructs';
910
import { ConstructFactory } from '@aws-amplify/plugin-types';
1011
import { FunctionResources } from '@aws-amplify/plugin-types';
12+
import { IFunction } from 'aws-cdk-lib/aws-lambda';
1113
import { LogLevel } from '@aws-amplify/plugin-types';
1214
import { LogRetention } from '@aws-amplify/plugin-types';
1315
import { ResourceAccessAcceptorFactory } from '@aws-amplify/plugin-types';
@@ -61,8 +63,11 @@ type DataClientError = {
6163
// @public (undocumented)
6264
type DataClientReturn<T> = T extends DataClientEnv ? DataClientConfig : DataClientError;
6365

64-
// @public
65-
export const defineFunction: (props?: FunctionProps) => ConstructFactory<ResourceProvider<FunctionResources> & ResourceAccessAcceptorFactory & AddEnvironmentFactory & StackProvider>;
66+
// @public (undocumented)
67+
export function defineFunction(props?: FunctionProps): ConstructFactory<ResourceProvider<FunctionResources> & ResourceAccessAcceptorFactory & AddEnvironmentFactory & StackProvider>;
68+
69+
// @public (undocumented)
70+
export function defineFunction(provider: (scope: Construct) => IFunction, providerProps?: ProvidedFunctionProps): ConstructFactory<ResourceProvider<FunctionResources> & ResourceAccessAcceptorFactory & StackProvider>;
6671

6772
// @public (undocumented)
6873
export type FunctionArchitecture = 'x86_64' | 'arm64';
@@ -135,6 +140,11 @@ type LibraryOptions = {
135140
// @public (undocumented)
136141
export type NodeVersion = 16 | 18 | 20 | 22;
137142

143+
// @public (undocumented)
144+
export type ProvidedFunctionProps = {
145+
resourceGroupName?: AmplifyResourceGroupName;
146+
};
147+
138148
// @public (undocumented)
139149
type ResourceConfig = {
140150
API: {

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam';
2424
import fsp from 'fs/promises';
2525
import path from 'node:path';
2626
import { AmplifyUserError } from '@aws-amplify/platform-core';
27+
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
2728

2829
const createStackAndSetContext = (): Stack => {
2930
const app = new App();
@@ -782,4 +783,35 @@ void describe('AmplifyFunctionFactory', () => {
782783
);
783784
});
784785
});
786+
787+
void describe('provided function runtime property', () => {
788+
void it('sets valid runtime', () => {
789+
const lambda = defineFunction((scope) => {
790+
return new NodejsFunction(scope, 'nodejs-provided', {
791+
entry:
792+
'./packages/backend-function/src/test-assets/default-lambda/handler.ts',
793+
runtime: Runtime.NODEJS_22_X,
794+
});
795+
}).getInstance(getInstanceProps);
796+
const template = Template.fromStack(lambda.stack);
797+
798+
template.hasResourceProperties('AWS::Lambda::Function', {
799+
Runtime: Runtime.NODEJS_22_X.name,
800+
});
801+
});
802+
803+
void it('provided function defaults to oldest runtime', () => {
804+
const lambda = defineFunction((scope) => {
805+
return new NodejsFunction(scope, 'nodejs-provided', {
806+
entry:
807+
'./packages/backend-function/src/test-assets/default-lambda/handler.ts',
808+
});
809+
}).getInstance(getInstanceProps);
810+
const template = Template.fromStack(lambda.stack);
811+
812+
template.hasResourceProperties('AWS::Lambda::Function', {
813+
Runtime: Runtime.NODEJS_16_X.name,
814+
});
815+
});
816+
});
785817
});

packages/backend-function/src/factory.ts

Lines changed: 45 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import {
2-
FunctionOutput,
3-
functionOutputKey,
4-
} from '@aws-amplify/backend-output-schemas';
5-
import { AttributionMetadataStorage } from '@aws-amplify/backend-output-storage';
1+
import { FunctionOutput } from '@aws-amplify/backend-output-schemas';
62
import {
73
AmplifyUserError,
84
CallerDirectoryExtractor,
@@ -20,19 +16,19 @@ import {
2016
GenerateContainerEntryProps,
2117
LogLevel,
2218
LogRetention,
19+
ResourceAccessAcceptor,
2320
ResourceAccessAcceptorFactory,
2421
ResourceNameValidator,
2522
ResourceProvider,
26-
SsmEnvironmentEntry,
2723
StackProvider,
2824
} from '@aws-amplify/plugin-types';
2925
import { Duration, Size, Stack, Tags } from 'aws-cdk-lib';
3026
import { Rule } from 'aws-cdk-lib/aws-events';
3127
import * as targets from 'aws-cdk-lib/aws-events-targets';
32-
import { Policy } from 'aws-cdk-lib/aws-iam';
3328
import {
3429
Architecture,
3530
CfnFunction,
31+
IFunction,
3632
ILayerVersion,
3733
LayerVersion,
3834
Runtime,
@@ -41,16 +37,19 @@ import { NodejsFunction, OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs';
4137
import { Construct } from 'constructs';
4238
import { readFileSync } from 'fs';
4339
import { createRequire } from 'module';
44-
import { fileURLToPath } from 'node:url';
4540
import { EOL } from 'os';
4641
import * as path from 'path';
4742
import { FunctionEnvironmentTranslator } from './function_env_translator.js';
4843
import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.js';
4944
import { FunctionLayerArnParser } from './layer_parser.js';
5045
import { convertLoggingOptionsToCDK } from './logging_options_parser.js';
5146
import { convertFunctionSchedulesToRuleSchedules } from './schedule_parser.js';
52-
53-
const functionStackType = 'function-Lambda';
47+
import {
48+
ProvidedFunctionFactory,
49+
ProvidedFunctionProps,
50+
} from './provided_function_factory.js';
51+
import { AmplifyFunctionBase } from './function_construct_base.js';
52+
import { FunctionResourceAccessAcceptor } from './resource_access_acceptor.js';
5453

5554
export type AddEnvironmentFactory = {
5655
addEnvironment: (key: string, value: string | BackendSecret) => void;
@@ -74,17 +73,38 @@ export type FunctionLogLevel = Extract<
7473
>;
7574
export type FunctionLogRetention = LogRetention;
7675

77-
/**
78-
* Entry point for defining a function in the Amplify ecosystem
79-
*/
80-
export const defineFunction = (
81-
props: FunctionProps = {}
76+
export function defineFunction(
77+
props?: FunctionProps
8278
): ConstructFactory<
8379
ResourceProvider<FunctionResources> &
8480
ResourceAccessAcceptorFactory &
8581
AddEnvironmentFactory &
8682
StackProvider
87-
> => new FunctionFactory(props, new Error().stack);
83+
>;
84+
export function defineFunction(
85+
provider: (scope: Construct) => IFunction,
86+
providerProps?: ProvidedFunctionProps
87+
): ConstructFactory<
88+
ResourceProvider<FunctionResources> &
89+
ResourceAccessAcceptorFactory &
90+
StackProvider
91+
>;
92+
/**
93+
* Entry point for defining a function in the Amplify ecosystem
94+
*/
95+
// This is the "implementation overload", it's not visible in public api.
96+
// We have to use function notation instead of arrow notation.
97+
// Arrow notation does not support overloads.
98+
// eslint-disable-next-line no-restricted-syntax
99+
export function defineFunction(
100+
propsOrProvider: FunctionProps | ((scope: Construct) => IFunction) = {},
101+
providerProps?: ProvidedFunctionProps
102+
): unknown {
103+
if (propsOrProvider && typeof propsOrProvider === 'function') {
104+
return new ProvidedFunctionFactory(propsOrProvider, providerProps);
105+
}
106+
return new FunctionFactory(propsOrProvider, new Error().stack);
107+
}
88108

89109
export type FunctionProps = {
90110
/**
@@ -507,14 +527,10 @@ class FunctionGenerator implements ConstructContainerEntryGenerator {
507527
}
508528

509529
class AmplifyFunction
510-
extends Construct
511-
implements
512-
ResourceProvider<FunctionResources>,
513-
ResourceAccessAcceptorFactory,
514-
AddEnvironmentFactory
530+
extends AmplifyFunctionBase
531+
implements AddEnvironmentFactory
515532
{
516533
readonly resources: FunctionResources;
517-
readonly stack: Stack;
518534
private readonly functionEnvironmentTranslator: FunctionEnvironmentTranslator;
519535
constructor(
520536
scope: Construct,
@@ -523,9 +539,7 @@ class AmplifyFunction
523539
backendSecretResolver: BackendSecretResolver,
524540
outputStorageStrategy: BackendOutputStorageStrategy<FunctionOutput>
525541
) {
526-
super(scope, id);
527-
528-
this.stack = Stack.of(scope);
542+
super(scope, id, outputStorageStrategy);
529543

530544
const runtime = nodeVersionMap[props.runtime];
531545

@@ -647,52 +661,18 @@ class AmplifyFunction
647661
},
648662
};
649663

650-
this.storeOutput(outputStorageStrategy);
651-
652-
new AttributionMetadataStorage().storeAttributionMetadata(
653-
Stack.of(this),
654-
functionStackType,
655-
fileURLToPath(new URL('../package.json', import.meta.url))
656-
);
664+
this.storeOutput();
657665
}
658666

659667
addEnvironment = (key: string, value: string | BackendSecret) => {
660668
this.functionEnvironmentTranslator.addEnvironmentEntry(key, value);
661669
};
662670

663-
getResourceAccessAcceptor = () => ({
664-
identifier: `${this.node.id}LambdaResourceAccessAcceptor`,
665-
acceptResourceAccess: (
666-
policy: Policy,
667-
ssmEnvironmentEntries: SsmEnvironmentEntry[]
668-
) => {
669-
const role = this.resources.lambda.role;
670-
if (!role) {
671-
// This should never happen since we are using the Function L2 construct
672-
throw new Error(
673-
'No execution role found to attach lambda permissions to'
674-
);
675-
}
676-
policy.attachToRole(role);
677-
ssmEnvironmentEntries.forEach(({ name, path }) => {
678-
this.functionEnvironmentTranslator.addSsmEnvironmentEntry(name, path);
679-
});
680-
},
681-
});
682-
683-
/**
684-
* Store storage outputs using provided strategy
685-
*/
686-
private storeOutput = (
687-
outputStorageStrategy: BackendOutputStorageStrategy<FunctionOutput>
688-
): void => {
689-
outputStorageStrategy.appendToBackendOutputList(functionOutputKey, {
690-
version: '1',
691-
payload: {
692-
definedFunctions: this.resources.lambda.functionName,
693-
},
694-
});
695-
};
671+
getResourceAccessAcceptor = (): ResourceAccessAcceptor =>
672+
new FunctionResourceAccessAcceptor(
673+
this,
674+
this.functionEnvironmentTranslator
675+
);
696676
}
697677

698678
const isWholeNumberBetweenInclusive = (
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Construct } from 'constructs';
2+
import {
3+
BackendOutputStorageStrategy,
4+
FunctionResources,
5+
ResourceAccessAcceptor,
6+
ResourceAccessAcceptorFactory,
7+
ResourceProvider,
8+
} from '@aws-amplify/plugin-types';
9+
import { Stack } from 'aws-cdk-lib';
10+
import {
11+
FunctionOutput,
12+
functionOutputKey,
13+
} from '@aws-amplify/backend-output-schemas';
14+
import { AttributionMetadataStorage } from '@aws-amplify/backend-output-storage';
15+
import { fileURLToPath } from 'node:url';
16+
17+
const functionStackType = 'function-Lambda';
18+
19+
/**
20+
* A base class for function constructs.
21+
*/
22+
export abstract class AmplifyFunctionBase
23+
extends Construct
24+
implements ResourceProvider<FunctionResources>, ResourceAccessAcceptorFactory
25+
{
26+
readonly stack: Stack;
27+
abstract resources: FunctionResources;
28+
29+
abstract getResourceAccessAcceptor: () => ResourceAccessAcceptor;
30+
31+
/**
32+
* Creates base function construct.
33+
*/
34+
protected constructor(
35+
scope: Construct,
36+
id: string,
37+
private readonly outputStorageStrategy: BackendOutputStorageStrategy<FunctionOutput>
38+
) {
39+
super(scope, id);
40+
41+
this.stack = Stack.of(scope);
42+
43+
new AttributionMetadataStorage().storeAttributionMetadata(
44+
Stack.of(this),
45+
functionStackType,
46+
fileURLToPath(new URL('../package.json', import.meta.url))
47+
);
48+
}
49+
50+
protected storeOutput = (): void => {
51+
this.outputStorageStrategy.appendToBackendOutputList(functionOutputKey, {
52+
version: '1',
53+
payload: {
54+
definedFunctions: this.resources.lambda.functionName,
55+
},
56+
});
57+
};
58+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
export * from './factory.js';
2+
import { ProvidedFunctionProps } from './provided_function_factory.js';
3+
export { ProvidedFunctionProps };

0 commit comments

Comments
 (0)