diff --git a/package.json b/package.json index a317e626c3..460cc21b4f 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@aws-cdk/assert": "~1.74.0", "@aws-cdk/aws-autoscaling": "~1.74.0", "@aws-cdk/aws-ec2": "~1.74.0", + "@aws-cdk/aws-apigateway": "~1.74.0", "@aws-cdk/aws-elasticloadbalancingv2": "~1.74.0", "@aws-cdk/aws-iam": "~1.74.0", "@aws-cdk/aws-lambda": "~1.74.0", diff --git a/src/constructs/lambda/__snapshots__/lambda.test.ts.snap b/src/constructs/lambda/__snapshots__/lambda.test.ts.snap index d4b3acbc76..1371a3edb0 100644 --- a/src/constructs/lambda/__snapshots__/lambda.test.ts.snap +++ b/src/constructs/lambda/__snapshots__/lambda.test.ts.snap @@ -2,9 +2,26 @@ exports[`GuLambdaFunction should create a lambda function with a schedule to run every week 1`] = ` Object { + "Parameters": Object { + "Stack": Object { + "Default": "deploy", + "Description": "Name of this stack", + "Type": "String", + }, + "Stage": Object { + "AllowedValues": Array [ + "CODE", + "PROD", + ], + "Default": "CODE", + "Description": "Stage name", + "Type": "String", + }, + }, "Resources": Object { "lambda8B5974B5": Object { "DependsOn": Array [ + "lambdaServiceRoleDefaultPolicyBF6FA5E7", "lambdaServiceRole494E4CA6", ], "Properties": Object { @@ -20,6 +37,20 @@ Object { ], }, "Runtime": "nodejs12.x", + "Tags": Array [ + Object { + "Key": "Stack", + "Value": Object { + "Ref": "Stack", + }, + }, + Object { + "Key": "Stage", + "Value": Object { + "Ref": "Stage", + }, + }, + ], }, "Type": "AWS::Lambda::Function", }, @@ -51,10 +82,93 @@ Object { ], }, ], + "Tags": Array [ + Object { + "Key": "Stack", + "Value": Object { + "Ref": "Stack", + }, + }, + Object { + "Key": "Stage", + "Value": Object { + "Ref": "Stage", + }, + }, + ], }, "Type": "AWS::IAM::Role", }, - "lambdalambda7daysAllowEventRulelambdalambda7daysA269ED0EFFB7184B": Object { + "lambdaServiceRoleDefaultPolicyBF6FA5E7": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::bucket1", + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::bucket1/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "lambdaServiceRoleDefaultPolicyBF6FA5E7", + "Roles": Array [ + Object { + "Ref": "lambdaServiceRole494E4CA6", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "lambdalambdarate7days078CFD8D7": Object { + "Properties": Object { + "Description": "run every week", + "ScheduleExpression": "rate(7 days)", + "State": "ENABLED", + "Targets": Array [ + Object { + "Arn": Object { + "Fn::GetAtt": Array [ + "lambda8B5974B5", + "Arn", + ], + }, + "Id": "Target0", + }, + ], + }, + "Type": "AWS::Events::Rule", + }, + "lambdalambdarate7days0AllowEventRulelambdalambdarate7days05DC0390ACB8353DF": Object { "Properties": Object { "Action": "lambda:InvokeFunction", "FunctionName": Object { @@ -66,31 +180,567 @@ Object { "Principal": "events.amazonaws.com", "SourceArn": Object { "Fn::GetAtt": Array [ - "lambdalambda7daysC799B8C2", + "lambdalambdarate7days078CFD8D7", "Arn", ], }, }, "Type": "AWS::Lambda::Permission", }, - "lambdalambda7daysC799B8C2": Object { + }, +} +`; + +exports[`GuLambdaFunction should create a lambda function with an api gateway 1`] = ` +Object { + "Outputs": Object { + "lambdaapiEndpoint3B6C471A": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "lambdaapiC1812993", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "lambdaapiDeploymentStageprod9598BC2F", + }, + "/", + ], + ], + }, + }, + }, + "Parameters": Object { + "Stack": Object { + "Default": "deploy", + "Description": "Name of this stack", + "Type": "String", + }, + "Stage": Object { + "AllowedValues": Array [ + "CODE", + "PROD", + ], + "Default": "CODE", + "Description": "Stage name", + "Type": "String", + }, + }, + "Resources": Object { + "lambda8B5974B5": Object { + "DependsOn": Array [ + "lambdaServiceRoleDefaultPolicyBF6FA5E7", + "lambdaServiceRole494E4CA6", + ], "Properties": Object { - "Description": "run every week", - "ScheduleExpression": "rate(7 days)", - "State": "ENABLED", - "Targets": Array [ + "Code": Object { + "S3Bucket": "bucket1", + "S3Key": "folder/to/key", + }, + "Handler": "handler.ts", + "Role": Object { + "Fn::GetAtt": Array [ + "lambdaServiceRole494E4CA6", + "Arn", + ], + }, + "Runtime": "nodejs12.x", + "Tags": Array [ Object { - "Arn": Object { - "Fn::GetAtt": Array [ - "lambda8B5974B5", - "Arn", + "Key": "Stack", + "Value": Object { + "Ref": "Stack", + }, + }, + Object { + "Key": "Stage", + "Value": Object { + "Ref": "Stage", + }, + }, + ], + }, + "Type": "AWS::Lambda::Function", + }, + "lambdaServiceRole494E4CA6": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", ], + ], + }, + ], + "Tags": Array [ + Object { + "Key": "Stack", + "Value": Object { + "Ref": "Stack", + }, + }, + Object { + "Key": "Stage", + "Value": Object { + "Ref": "Stage", }, - "Id": "Target0", }, ], }, - "Type": "AWS::Events::Rule", + "Type": "AWS::IAM::Role", + }, + "lambdaServiceRoleDefaultPolicyBF6FA5E7": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::bucket1", + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::bucket1/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "lambdaServiceRoleDefaultPolicyBF6FA5E7", + "Roles": Array [ + Object { + "Ref": "lambdaServiceRole494E4CA6", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "lambdaapiANY4EBCADD1": Object { + "Properties": Object { + "AuthorizationType": "NONE", + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "lambda8B5974B5", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Fn::GetAtt": Array [ + "lambdaapiC1812993", + "RootResourceId", + ], + }, + "RestApiId": Object { + "Ref": "lambdaapiC1812993", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "lambdaapiANYApiPermissionTestlambdaapiC0087213ANY5E7CDADF": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "lambda8B5974B5", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "lambdaapiC1812993", + }, + "/test-invoke-stage/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "lambdaapiANYApiPermissionlambdaapiC0087213ANY629951AC": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "lambda8B5974B5", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "lambdaapiC1812993", + }, + "/", + Object { + "Ref": "lambdaapiDeploymentStageprod9598BC2F", + }, + "/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "lambdaapiAccountAF0DCB01": Object { + "DependsOn": Array [ + "lambdaapiC1812993", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "lambdaapiCloudWatchRole7E36513A", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "lambdaapiC1812993": Object { + "Properties": Object { + "Description": "this is a test", + "Name": "api", + "Tags": Array [ + Object { + "Key": "Stack", + "Value": Object { + "Ref": "Stack", + }, + }, + Object { + "Key": "Stage", + "Value": Object { + "Ref": "Stage", + }, + }, + ], + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "lambdaapiCloudWatchRole7E36513A": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs", + ], + ], + }, + ], + "Tags": Array [ + Object { + "Key": "Stack", + "Value": Object { + "Ref": "Stack", + }, + }, + Object { + "Key": "Stage", + "Value": Object { + "Ref": "Stage", + }, + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "lambdaapiDeployment8E95B585cd631e09cd975e3e69d1ae39cb1f1c4c": Object { + "DependsOn": Array [ + "lambdaapiproxyANYA94E968A", + "lambdaapiproxyB573C729", + "lambdaapiANY4EBCADD1", + ], + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "lambdaapiC1812993", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "lambdaapiDeploymentStageprod9598BC2F": Object { + "Properties": Object { + "DeploymentId": Object { + "Ref": "lambdaapiDeployment8E95B585cd631e09cd975e3e69d1ae39cb1f1c4c", + }, + "RestApiId": Object { + "Ref": "lambdaapiC1812993", + }, + "StageName": "prod", + "Tags": Array [ + Object { + "Key": "Stack", + "Value": Object { + "Ref": "Stack", + }, + }, + Object { + "Key": "Stage", + "Value": Object { + "Ref": "Stage", + }, + }, + ], + }, + "Type": "AWS::ApiGateway::Stage", + }, + "lambdaapiproxyANYA94E968A": Object { + "Properties": Object { + "AuthorizationType": "NONE", + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "lambda8B5974B5", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Ref": "lambdaapiproxyB573C729", + }, + "RestApiId": Object { + "Ref": "lambdaapiC1812993", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "lambdaapiproxyANYApiPermissionTestlambdaapiC0087213ANYproxyB7FE8155": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "lambda8B5974B5", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "lambdaapiC1812993", + }, + "/test-invoke-stage/*/*", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "lambdaapiproxyANYApiPermissionlambdaapiC0087213ANYproxyAA2F1520": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "lambda8B5974B5", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "lambdaapiC1812993", + }, + "/", + Object { + "Ref": "lambdaapiDeploymentStageprod9598BC2F", + }, + "/*/*", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "lambdaapiproxyB573C729": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "lambdaapiC1812993", + "RootResourceId", + ], + }, + "PathPart": "{proxy+}", + "RestApiId": Object { + "Ref": "lambdaapiC1812993", + }, + }, + "Type": "AWS::ApiGateway::Resource", }, }, } @@ -98,9 +748,26 @@ Object { exports[`GuLambdaFunction should create a lambda function with no schedule rules 1`] = ` Object { + "Parameters": Object { + "Stack": Object { + "Default": "deploy", + "Description": "Name of this stack", + "Type": "String", + }, + "Stage": Object { + "AllowedValues": Array [ + "CODE", + "PROD", + ], + "Default": "CODE", + "Description": "Stage name", + "Type": "String", + }, + }, "Resources": Object { "lambda8B5974B5": Object { "DependsOn": Array [ + "lambdaServiceRoleDefaultPolicyBF6FA5E7", "lambdaServiceRole494E4CA6", ], "Properties": Object { @@ -116,6 +783,20 @@ Object { ], }, "Runtime": "nodejs12.x", + "Tags": Array [ + Object { + "Key": "Stack", + "Value": Object { + "Ref": "Stack", + }, + }, + Object { + "Key": "Stage", + "Value": Object { + "Ref": "Stage", + }, + }, + ], }, "Type": "AWS::Lambda::Function", }, @@ -147,9 +828,73 @@ Object { ], }, ], + "Tags": Array [ + Object { + "Key": "Stack", + "Value": Object { + "Ref": "Stack", + }, + }, + Object { + "Key": "Stage", + "Value": Object { + "Ref": "Stage", + }, + }, + ], }, "Type": "AWS::IAM::Role", }, + "lambdaServiceRoleDefaultPolicyBF6FA5E7": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::bucket1", + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::bucket1/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "lambdaServiceRoleDefaultPolicyBF6FA5E7", + "Roles": Array [ + Object { + "Ref": "lambdaServiceRole494E4CA6", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, }, } `; diff --git a/src/constructs/lambda/lambda.test.ts b/src/constructs/lambda/lambda.test.ts index 70232beb12..0d60f70ad5 100644 --- a/src/constructs/lambda/lambda.test.ts +++ b/src/constructs/lambda/lambda.test.ts @@ -1,13 +1,15 @@ import "@aws-cdk/assert/jest"; import { SynthUtils } from "@aws-cdk/assert"; +import { Schedule } from "@aws-cdk/aws-events"; import { Runtime } from "@aws-cdk/aws-lambda"; -import { Duration, Stack } from "@aws-cdk/core"; +import { App, Duration } from "@aws-cdk/core"; +import { GuStack } from "../core"; import { GuLambdaFunction } from "./lambda"; describe("GuLambdaFunction", () => { it("should create a lambda function with no schedule rules", () => { - const stack = new Stack(); + const stack = new GuStack(new App()); new GuLambdaFunction(stack, "lambda", { code: { bucket: "bucket1", key: "folder/to/key" }, @@ -19,7 +21,7 @@ describe("GuLambdaFunction", () => { }); it("should create a lambda function with a schedule to run every week", () => { - const stack = new Stack(); + const stack = new GuStack(new App()); new GuLambdaFunction(stack, "lambda", { code: { bucket: "bucket1", key: "folder/to/key" }, @@ -27,7 +29,7 @@ describe("GuLambdaFunction", () => { runtime: Runtime.NODEJS_12_X, rules: [ { - frequency: Duration.days(7), + schedule: Schedule.rate(Duration.days(7)), description: "run every week", }, ], @@ -35,4 +37,22 @@ describe("GuLambdaFunction", () => { expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); }); + + it("should create a lambda function with an api gateway", () => { + const stack = new GuStack(new App()); + + new GuLambdaFunction(stack, "lambda", { + code: { bucket: "bucket1", key: "folder/to/key" }, + handler: "handler.ts", + runtime: Runtime.NODEJS_12_X, + apis: [ + { + id: "api", + description: "this is a test", + }, + ], + }); + + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + }); }); diff --git a/src/constructs/lambda/lambda.ts b/src/constructs/lambda/lambda.ts index 519b34f172..ef0f2977a5 100644 --- a/src/constructs/lambda/lambda.ts +++ b/src/constructs/lambda/lambda.ts @@ -1,20 +1,28 @@ -import { Rule, Schedule } from "@aws-cdk/aws-events"; +import type { LambdaRestApiProps } from "@aws-cdk/aws-apigateway"; +import { LambdaRestApi } from "@aws-cdk/aws-apigateway"; +import type { Schedule } from "@aws-cdk/aws-events"; +import { Rule } from "@aws-cdk/aws-events"; import { LambdaFunction } from "@aws-cdk/aws-events-targets"; import { Code, Function } from "@aws-cdk/aws-lambda"; import type { FunctionProps } from "@aws-cdk/aws-lambda"; import { Bucket } from "@aws-cdk/aws-s3"; -import type { Construct, Duration } from "@aws-cdk/core"; +import type { GuStack } from "../core"; + +interface ApiProps extends Omit { + id: string; +} interface GuFunctionProps extends Omit { code: { bucket: string; key: string }; rules?: Array<{ - frequency: Duration; + schedule: Schedule; description?: string; }>; + apis?: ApiProps[]; } export class GuLambdaFunction extends Function { - constructor(scope: Construct, id: string, props: GuFunctionProps) { + constructor(scope: GuStack, id: string, props: GuFunctionProps) { const bucket = Bucket.fromBucketName(scope, `${id}-bucket`, props.code.bucket); const code = Code.fromBucket(bucket, props.code.key); super(scope, id, { @@ -22,16 +30,23 @@ export class GuLambdaFunction extends Function { code, }); - props.rules?.forEach((rule) => { - const frequency = rule.frequency; - const schedule = Schedule.rate(frequency); + props.rules?.forEach((rule, index) => { const target = new LambdaFunction(this); - new Rule(this, `${id}-${frequency.toHumanString()}`, { - schedule: schedule, + new Rule(this, `${id}-${rule.schedule.expressionString}-${index}`, { + schedule: rule.schedule, targets: [target], ...(rule.description && { description: rule.description }), enabled: true, }); }); + + props.apis?.forEach((api) => { + new LambdaRestApi(this, api.id, { + handler: this, + ...api, + }); + }); + + bucket.grantRead(this); } }