diff --git a/libs/wingsdk/src/target-awscdk/app.ts b/libs/wingsdk/src/target-awscdk/app.ts index 9892bdffe8f..7e376a733f0 100644 --- a/libs/wingsdk/src/target-awscdk/app.ts +++ b/libs/wingsdk/src/target-awscdk/app.ts @@ -7,6 +7,7 @@ import stringify from "safe-stable-stringify"; import { Bucket } from "./bucket"; import { Counter } from "./counter"; import { Function } from "./function"; +import { OnDeploy } from "./on-deploy"; import { Queue } from "./queue"; import { Schedule } from "./schedule"; import { Secret } from "./secret"; @@ -18,6 +19,7 @@ import { BUCKET_FQN, COUNTER_FQN, FUNCTION_FQN, + ON_DEPLOY_FQN, QUEUE_FQN, SECRET_FQN, TOPIC_FQN, @@ -161,6 +163,9 @@ export class App extends CoreApp { case SECRET_FQN: return new Secret(scope, id, args[0]); + + case ON_DEPLOY_FQN: + return new OnDeploy(scope, id, args[0], args[1]); } return undefined; } diff --git a/libs/wingsdk/src/target-awscdk/on-deploy.ts b/libs/wingsdk/src/target-awscdk/on-deploy.ts new file mode 100644 index 00000000000..692083ef38e --- /dev/null +++ b/libs/wingsdk/src/target-awscdk/on-deploy.ts @@ -0,0 +1,41 @@ +import { Trigger } from "aws-cdk-lib/triggers"; +import { Construct } from "constructs"; +import { Function as AwsFunction } from "./function"; +import * as cloud from "../cloud"; +import * as core from "../core"; + +/** + * AWS implementation of `cloud.OnDeploy`. + * + * @inflight `@winglang/sdk.cloud.IOnDeployClient` + */ +export class OnDeploy extends cloud.OnDeploy { + constructor( + scope: Construct, + id: string, + handler: cloud.IOnDeployHandler, + props: cloud.OnDeployProps = {} + ) { + super(scope, id, handler, props); + + let fn = cloud.Function._newFunction(this, "Function", handler, props); + const awsFn = fn as AwsFunction; + + let trigger = new Trigger(this, "Trigger", { + handler: awsFn._function, + }); + + trigger.executeAfter(...(props.executeAfter ?? [])); + trigger.executeBefore(...(props.executeBefore ?? [])); + } + + /** @internal */ + public _toInflight(): core.Code { + return core.InflightClient.for( + __dirname.replace("target-awscdk", "shared-aws"), + __filename, + "OnDeployClient", + [] + ); + } +} diff --git a/libs/wingsdk/test/target-awscdk/__snapshots__/on-deploy.test.ts.snap b/libs/wingsdk/test/target-awscdk/__snapshots__/on-deploy.test.ts.snap new file mode 100644 index 00000000000..080f5376473 --- /dev/null +++ b/libs/wingsdk/test/target-awscdk/__snapshots__/on-deploy.test.ts.snap @@ -0,0 +1,657 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`create an OnDeploy 1`] = ` +{ + "Outputs": { + "WingTestRunnerFunctionArns": { + "Value": "[]", + }, + }, + "Parameters": { + "BootstrapVersion": { + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", + "Type": "AWS::SSM::Parameter::Value", + }, + }, + "Resources": { + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderHandler97BECD91": { + "DependsOn": [ + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A", + ], + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", + }, + "S3Key": "f852bfab94a36947a0c426b4c9aa55f31d1ba844ac1c6c40af84b941fd4ae8bb.zip", + }, + "Handler": "__entrypoint__.handler", + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 900, + }, + "Type": "AWS::Lambda::Function", + }, + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + }, + ], + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "lambda:InvokeFunction", + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "myondeployFunction47551748", + "Arn", + ], + }, + ":*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "Inline", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "myondeployFunction47551748": { + "DependsOn": [ + "myondeployFunctionServiceRole7277F8BB", + ], + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", + }, + "S3Key": "34031e10a718327bb9ec731d7b54025eebeef68c03bb4f5a3d8e6ad5262a0517.zip", + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "myondeployFunctionServiceRole7277F8BB", + "Arn", + ], + }, + "Runtime": "nodejs18.x", + "Timeout": 30, + }, + "Type": "AWS::Lambda::Function", + }, + "myondeployFunctionCurrentVersion6BC23754dc8a690e8db1c57e1302de9438f9dfa2": { + "Properties": { + "FunctionName": { + "Ref": "myondeployFunction47551748", + }, + }, + "Type": "AWS::Lambda::Version", + }, + "myondeployFunctionServiceRole7277F8BB": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "myondeployTrigger63552203": { + "DeletionPolicy": "Delete", + "Properties": { + "HandlerArn": { + "Ref": "myondeployFunctionCurrentVersion6BC23754dc8a690e8db1c57e1302de9438f9dfa2", + }, + "ServiceToken": { + "Fn::GetAtt": [ + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderHandler97BECD91", + "Arn", + ], + }, + }, + "Type": "Custom::Trigger", + "UpdateReplacePolicy": "Delete", + }, + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5", + ], + { + "Ref": "BootstrapVersion", + }, + ], + }, + ], + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", + }, + ], + }, + }, +} +`; + +exports[`execute OnDeploy after other resources 1`] = ` +{ + "Outputs": { + "WingTestRunnerFunctionArns": { + "Value": "[]", + }, + }, + "Parameters": { + "BootstrapVersion": { + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", + "Type": "AWS::SSM::Parameter::Value", + }, + }, + "Resources": { + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderHandler97BECD91": { + "DependsOn": [ + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A", + ], + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", + }, + "S3Key": "f852bfab94a36947a0c426b4c9aa55f31d1ba844ac1c6c40af84b941fd4ae8bb.zip", + }, + "Handler": "__entrypoint__.handler", + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 900, + }, + "Type": "AWS::Lambda::Function", + }, + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + }, + ], + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "lambda:InvokeFunction", + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "myondeployFunction47551748", + "Arn", + ], + }, + ":*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "Inline", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "mybucketD601CBAA": { + "DeletionPolicy": "Delete", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + }, + "myondeployFunction47551748": { + "DependsOn": [ + "myondeployFunctionServiceRole7277F8BB", + ], + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", + }, + "S3Key": "34031e10a718327bb9ec731d7b54025eebeef68c03bb4f5a3d8e6ad5262a0517.zip", + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "myondeployFunctionServiceRole7277F8BB", + "Arn", + ], + }, + "Runtime": "nodejs18.x", + "Timeout": 30, + }, + "Type": "AWS::Lambda::Function", + }, + "myondeployFunctionCurrentVersion6BC23754dc8a690e8db1c57e1302de9438f9dfa2": { + "Properties": { + "FunctionName": { + "Ref": "myondeployFunction47551748", + }, + }, + "Type": "AWS::Lambda::Version", + }, + "myondeployFunctionServiceRole7277F8BB": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "myondeployTrigger63552203": { + "DeletionPolicy": "Delete", + "DependsOn": [ + "mybucketD601CBAA", + ], + "Properties": { + "HandlerArn": { + "Ref": "myondeployFunctionCurrentVersion6BC23754dc8a690e8db1c57e1302de9438f9dfa2", + }, + "ServiceToken": { + "Fn::GetAtt": [ + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderHandler97BECD91", + "Arn", + ], + }, + }, + "Type": "Custom::Trigger", + "UpdateReplacePolicy": "Delete", + }, + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5", + ], + { + "Ref": "BootstrapVersion", + }, + ], + }, + ], + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", + }, + ], + }, + }, +} +`; + +exports[`execute OnDeploy before other resources 1`] = ` +{ + "Outputs": { + "WingTestRunnerFunctionArns": { + "Value": "[]", + }, + }, + "Parameters": { + "BootstrapVersion": { + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", + "Type": "AWS::SSM::Parameter::Value", + }, + }, + "Resources": { + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderHandler97BECD91": { + "DependsOn": [ + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A", + ], + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", + }, + "S3Key": "f852bfab94a36947a0c426b4c9aa55f31d1ba844ac1c6c40af84b941fd4ae8bb.zip", + }, + "Handler": "__entrypoint__.handler", + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 900, + }, + "Type": "AWS::Lambda::Function", + }, + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + }, + ], + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "lambda:InvokeFunction", + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "myondeployFunction47551748", + "Arn", + ], + }, + ":*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "Inline", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "mybucketD601CBAA": { + "DeletionPolicy": "Delete", + "DependsOn": [ + "myondeployTrigger63552203", + ], + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + }, + "myondeployFunction47551748": { + "DependsOn": [ + "myondeployFunctionServiceRole7277F8BB", + ], + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", + }, + "S3Key": "34031e10a718327bb9ec731d7b54025eebeef68c03bb4f5a3d8e6ad5262a0517.zip", + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "myondeployFunctionServiceRole7277F8BB", + "Arn", + ], + }, + "Runtime": "nodejs18.x", + "Timeout": 30, + }, + "Type": "AWS::Lambda::Function", + }, + "myondeployFunctionCurrentVersion6BC23754dc8a690e8db1c57e1302de9438f9dfa2": { + "Properties": { + "FunctionName": { + "Ref": "myondeployFunction47551748", + }, + }, + "Type": "AWS::Lambda::Version", + }, + "myondeployFunctionServiceRole7277F8BB": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "myondeployTrigger63552203": { + "DeletionPolicy": "Delete", + "Properties": { + "HandlerArn": { + "Ref": "myondeployFunctionCurrentVersion6BC23754dc8a690e8db1c57e1302de9438f9dfa2", + }, + "ServiceToken": { + "Fn::GetAtt": [ + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderHandler97BECD91", + "Arn", + ], + }, + }, + "Type": "Custom::Trigger", + "UpdateReplacePolicy": "Delete", + }, + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5", + ], + { + "Ref": "BootstrapVersion", + }, + ], + }, + ], + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", + }, + ], + }, + }, +} +`; diff --git a/libs/wingsdk/test/target-awscdk/on-deploy.test.ts b/libs/wingsdk/test/target-awscdk/on-deploy.test.ts new file mode 100644 index 00000000000..224b50057e3 --- /dev/null +++ b/libs/wingsdk/test/target-awscdk/on-deploy.test.ts @@ -0,0 +1,63 @@ +import { Capture, Match, Template } from "aws-cdk-lib/assertions"; +import { expect, test } from "vitest"; +import { Bucket, OnDeploy } from "../../src/cloud"; +import * as awscdk from "../../src/target-awscdk"; +import { Testing } from "../../src/testing"; +import { sanitizeCode, mkdtemp } from "../util"; + +const CDK_APP_OPTS = { + stackName: "my-project", +}; + +const INFLIGHT_CODE = `async handle(name) { console.log("Hello, " + name); }`; + +test("create an OnDeploy", () => { + // GIVEN + const app = new awscdk.App({ outdir: mkdtemp(), ...CDK_APP_OPTS }); + const handler = Testing.makeHandler(app, "Handler", INFLIGHT_CODE); + OnDeploy._newOnDeploy(app, "my_on_deploy", handler); + const output = app.synth(); + + // THEN + const template = Template.fromJSON(JSON.parse(output)); + template.resourceCountIs("Custom::Trigger", 1); + expect(template.toJSON()).toMatchSnapshot(); +}); + +test("execute OnDeploy after other resources", () => { + // GIVEN + const app = new awscdk.App({ outdir: mkdtemp(), ...CDK_APP_OPTS }); + const bucket = Bucket._newBucket(app, "my_bucket"); + const handler = Testing.makeHandler(app, "Handler", INFLIGHT_CODE); + OnDeploy._newOnDeploy(app, "my_on_deploy", handler, { + executeAfter: [bucket], + }); + const output = app.synth(); + + // THEN + const template = Template.fromJSON(JSON.parse(output)); + template.resourceCountIs("Custom::Trigger", 1); + template.hasResource("Custom::Trigger", { + DependsOn: Match.anyValue(), + }); + expect(template.toJSON()).toMatchSnapshot(); +}); + +test("execute OnDeploy before other resources", () => { + // GIVEN + const app = new awscdk.App({ outdir: mkdtemp(), ...CDK_APP_OPTS }); + const bucket = Bucket._newBucket(app, "my_bucket"); + const handler = Testing.makeHandler(app, "Handler", INFLIGHT_CODE); + OnDeploy._newOnDeploy(app, "my_on_deploy", handler, { + executeBefore: [bucket], + }); + const output = app.synth(); + + // THEN + const template = Template.fromJSON(JSON.parse(output)); + template.resourceCountIs("Custom::Trigger", 1); + template.hasResource("AWS::S3::Bucket", { + DependsOn: Match.anyValue(), + }); + expect(template.toJSON()).toMatchSnapshot(); +});