Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(aws-openapigatway-lamba): accept lambda function aliases #1260

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deployment/v2/align-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const nullVersionMarker = process.argv[2];
const targetSolutionsConstructsVersion = process.argv[3];

// these versions need to be sourced from a config file
const awsCdkLibVersion = '2.177.0';
const awsCdkLibVersion = '2.179.0';

for (const file of process.argv.splice(4)) {
const pkg = JSON.parse(fs.readFileSync(file).toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ When the construct is created or updated, it will overwrite the `MessagesHandler
For more information on specifying an API with OpenAPI, please see the [OpenAPI Specification](https://spec.openapis.org/oas/latest.html)

## ApiIntegration Details
This construct defines a custom type, `ApiIntegration`, that is specified as a required prop. The type has a required property, `id`, and two optional properties `existingLambdaObj` and `lambdaFunctionProps`. The `id` property is used to map the corresponding lambda function being defined with the placeholder string in the OpenAPI template file, and is not a CDK construct ID. Exactly one of `existingLambdaObj` or `lambdaFunctionProps` must be specified or the construct will throw an error.
This construct defines a custom type, `ApiIntegration`, that is specified as a required prop. The type has a required property, `id`, and three optional properties `existingLambdaObj`, `existingFunctionAlias` and `lambdaFunctionProps`. The `id` property is used to map the corresponding lambda function being defined with the placeholder string in the OpenAPI template file, and is not a CDK construct ID. Exactly one of `existingLambdaObj` or `lambdaFunctionProps` must be specified or the construct will throw an error.

## Default settings

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Construct } from 'constructs';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as defaults from '@aws-solutions-constructs/core';
import { RestApiBaseProps } from 'aws-cdk-lib/aws-apigateway';
Expand Down Expand Up @@ -116,24 +117,41 @@ export class OpenApiGatewayToLambda extends Construct {
// store a counter to be able to uniquely name lambda functions to avoid naming collisions
let lambdaCounter = 0;

this.apiLambdaFunctions = props.apiIntegrations.map(apiIntegration => {
if (apiIntegration.existingLambdaObj && apiIntegration.lambdaFunctionProps) {
throw new Error(`Error - Cannot provide both lambdaFunctionProps and existingLambdaObj in an ApiIntegrationfor the api integration with id: ${apiIntegration.id}`);
}
if (apiIntegration.existingLambdaObj || apiIntegration.lambdaFunctionProps) {
// TODO: Should this functionality be moved to openapi-helper.ts?
this.apiLambdaFunctions = props.apiIntegrations.map(rawApiIntegration => {
// let updatedIntegration: ApiIntegration;
// if (rawApiIntegration.alternateType) {
// if ((rawApiIntegration.alternateType as lambda.Alias).aliasName) {
// updatedIntegration = {
// ...rawApiIntegration,
// existingFunctionAlias: rawApiIntegration.alternateType as lambda.Alias
// };
// } else {
// updatedIntegration = {
// ...rawApiIntegration,
// existingLambdaObj: rawApiIntegration.alternateType as lambda.Function
// };
// }
// } else {
// updatedIntegration = rawApiIntegration;
// }
if (rawApiIntegration.existingLambdaObj && this.isResourceAnAlias(rawApiIntegration.existingLambdaObj)) {
return {
id: apiIntegration.id,
lambdaFunction: defaults.buildLambdaFunction(this, {
existingLambdaObj: apiIntegration.existingLambdaObj,
lambdaFunctionProps: apiIntegration.lambdaFunctionProps
}, `${apiIntegration.id}ApiFunction${lambdaCounter++}`)
id: rawApiIntegration.id,
functionAlias: rawApiIntegration.existingLambdaObj as lambda.Alias
};
} else {
throw new Error(`One of existingLambdaObj or lambdaFunctionProps must be specified for the api integration with id: ${apiIntegration.id}`);
return {
id: rawApiIntegration.id,
lambdaFunction: defaults.buildLambdaFunction(this, {
existingLambdaObj: rawApiIntegration.existingLambdaObj as lambda.Function,
lambdaFunctionProps: rawApiIntegration.lambdaFunctionProps
}, `${rawApiIntegration.id}ApiFunction${lambdaCounter++}`),
};
}
});

const definition = ObtainApiDefinition(this, {
const definition = ObtainApiDefinition(this, {
tokenToFunctionMap: this.apiLambdaFunctions,
apiDefinitionBucket: props.apiDefinitionBucket,
apiDefinitionKey: props.apiDefinitionKey,
Expand All @@ -155,13 +173,28 @@ export class OpenApiGatewayToLambda extends Construct {
// Redeploy the API any time a decoupled (non-inline) API definition changes (from asset or s3 object)
this.apiGateway.latestDeployment?.addToLogicalId(props.apiDefinitionKey ?? props.apiDefinitionAsset?.s3ObjectKey);
this.apiLambdaFunctions.forEach(apiLambdaFunction => {
// We confirm upstream that one of these two values exists, so we can cast away Typescripts doubt
const targetInterface: lambda.IFunction = (apiLambdaFunction.lambdaFunction ?? apiLambdaFunction.functionAlias) as lambda.IFunction;

// Redeploy the API any time one of the lambda functions changes
this.apiGateway.latestDeployment?.addToLogicalId(apiLambdaFunction.lambdaFunction.functionArn);
// Grant APIGW invocation rights for each lambda function
apiLambdaFunction.lambdaFunction.addPermission(`${id}PermitAPIGInvocation`, {
principal: new iam.ServicePrincipal('apigateway.amazonaws.com'),
sourceArn: this.apiGateway.arnForExecuteApi('*')
});
this.apiGateway.latestDeployment?.addToLogicalId(targetInterface.functionArn);
if (apiLambdaFunction.functionAlias) {
// Grant APIGW invocation rights for each lambda function
apiLambdaFunction.functionAlias.addPermission(`${id}PermitAPIGInvocation`, {
principal: new iam.ServicePrincipal('apigateway.amazonaws.com'),
sourceArn: this.apiGateway.arnForExecuteApi('*')
});
} else {
// Grant APIGW invocation rights for each lambda function
targetInterface.addPermission(`${id}PermitAPIGInvocation`, {
principal: new iam.ServicePrincipal('apigateway.amazonaws.com'),
sourceArn: this.apiGateway.arnForExecuteApi('*')
});
}
});
}

private isResourceAnAlias(lambdaResource: lambda.Function | lambda.Alias): boolean {
return (lambdaResource as lambda.Alias).aliasName !== undefined;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export interface ApiIntegration {
*
* One and only one of existingLambdaObj or lambdaFunctionProps must be specified, any other combination will cause an error.
*/
readonly existingLambdaObj?: lambda.Function;
readonly existingLambdaObj?: lambda.Function | lambda.Alias;
/**
* Properties for the Lambda function to create and associate with the API method in the OpenAPI file matched by id.
*
Expand All @@ -65,9 +65,11 @@ export interface ApiLambdaFunction {
*/
readonly id: string;
/**
* The instantiated lambda.Function.
* The function the API method will integrate with -
* Must be defined in lambdaFunction or functionAlias (but not both)
*/
readonly lambdaFunction: lambda.Function;
readonly lambdaFunction?: lambda.Function;
readonly functionAlias?: lambda.Alias;
}

export interface OpenApiProps {
Expand Down Expand Up @@ -104,6 +106,20 @@ export function CheckOpenApiProps(props: OpenApiProps) {
if (props.apiIntegrations === undefined || props.apiIntegrations.length < 1) {
errorMessages += 'At least one ApiIntegration must be specified in the apiIntegrations property\n';
errorFound = true;
} else {
props.apiIntegrations.forEach((apiIntegration: ApiIntegration) => {
if (!apiIntegration.id) {
errorMessages += 'Each ApiIntegration must have a non-empty id property\n';
errorFound = true;
}
let functionDefCount = 0;
if (apiIntegration.lambdaFunctionProps) { functionDefCount++; }
if (apiIntegration.existingLambdaObj) { functionDefCount++; }
if (functionDefCount !== 1) {
errorMessages += `ApiIntegration id:${apiIntegration.id} must have exactly one of lambdaFunctionProps or existingLambdaObj\n`;
errorFound = true;
}
});
}

if (errorFound) {
Expand Down Expand Up @@ -136,7 +152,9 @@ export function ObtainApiDefinition(scope: Construct, props: ObtainApiDefinition
const uriPlaceholderString = apiLambdaFunction.id;
// the endpoint URI of the backing lambda function, as defined in the API Gateway extensions for OpenAPI here:
// https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-integration.html
const uriResolvedValue = `arn:${Aws.PARTITION}:apigateway:${Aws.REGION}:lambda:path/2015-03-31/functions/${apiLambdaFunction.lambdaFunction.functionArn}/invocations`;
// We know that either functionAlias or lambdaFunction must be defined, so we can use ! to satisfy Typescript
const targetArn = apiLambdaFunction.functionAlias ? apiLambdaFunction.functionAlias.functionArn : apiLambdaFunction.lambdaFunction!.functionArn;
const uriResolvedValue = `arn:${Aws.PARTITION}:apigateway:${Aws.REGION}:lambda:path/2015-03-31/functions/${targetArn}/invocations`;

return {
id: uriPlaceholderString,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const handler = async (event) => {
switch (event.httpMethod) {
case 'POST':
return {
statusCode: 200,
body: JSON.stringify({"message": "NEW - successfully handled POST from messages lambda"})
};
case 'GET':
return {
statusCode: 200,
body: JSON.stringify({"message": "NEW - successfully handled GET from messages lambda"})
};
default:
throw new Error(`cannot handle httpMethod: ${event.httpMethod}`);
}
};

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading