Skip to content

Commit

Permalink
Add rancid code so CloudFormation will deploy the API.
Browse files Browse the repository at this point in the history
  • Loading branch information
AshtonStephens committed May 2, 2024
1 parent 39264c6 commit 398375f
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 17 deletions.
2 changes: 1 addition & 1 deletion emily/api-definition/models/emily.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use aws.protocols#restJson1
// Specifies the integration's HTTP method type (for example, POST). For
// Lambda function invocations, the value must be POST.
httpMethod: "POST",
uri: "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OperationLambda.Arn}/invocations",
uri: "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OperationLambda}/invocations",
)
@title("Emily")
service Emily {
Expand Down
2 changes: 1 addition & 1 deletion emily/api-definition/smithy-build.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"openapi": {
"service": "stacks.sbtc#Emily",
"protocol": "aws.protocols#restJson1",
"version": "3.1.0",
"version": "3.0.2",
"apiGatewayDefaults": "2023-08-11"
}
}
Expand Down
56 changes: 51 additions & 5 deletions emily/cdk/lib/emily-stack-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ApiDefinition } from "aws-cdk-lib/aws-apigateway";
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as fs from 'fs';
import { resolve } from "path";
import { EmilyStackProps } from "./emily-stack-props";
Expand Down Expand Up @@ -93,14 +94,59 @@ export class EmilyStackUtils {
}

/**
* @description Generate an api definition asset from a local OpenAPI definition, replacing the lambda
* integration tags with the appropriate resource values.
* @description Generate an api definition asset from a local OpenAPI definition and modifies the
* template such that CloudFormation can replace the lambda identifiers with the correct lambda arn.
* @param {fs.PathOrFileDescriptor} restApiPathOrFileDescriptor the location of the definition asset
* @param {string} lambdaFunctionId lambdaFunction Id.
* @param {CloudFormationStackProps} props properties of the cloud formation stack.
* @returns {ApiDefinition} The name of the resource.
*/
public static restApiDefinition(
public static restApiDefinitionWithLambdaIntegration(
restApiPathOrFileDescriptor: fs.PathOrFileDescriptor,
apiLambdas: [lambdaIdentifier: string, lambdaFunction: lambda.Function][],
): ApiDefinition {
// Replace our `${API_LAMBDA_URI}` token with the calculated lambda invokation URI.
return ApiDefinition.fromInline(JSON.parse(fs.readFileSync(restApiPathOrFileDescriptor, 'utf-8')))

// This whole section is unfortunate but there's not a standard solution. The autogenerated OpenAPI
// template will always setup the `uri` to have injected substitution with `Fn::Sub`, and the only
// way to gather the ARN of the lambda as a string at build time is build the whole arn yourself which
// is worse than this solution; if the arn format changes the api will break without an obvious error.
// When we get the ARN from lambda.Function object it generates a template function that will gather
// the ARN and pretend it's a string, so you cannot substitute anything with it directly.
//
// In this loop we go through the OpenAPI json and inject it with a parameter such that the
// `Fn::Sub` can replace with our desired lambda's arn.
//
// https://repost.aws/knowledge-center/cloudformation-fn-sub-function
//
// If you look at the template that spawns from this you'll see that the `Fn::Sub` actually defines
// the key to be ANOTHER `Fn::XXX` template function that resolves to be the lambda's Arn.
//
// It would be nice if there were a solution that didn't involve doing this ourselves.
// At the moment, there isn't.
let apiJsonDefiniton = JSON.parse(fs.readFileSync(restApiPathOrFileDescriptor, 'utf-8'));
let paths = apiJsonDefiniton["paths"];
Object.keys(paths).forEach(path => {
let verbs = paths[path];
Object.keys(verbs).forEach(verb => {
if (Object.keys(verbs[verb]).includes("x-amazon-apigateway-integration")) {
let awsIntegration = verbs[verb]["x-amazon-apigateway-integration"];
let originalSubString: string = awsIntegration["uri"]["Fn::Sub"];
apiLambdas.forEach(([lambdaIdentifier, lambdaFunction]) => {
if (originalSubString.includes(`\${${lambdaIdentifier}}`)) {
// If the identifier is present in the uri string then generate the template function to
// replace the identifier with the lambda arn.
//
// This will incorrectly handle the already invalid case where two function ARNS are in the uri.
// Handling multiple replacements is left as an exercize for the reader.
apiJsonDefiniton["paths"][path][verb]["x-amazon-apigateway-integration"]["uri"]["Fn::Sub"] =
[ originalSubString, { [lambdaIdentifier] : lambdaFunction.functionArn } ]
}
})
}
})
})

// Return the modified template as an ApiDefinition.
return ApiDefinition.fromInline(apiJsonDefiniton)
}
}
24 changes: 14 additions & 10 deletions emily/cdk/lib/emily-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,7 @@ export class EmilyStack extends cdk.Stack {
props: EmilyStackProps
): lambda.Function {

// This must match the Lambda name from the @aws.apigateway#integration trait in the
// smithy operations and resources that should be handled by this Lambda.
const operationLambdaId: string = "OperationLambda";

const operationLambda: lambda.Function = new lambda.Function(this, operationLambdaId, {
functionName: EmilyStackUtils.getResourceName(operationLambdaId, props),
architecture: lambda.Architecture.ARM_64, // <- Will need to change when run locally for x86
Expand All @@ -187,7 +184,7 @@ export class EmilyStack extends cdk.Stack {
}
});

// Give the server lambda full access to the DynamoDB table.
// Give the server lambda full access to the DynamoDB tables.
depositTable.grantReadWriteData(operationLambda);
withdrawalTable.grantReadWriteData(operationLambda);
chainstateTable.grantReadWriteData(operationLambda);
Expand All @@ -198,27 +195,34 @@ export class EmilyStack extends cdk.Stack {

/**
* Creates or updates the API Gateway to connect with the Lambda function.
* @param {lambda.Function} serverLambda The Lambda function to connect to the API.
* @param {lambda.Function} operationLambda The Lambda function to connect to the API.
* @param {EmilyStackProps} props The stack properties.
* @returns {apig.SpecRestApi} The created or updated API Gateway.
* @post An API Gateway with execute permissions linked to the Lambda function is returned.
*/
createOrUpdateApi(
serverLambda: lambda.Function,
operationLambda: lambda.Function,
props: EmilyStackProps
): apig.SpecRestApi {

const restApiId: string = "EmilyAPI";
const restApi: apig.SpecRestApi = new apig.SpecRestApi(this, restApiId, {
restApiName: EmilyStackUtils.getResourceName(restApiId, props),
apiDefinition: EmilyStackUtils.restApiDefinition(EmilyStackUtils.getPathFromProjectRoot(
".generated-sources/openapi/openapi/Emily.openapi.json"
)),
apiDefinition: EmilyStackUtils.restApiDefinitionWithLambdaIntegration(
EmilyStackUtils.getPathFromProjectRoot(
".generated-sources/openapi/openapi/Emily.openapi.json"
),
[
// This must match the Lambda name from the @aws.apigateway#integration trait in the
// smithy operations and resources that should be handled by this Lambda.
["OperationLambda", operationLambda]
],
),
deployOptions: { stageName: props.stageName },
});

// Give the the rest api execute ARN permission to invoke the lambda.
serverLambda.addPermission("ApiInvokeLambdaPermission", {
operationLambda.addPermission("ApiInvokeLambdaPermission", {
principal: new iam.ServicePrincipal("apigateway.amazonaws.com"),
action: "lambda:InvokeFunction",
sourceArn: restApi.arnForExecuteApi(),
Expand Down

0 comments on commit 398375f

Please sign in to comment.