Skip to content

Commit

Permalink
Feature: custom runtimes to define function (#1602)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Jan 7, 2025
1 parent a04dbb8 commit 3f521c3
Show file tree
Hide file tree
Showing 11 changed files with 365 additions and 67 deletions.
6 changes: 6 additions & 0 deletions .changeset/long-berries-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@aws-amplify/backend-function': minor
'@aws-amplify/backend': minor
---

add custom provided function support to define function
14 changes: 12 additions & 2 deletions packages/backend-function/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

import { AmplifyResourceGroupName } from '@aws-amplify/plugin-types';
import { BackendSecret } from '@aws-amplify/plugin-types';
import { Construct } from 'constructs';
import { ConstructFactory } from '@aws-amplify/plugin-types';
import { FunctionResources } from '@aws-amplify/plugin-types';
import { IFunction } from 'aws-cdk-lib/aws-lambda';
import { LogLevel } from '@aws-amplify/plugin-types';
import { LogRetention } from '@aws-amplify/plugin-types';
import { ResourceAccessAcceptorFactory } from '@aws-amplify/plugin-types';
Expand Down Expand Up @@ -61,8 +63,11 @@ type DataClientError = {
// @public (undocumented)
type DataClientReturn<T> = T extends DataClientEnv ? DataClientConfig : DataClientError;

// @public
export const defineFunction: (props?: FunctionProps) => ConstructFactory<ResourceProvider<FunctionResources> & ResourceAccessAcceptorFactory & AddEnvironmentFactory & StackProvider>;
// @public (undocumented)
export function defineFunction(props?: FunctionProps): ConstructFactory<ResourceProvider<FunctionResources> & ResourceAccessAcceptorFactory & AddEnvironmentFactory & StackProvider>;

// @public (undocumented)
export function defineFunction(provider: (scope: Construct) => IFunction, providerProps?: ProvidedFunctionProps): ConstructFactory<ResourceProvider<FunctionResources> & ResourceAccessAcceptorFactory & StackProvider>;

// @public (undocumented)
export type FunctionArchitecture = 'x86_64' | 'arm64';
Expand Down Expand Up @@ -135,6 +140,11 @@ type LibraryOptions = {
// @public (undocumented)
export type NodeVersion = 16 | 18 | 20 | 22;

// @public (undocumented)
export type ProvidedFunctionProps = {
resourceGroupName?: AmplifyResourceGroupName;
};

// @public (undocumented)
type ResourceConfig = {
API: {
Expand Down
32 changes: 32 additions & 0 deletions packages/backend-function/src/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam';
import fsp from 'fs/promises';
import path from 'node:path';
import { AmplifyUserError } from '@aws-amplify/platform-core';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';

const createStackAndSetContext = (): Stack => {
const app = new App();
Expand Down Expand Up @@ -782,4 +783,35 @@ void describe('AmplifyFunctionFactory', () => {
);
});
});

void describe('provided function runtime property', () => {
void it('sets valid runtime', () => {
const lambda = defineFunction((scope) => {
return new NodejsFunction(scope, 'nodejs-provided', {
entry:
'./packages/backend-function/src/test-assets/default-lambda/handler.ts',
runtime: Runtime.NODEJS_22_X,
});
}).getInstance(getInstanceProps);
const template = Template.fromStack(lambda.stack);

template.hasResourceProperties('AWS::Lambda::Function', {
Runtime: Runtime.NODEJS_22_X.name,
});
});

void it('provided function defaults to oldest runtime', () => {
const lambda = defineFunction((scope) => {
return new NodejsFunction(scope, 'nodejs-provided', {
entry:
'./packages/backend-function/src/test-assets/default-lambda/handler.ts',
});
}).getInstance(getInstanceProps);
const template = Template.fromStack(lambda.stack);

template.hasResourceProperties('AWS::Lambda::Function', {
Runtime: Runtime.NODEJS_16_X.name,
});
});
});
});
110 changes: 45 additions & 65 deletions packages/backend-function/src/factory.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
FunctionOutput,
functionOutputKey,
} from '@aws-amplify/backend-output-schemas';
import { AttributionMetadataStorage } from '@aws-amplify/backend-output-storage';
import { FunctionOutput } from '@aws-amplify/backend-output-schemas';
import {
AmplifyUserError,
CallerDirectoryExtractor,
Expand All @@ -20,19 +16,19 @@ import {
GenerateContainerEntryProps,
LogLevel,
LogRetention,
ResourceAccessAcceptor,
ResourceAccessAcceptorFactory,
ResourceNameValidator,
ResourceProvider,
SsmEnvironmentEntry,
StackProvider,
} from '@aws-amplify/plugin-types';
import { Duration, Size, 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 {
Architecture,
CfnFunction,
IFunction,
ILayerVersion,
LayerVersion,
Runtime,
Expand All @@ -41,16 +37,19 @@ 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 * as path from 'path';
import { FunctionEnvironmentTranslator } from './function_env_translator.js';
import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.js';
import { FunctionLayerArnParser } from './layer_parser.js';
import { convertLoggingOptionsToCDK } from './logging_options_parser.js';
import { convertFunctionSchedulesToRuleSchedules } from './schedule_parser.js';

const functionStackType = 'function-Lambda';
import {
ProvidedFunctionFactory,
ProvidedFunctionProps,
} from './provided_function_factory.js';
import { AmplifyFunctionBase } from './function_construct_base.js';
import { FunctionResourceAccessAcceptor } from './resource_access_acceptor.js';

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

/**
* Entry point for defining a function in the Amplify ecosystem
*/
export const defineFunction = (
props: FunctionProps = {}
export function defineFunction(
props?: FunctionProps
): ConstructFactory<
ResourceProvider<FunctionResources> &
ResourceAccessAcceptorFactory &
AddEnvironmentFactory &
StackProvider
> => new FunctionFactory(props, new Error().stack);
>;
export function defineFunction(
provider: (scope: Construct) => IFunction,
providerProps?: ProvidedFunctionProps
): ConstructFactory<
ResourceProvider<FunctionResources> &
ResourceAccessAcceptorFactory &
StackProvider
>;
/**
* Entry point for defining a function in the Amplify ecosystem
*/
// This is the "implementation overload", it's not visible in public api.
// We have to use function notation instead of arrow notation.
// Arrow notation does not support overloads.
// eslint-disable-next-line no-restricted-syntax
export function defineFunction(
propsOrProvider: FunctionProps | ((scope: Construct) => IFunction) = {},
providerProps?: ProvidedFunctionProps
): unknown {
if (propsOrProvider && typeof propsOrProvider === 'function') {
return new ProvidedFunctionFactory(propsOrProvider, providerProps);
}
return new FunctionFactory(propsOrProvider, new Error().stack);
}

export type FunctionProps = {
/**
Expand Down Expand Up @@ -507,14 +527,10 @@ class FunctionGenerator implements ConstructContainerEntryGenerator {
}

class AmplifyFunction
extends Construct
implements
ResourceProvider<FunctionResources>,
ResourceAccessAcceptorFactory,
AddEnvironmentFactory
extends AmplifyFunctionBase
implements AddEnvironmentFactory
{
readonly resources: FunctionResources;
readonly stack: Stack;
private readonly functionEnvironmentTranslator: FunctionEnvironmentTranslator;
constructor(
scope: Construct,
Expand All @@ -523,9 +539,7 @@ class AmplifyFunction
backendSecretResolver: BackendSecretResolver,
outputStorageStrategy: BackendOutputStorageStrategy<FunctionOutput>
) {
super(scope, id);

this.stack = Stack.of(scope);
super(scope, id, outputStorageStrategy);

const runtime = nodeVersionMap[props.runtime];

Expand Down Expand Up @@ -647,52 +661,18 @@ class AmplifyFunction
},
};

this.storeOutput(outputStorageStrategy);

new AttributionMetadataStorage().storeAttributionMetadata(
Stack.of(this),
functionStackType,
fileURLToPath(new URL('../package.json', import.meta.url))
);
this.storeOutput();
}

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

getResourceAccessAcceptor = () => ({
identifier: `${this.node.id}LambdaResourceAccessAcceptor`,
acceptResourceAccess: (
policy: Policy,
ssmEnvironmentEntries: SsmEnvironmentEntry[]
) => {
const role = this.resources.lambda.role;
if (!role) {
// This should never happen since we are using the Function L2 construct
throw new Error(
'No execution role found to attach lambda permissions to'
);
}
policy.attachToRole(role);
ssmEnvironmentEntries.forEach(({ name, path }) => {
this.functionEnvironmentTranslator.addSsmEnvironmentEntry(name, path);
});
},
});

/**
* Store storage outputs using provided strategy
*/
private storeOutput = (
outputStorageStrategy: BackendOutputStorageStrategy<FunctionOutput>
): void => {
outputStorageStrategy.appendToBackendOutputList(functionOutputKey, {
version: '1',
payload: {
definedFunctions: this.resources.lambda.functionName,
},
});
};
getResourceAccessAcceptor = (): ResourceAccessAcceptor =>
new FunctionResourceAccessAcceptor(
this,
this.functionEnvironmentTranslator
);
}

const isWholeNumberBetweenInclusive = (
Expand Down
58 changes: 58 additions & 0 deletions packages/backend-function/src/function_construct_base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Construct } from 'constructs';
import {
BackendOutputStorageStrategy,
FunctionResources,
ResourceAccessAcceptor,
ResourceAccessAcceptorFactory,
ResourceProvider,
} from '@aws-amplify/plugin-types';
import { Stack } from 'aws-cdk-lib';
import {
FunctionOutput,
functionOutputKey,
} from '@aws-amplify/backend-output-schemas';
import { AttributionMetadataStorage } from '@aws-amplify/backend-output-storage';
import { fileURLToPath } from 'node:url';

const functionStackType = 'function-Lambda';

/**
* A base class for function constructs.
*/
export abstract class AmplifyFunctionBase
extends Construct
implements ResourceProvider<FunctionResources>, ResourceAccessAcceptorFactory
{
readonly stack: Stack;
abstract resources: FunctionResources;

abstract getResourceAccessAcceptor: () => ResourceAccessAcceptor;

/**
* Creates base function construct.
*/
protected constructor(
scope: Construct,
id: string,
private readonly outputStorageStrategy: BackendOutputStorageStrategy<FunctionOutput>
) {
super(scope, id);

this.stack = Stack.of(scope);

new AttributionMetadataStorage().storeAttributionMetadata(
Stack.of(this),
functionStackType,
fileURLToPath(new URL('../package.json', import.meta.url))
);
}

protected storeOutput = (): void => {
this.outputStorageStrategy.appendToBackendOutputList(functionOutputKey, {
version: '1',
payload: {
definedFunctions: this.resources.lambda.functionName,
},
});
};
}
2 changes: 2 additions & 0 deletions packages/backend-function/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './factory.js';
import { ProvidedFunctionProps } from './provided_function_factory.js';
export { ProvidedFunctionProps };
Loading

0 comments on commit 3f521c3

Please sign in to comment.