From dd14fa3f166e27657de848d7fb4be59e6772e4e5 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Mon, 11 Mar 2024 19:49:18 +0200 Subject: [PATCH 1/2] feat: improved extensibility of the `awscdk` platform (#5880) A few extensibility hooks for the AWS CDK platform. ### Custom functions If you wish to customize how `awscdk.Function` creates the underlying AWS CDK `Function`, you can now extend the base `awscdk.Function` class and override the `createFunction()` method which accepts a `Code` object with the bundled inflight and the `cloud.FunctionProps` passed by the user. It returns an `aws-cdk-lib.aws_lambda.Function` object. ### Custom stacks Fixes #5877 to allow customizing the root stack. This can be used to set custom properties when creating a stack such as `synthesizer` or use a subclass. The `App` class now has a `stackFactory` property that can be used to customize the root stack: ```js import { App } from "@winglang/platform-awscdk"; import { platform } from "@winglang/sdk"; export class Platform implements platform.IPlatform { public readonly target = "awscdk"; public newApp?(appProps: any): any { return new App({ ...appProps, stackFactory: (app: cdk.App, stackName: string) => { // customize here! return new cdk.Stack(app, stackName); } }); } } ``` ### Additional bug fixes Fixes #5871 so that when implicit functions are created by other resources such as `queue.setConsumer()` or `api.xxx()`, now we go through the dependency injection system. This means that custom function implementations (returned from `newInstance()`) will be respected. Fixes #5873 by changing the various `onLift()` methods to structurally check that the host has a `awscdkFunction` property instead of nominal typing. ## Checklist - [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted) - [x] Description explains motivation and solution - [x] Tests added (always) - [x] Docs updated (only required for features) - [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*. --- docs/docs/02-concepts/03-platforms.md | 18 ++-- docs/docs/055-platforms/_category_.yml | 6 ++ docs/docs/055-platforms/awscdk.md | 78 +++++++++++++++++ docs/docs/055-platforms/sim.md | 35 ++++++++ docs/docs/055-platforms/tf-aws.md | 47 +++++++++++ docs/docs/055-platforms/tf-azure.md | 28 ++++++ docs/docs/055-platforms/tf-gcp.md | 29 +++++++ docs/docs/06-tools/01-cli.md | 108 ++---------------------- libs/awscdk/src/api.ts | 40 ++++----- libs/awscdk/src/app.ts | 15 +++- libs/awscdk/src/bucket.ts | 32 +++---- libs/awscdk/src/counter.ts | 10 +-- libs/awscdk/src/dynamodb-table.ts | 12 +-- libs/awscdk/src/endpoint.ts | 5 -- libs/awscdk/src/function.ts | 103 +++++++++++++--------- libs/awscdk/src/on-deploy.ts | 9 +- libs/awscdk/src/queue.ts | 20 ++--- libs/awscdk/src/schedule.ts | 15 ++-- libs/awscdk/src/secret.ts | 10 +-- libs/awscdk/src/test-runner.ts | 10 +-- libs/awscdk/src/tokens.ts | 11 ++- libs/awscdk/src/topic.ts | 19 ++--- libs/awscdk/test/app.test.ts | 48 +++++++++++ libs/awscdk/test/bucket.test.ts | 7 +- libs/awscdk/test/counter.test.ts | 7 +- libs/awscdk/test/dynamodb-table.test.ts | 7 +- libs/awscdk/test/function.test.ts | 7 +- libs/awscdk/test/on-deploy.test.ts | 9 +- libs/awscdk/test/queue.test.ts | 7 +- libs/awscdk/test/schedule.test.ts | 7 +- libs/awscdk/test/secret.test.ts | 6 +- libs/awscdk/test/topic.test.ts | 7 +- libs/awscdk/test/util.ts | 7 +- 33 files changed, 464 insertions(+), 315 deletions(-) create mode 100644 docs/docs/055-platforms/_category_.yml create mode 100644 docs/docs/055-platforms/awscdk.md create mode 100644 docs/docs/055-platforms/sim.md create mode 100644 docs/docs/055-platforms/tf-aws.md create mode 100644 docs/docs/055-platforms/tf-azure.md create mode 100644 docs/docs/055-platforms/tf-gcp.md create mode 100644 libs/awscdk/test/app.test.ts diff --git a/docs/docs/02-concepts/03-platforms.md b/docs/docs/02-concepts/03-platforms.md index 7a3f9108706..ccb67f696f7 100644 --- a/docs/docs/02-concepts/03-platforms.md +++ b/docs/docs/02-concepts/03-platforms.md @@ -7,8 +7,6 @@ keywords: [platforms, targets, target, platform, aws, gcp, azure, sim, terraform When working with the Wing programming language, an integral part of the compilation process is the use of platform. In essence, platform specify how and where your application is deployed. They determine both the cloud environment and the provisioning engine that the code will be deployed with. -## Platforms - You can view the list of available builtin platform with the `wing compile --help` command. Here is an example of the output: ```sh @@ -83,11 +81,17 @@ Though this may be a bit verbose. As an alternative you can use a values file. V Here is an example of using a `wing.toml` file to provide the same parameters as above: ```toml -[tf-aws] -vpc = "existing" -vpcId = "vpc-1234567890" -privateSubnetId = "subnet-1234567890" -publicSubnetId = "subnet-1234567890" +[ tf-aws ] +# vpc can be set to "new" or "existing" +vpc = "new" +# vpc_lambda will ensure that lambda functions are created within the vpc on the private subnet +vpc_lambda = true +# vpc_api_gateway will ensure that the api gateway is created within the vpc on the private subnet +vpc_api_gateway = true +# The following parameters will be required if using "existing" vpc +# vpc_id = "vpc-123xyz" +# private_subnet_ids = ["subnet-123xyz"] +# public_subnet_ids = ["subnet-123xyz"] ``` #### Target-specific code diff --git a/docs/docs/055-platforms/_category_.yml b/docs/docs/055-platforms/_category_.yml new file mode 100644 index 00000000000..3e7b49bbf48 --- /dev/null +++ b/docs/docs/055-platforms/_category_.yml @@ -0,0 +1,6 @@ +label: Platforms +collapsible: true +collapsed: true +link: + type: generated-index + title: Platforms diff --git a/docs/docs/055-platforms/awscdk.md b/docs/docs/055-platforms/awscdk.md new file mode 100644 index 00000000000..d73b2dbe838 --- /dev/null +++ b/docs/docs/055-platforms/awscdk.md @@ -0,0 +1,78 @@ +--- +title: AWS CDK +id: awscdk +sidebar_label: awscdk +description: AWS CDK platform +keywords: [Wing reference, Wing language, language, Wing language spec, Wing programming language, aws, awscdk, amazon web services, cloudformation] +--- + +The `@winglang/platform-awscdk` [platform](../02-concepts/03-platforms.md) compiles your program for the AWS CDK (CloudFormation). + +## Usage + +You will need to install the `@winglang/platform-awscdk` library in order to use this platform. + +```sh +$ npm i @winglang/platform-awscdk +``` + +This platform requires the environment variable `CDK_STACK_NAME` to be set to the name of the CDK +stack to synthesize. + +```sh +$ export CDK_STACK_NAME="my-project" +$ wing compile --platform @winglang/platform-awscdk [entrypoint] +``` + +## Parameters + +The `CDK_STACK_NAME` environment variable specifies the name of the CDK stack to synthesize. + +## Output + +The output includes both a AWS-CDK configuration file (under `target/.awscdk`) and +JavaScript bundles that include inflight code that executes on compute platforms such as AWS Lambda. + +## Deployment + +To deploy your app, you will first need to install the [AWS CDK +CLI](https://docs.aws.amazon.com/cdk/v2/guide/cli.html). + +If not previously done, you will need to bootstrap your environment (account/region): + +```sh +$ cd bootstrap --app target/app.awscdk +``` + +And then you can deploy: + +```sh +$ cdk deploy --app target/app.awscdk +``` + +## Customizations + +### Custom CDK Stack + +The `App` class has a `stackFactory` property that can be used to customize how the root CDK stack +is created. + +To use this, create a custom platform like this: + +```js +import { App } from "@winglang/platform-awscdk"; +import { platform } from "@winglang/sdk"; + +export class Platform implements platform.IPlatform { + public readonly target = "awscdk"; + public newApp?(appProps: any): any { + return new App({ + ...appProps, + stackFactory: (app: cdk.App, stackName: string) => { + // customize here! + return new cdk.Stack(app, stackName); + } + }); + } +} +``` diff --git a/docs/docs/055-platforms/sim.md b/docs/docs/055-platforms/sim.md new file mode 100644 index 00000000000..e209de4e552 --- /dev/null +++ b/docs/docs/055-platforms/sim.md @@ -0,0 +1,35 @@ +--- +title: Wing Cloud Simulator +id: sim +sidebar_label: sim +description: Simulator Platform +keywords: [Wing reference, Wing language, language, Wing language spec, Wing programming language, simulator, sim, wing simulator] +--- + +The Wing Cloud Simulator is a tool for running Wing applications on a single host. It offers a +simple localhost implementation of all the resources of the Wing Cloud Library to allow developers +to develop and functionally test cloud applications without having to deploy to the cloud. + +The `sim` [platform]((../02-concepts/03-platforms.md)) compiles your program so it can run in the +Wing Cloud Simulator. + +## Usage + +```sh +$ wing compile [entrypoint] --platform sim +``` + +## Parameters + +No parameters. + +## Output + +The output will be found under `target/.wsim`. + +## Deployment + +The Wing Simulator can be used in one of these methods: + +* Interactively through the [Wing Console](/docs/start-here/local) +* Using the `wing run|it target/.wsim` command through the Wing CLI. diff --git a/docs/docs/055-platforms/tf-aws.md b/docs/docs/055-platforms/tf-aws.md new file mode 100644 index 00000000000..d09ddfb7484 --- /dev/null +++ b/docs/docs/055-platforms/tf-aws.md @@ -0,0 +1,47 @@ +--- +title: Terraform/AWS +id: tf-aws +sidebar_label: tf-aws +description: Terraform/AWS platform +keywords: [Wing reference, Wing language, language, Wing language spec, Wing programming language, cli, terraform, aws, tf-aws, tfaws, amazon web services, platform] +--- + +The `tf-gcp` [platform](../02-concepts/03-platforms.md) compiles your program for Terraform and run on AWS. + +## Usage + +```sh +$ wing compile --platform tf-aws [entrypoint] +``` + +## Parameters + +The `tf-aws` platform supports the following parameters (in `wing.toml`): + +* `vpc` - Determine whether to create a new VPC or use an existing one. Allowed values: `"new"` or `"existing"`. +* `private_subnet_ids` (array of strings) - If using an existing VPC, provide the private subnet IDs. +* `public_subnet_ids` (array of strings) - If using an existing VPC, provide the public subnet IDs. +* `vpc_api_gateway` (boolean) - Whether Api gateways should be deployed in a VPC. +* `vpc_lambda` (boolean) - Whether Lambda functions should be deployed in a VPC. + +Example `wing.toml`: + +```toml +[ tf-aws ] +vpc = "new" +vpc_lambda = true +vpc_api_gateway = true +vpc_id = "vpc-123xyz" +private_subnet_ids = ["subnet-123xyz"] +public_subnet_ids = ["subnet-123xyz"] +``` + + +## Output + +The output includes both a Terraform configuration file (under `target/cdktf.out/stacks/root`) and +JavaScript bundles that include inflight code that executes on compute platform such as AWS Lambda. + +## Deployment + +You can deploy your stack to AWS using Terraform ([instructions](/docs/start-here/aws)). diff --git a/docs/docs/055-platforms/tf-azure.md b/docs/docs/055-platforms/tf-azure.md new file mode 100644 index 00000000000..c9ed4a6f11d --- /dev/null +++ b/docs/docs/055-platforms/tf-azure.md @@ -0,0 +1,28 @@ +--- +title: Terraform/Azure +id: tf-azure +sidebar_label: tf-azure +description: Terraform/Azure platform +keywords: [Wing reference, Wing language, language, Wing language spec, Wing programming language, cli, terraform, tf-azure, azure, microsoft azure, platform] +--- + +The `tf-azure` [platform](../02-concepts/03-platforms.md) compiles your program for Terraform and run on Azure. + +## Usage + +```sh +$ export AZURE_LOCATION="East US" +$ wing compile [entrypoint] --platform tf-azure +``` + +## Parameters + +The environment variable `AZURE_LOCATION` is required and indicates the [deployment +location](https://github.com/claranet/terraform-azurerm-regions/blob/master/REGIONS.md) of your +stack. + +## Output + +The output includes both a Terraform configuration file (under `target/cdktf.out/stacks/root`) and +JavaScript bundles that include inflight code that executes on compute platform such as Azure +Functions. diff --git a/docs/docs/055-platforms/tf-gcp.md b/docs/docs/055-platforms/tf-gcp.md new file mode 100644 index 00000000000..4c11fc555ab --- /dev/null +++ b/docs/docs/055-platforms/tf-gcp.md @@ -0,0 +1,29 @@ +--- +title: Terraform/GCP +id: tf-gcp +sidebar_label: tf-gcp +description: Terraform/GCP platform +keywords: [Wing reference, Wing language, language, Wing language spec, Wing programming language, cli, terraform, tf-gcp, gcp, google cloud platform, platform] +--- + +The `tf-gcp` [platform](../02-concepts/03-platforms.md) compiles your program for Terraform and run on Google Cloud Platform. + +## Usage + +```sh +$ export GOOGLE_PROJECT_ID="my-project" +$ export GOOGLE_STORAGE_LOCATION="US" +$ wing compile [entrypoint] --platform tf-gcp +``` + +## Parameters + +The environment variable `GOOGLE_STORAGE_LOCATION` is required and indicates the [deployment +location](https://cloud.google.com/storage/docs/locations) of all storage +resources (such as buckets and queues). + +The environment variable `GOOGLE_PROJECT_ID` is required and indicates +the project ID of your stack. + +The output includes both a Terraform configuration file (under `target/cdktf.out/stacks/root`) and +JavaScript bundles that include inflight code that executes on compute platform such as Google Cloud Functions. diff --git a/docs/docs/06-tools/01-cli.md b/docs/docs/06-tools/01-cli.md index d27cf9ab216..1abbbf6712e 100644 --- a/docs/docs/06-tools/01-cli.md +++ b/docs/docs/06-tools/01-cli.md @@ -70,108 +70,16 @@ By default, `wing compile` will look for exactly one file named `main.w` or endi ::: -The --platform option specifies the target platform to compile for. The default platform is `sim`. -The following platforms are built-in: +The --platform option (or `-t`) specifies the target platform to compile for. The default platform +is `sim`. -* `sim` - [Wing Simulator](#sim-target) -* `tf-aws` - Terraform/AWS -* `tf-azure` - Terraform/Azure -* `tf-gcp` - Terraform/Google Cloud Platform +You can use one of the built-in platform providers: -### `sim` Platform - -The Wing program is going to be compiled for the Wing simulator (`.wsim`). - -Usage: - -```sh -$ wing compile [entrypoint] --platform sim -``` - -The output will be found under `target/.wsim` and can be opened in two ways: - -* Interactively through the [Wing Console](/docs/start-here/local) -* Using the `wing run|it target/.wsim` command through the Wing CLI. - - -### `tf-aws` Platform - -Compiles your program for Terraform and run on AWS. - -Usage: - -```sh -$ wing compile [entrypoint] --platform tf-aws -``` - -The output includes both a Terraform configuration file (under `target/cdktf.out/stacks/root`) and -JavaScript bundles that include inflight code that executes on compute platform such as AWS Lambda. - -You can deploy your stack to AWS using Terraform ([instructions](/docs/start-here/aws)). - - - -### `tf-azure` Platform - -Compiles your program for Terraform and run on Azure. - -Usage: - -```sh -$ export AZURE_LOCATION="East US" -$ wing compile [entrypoint] --platform tf-azure -``` - -The variable `AZURE_LOCATION` is required and indicates the [deployment -location](https://github.com/claranet/terraform-azurerm-regions/blob/master/REGIONS.md) of your -stack. - -The output includes both a Terraform configuration file (under `target/cdktf.out/stacks/root`) and -JavaScript bundles that include inflight code that executes on compute platform such as Azure -Functions. - -### `tf-gcp` Platform - -Compiles your program for Terraform and run on Google Cloud Platform. - -Usage: - -```sh -$ export GOOGLE_PROJECT_ID="my-project" -$ export GOOGLE_STORAGE_LOCATION="US" -$ wing compile [entrypoint] --platform tf-gcp -``` - -The variable `GOOGLE_STORAGE_LOCATION` is required and indicates the [deployment -location](https://cloud.google.com/storage/docs/locations) of all storage -resources (such as buckets and queues). The variable `GOOGLE_PROJECT_ID` is required and indicates -the project ID of your stack. - -The output includes both a Terraform configuration file (under `target/cdktf.out/stacks/root`) and -JavaScript bundles that include inflight code that executes on compute platform such as Google Cloud Functions. - - -### `awscdk` Platform - -Compiles your program for AWS CDK with CloudFormation to run on AWS. - -Usage: - -```sh -# npm init is only needed if you don't already have a package.json file -$ npm init -y -$ npm i @winglang/platform-awscdk -$ export CDK_STACK_NAME="my-project" -$ wing compile --platform @winglang/platform-awscdk [entrypoint] -``` - -The output includes both a AWS-CDK configuration file (under `target/.awscdk`) and -JavaScript bundles that include inflight code that executes on compute platforms such as AWS Lambda. - -You can deploy your stack to AWS by installing the [AWS CDK Toolkit](https://docs.aws.amazon.com/cdk/v2/guide/cli.html) and running: -```sh -$ cdk deploy --app target/app.awscdk -``` +* [Wing Cloud Simulator](../055-platforms/sim.md) - `sim` +* [Terraform/AWS](../055-platforms/tf-aws.md) - `tf-aws` +* [Terraform/Azure](../055-platforms/tf-azure.md) - `tf-azure` +* [Terraform/GCP](../055-platforms/tf-gcp.md) - `tf-gcp` +* [AWS CDK](../055-platforms/awscdk.md) - `@winglang/platform-awscdk` ## Test: `wing test` diff --git a/libs/awscdk/src/api.ts b/libs/awscdk/src/api.ts index a188436ea82..927e190220a 100644 --- a/libs/awscdk/src/api.ts +++ b/libs/awscdk/src/api.ts @@ -10,18 +10,18 @@ import { import { CfnPermission } from "aws-cdk-lib/aws-lambda"; import { Construct } from "constructs"; import { App } from "./app"; -import { Function } from "./function"; import { cloud, core, std } from "@winglang/sdk"; import { convertBetweenHandlers } from "@winglang/sdk/lib/shared/convert"; import { IAwsApi, STAGE_NAME } from "@winglang/sdk/lib/shared-aws/api"; import { API_DEFAULT_RESPONSE } from "@winglang/sdk/lib/shared-aws/api.default"; +import { isAwsCdkFunction } from "./function"; /** * AWS Implementation of `cloud.Api`. */ export class Api extends cloud.Api implements IAwsApi { private readonly api: WingRestApi; - private readonly handlers: Record = {}; + private readonly handlers: Record = {}; private readonly endpoint: cloud.Endpoint; constructor(scope: Construct, id: string, props: cloud.ApiProps = {}) { @@ -187,12 +187,8 @@ export class Api extends cloud.Api implements IAwsApi { inflight: cloud.IApiEndpointHandler, method: string, path: string - ): Function { - let fn = this.addInflightHandler(inflight, method, path); - if (!(fn instanceof Function)) { - throw new Error("Api only supports creating tfaws.Function right now"); - } - return fn; + ): cloud.Function { + return this.addInflightHandler(inflight, method, path); } /** @@ -205,7 +201,7 @@ export class Api extends cloud.Api implements IAwsApi { inflight: cloud.IApiEndpointHandler, method: string, path: string - ): Function { + ): cloud.Function { let handler = this.handlers[inflight._id]; if (!handler) { const newInflight = convertBetweenHandlers( @@ -218,7 +214,7 @@ export class Api extends cloud.Api implements IAwsApi { } ); const prefix = `${method.toLowerCase()}${path.replace(/\//g, "_")}_}`; - handler = new Function( + handler = new cloud.Function( this, App.of(this).makeId(this, prefix), newInflight @@ -231,12 +227,7 @@ export class Api extends cloud.Api implements IAwsApi { /** @internal */ public onLift(host: std.IInflightHost, ops: string[]): void { - if (!(host instanceof Function)) { - throw new Error("apis can only be bound by awscdk.Function for now"); - } - host.addEnvironment(this.urlEnvName(), this.url); - super.onLift(host, ops); } @@ -338,7 +329,7 @@ class WingRestApi extends Construct { * @param handler Lambda function to handle the endpoint * @returns OpenApi spec extension for the endpoint */ - public addEndpoint(path: string, method: string, handler: Function) { + public addEndpoint(path: string, method: string, handler: cloud.Function) { const endpointExtension = this.createApiSpecExtension(handler); this.addHandlerPermissions(path, method, handler); return endpointExtension; @@ -349,10 +340,14 @@ class WingRestApi extends Construct { * @param handler Lambda function to handle the endpoint * @returns OpenApi extension object for the endpoint and handler */ - private createApiSpecExtension(handler: Function) { + private createApiSpecExtension(handler: cloud.Function) { + if (!isAwsCdkFunction(handler)) { + throw new Error("Expected 'handler' to implement IAwsCdkFunction"); + } + const extension = { "x-amazon-apigateway-integration": { - uri: `arn:aws:apigateway:${this.region}:lambda:path/2015-03-31/functions/${handler.functionArn}/invocations`, + uri: `arn:aws:apigateway:${this.region}:lambda:path/2015-03-31/functions/${handler.awscdkFunction.functionArn}/invocations`, type: "aws_proxy", httpMethod: "POST", responses: { @@ -377,13 +372,18 @@ class WingRestApi extends Construct { private addHandlerPermissions = ( path: string, method: string, - handler: Function + handler: cloud.Function ) => { + if (!isAwsCdkFunction(handler)) { + throw new Error("Expected 'handler' to implement IAwsCdkFunction"); + } + const pathHash = createHash("sha1").update(path).digest("hex").slice(-8); const permissionId = `${method}-${pathHash}`; + new CfnPermission(this, `permission-${permissionId}`, { action: "lambda:InvokeFunction", - functionName: handler.functionName, + functionName: handler.awscdkFunction.functionName, principal: "apigateway.amazonaws.com", sourceArn: this.api.arnForExecuteApi(method, Api._toOpenApiPath(path)), }); diff --git a/libs/awscdk/src/app.ts b/libs/awscdk/src/app.ts index 5f914fdf20d..ccf9addc6be 100644 --- a/libs/awscdk/src/app.ts +++ b/libs/awscdk/src/app.ts @@ -44,9 +44,18 @@ import { registerTokenResolver } from "@winglang/sdk/lib/core/tokens"; export interface CdkAppProps extends core.AppProps { /** * CDK Stack Name - * @default - undefined + * + * @default - read from the CDK_STACK_NAME environment variable */ readonly stackName?: string; + + /** + * A hook for customizating the way the root CDK stack is created. You can override this if you wish to use a custom stack + * instead of the default `cdk.Stack`. + * + * @default - creates a standard `cdk.Stack` + */ + readonly stackFactory?: (app: cdk.App, stackName: string) => cdk.Stack; } /** @@ -85,7 +94,9 @@ export class App extends core.App { mkdirSync(cdkOutdir, { recursive: true }); const cdkApp = new cdk.App({ outdir: cdkOutdir }); - const cdkStack = new cdk.Stack(cdkApp, stackName); + + const createStack = props.stackFactory ?? ((app, stackName) => new cdk.Stack(app, stackName)); + const cdkStack = createStack(cdkApp, stackName); super(cdkStack, props.rootId ?? "Default", props); diff --git a/libs/awscdk/src/bucket.ts b/libs/awscdk/src/bucket.ts index a99f0f4fc2c..d6d20585c98 100644 --- a/libs/awscdk/src/bucket.ts +++ b/libs/awscdk/src/bucket.ts @@ -10,11 +10,11 @@ import { BucketDeployment, Source } from "aws-cdk-lib/aws-s3-deployment"; import { LambdaDestination } from "aws-cdk-lib/aws-s3-notifications"; import { Construct } from "constructs"; import { App } from "./app"; -import { Function } from "./function"; import { cloud, core, std } from "@winglang/sdk"; import { convertBetweenHandlers } from "@winglang/sdk/lib/shared/convert"; import { calculateBucketPermissions } from "@winglang/sdk/lib/shared-aws/permissions"; import { IAwsBucket } from "@winglang/sdk/lib/shared-aws/bucket"; +import { IAwsCdkFunction, addPolicyStatements, isAwsCdkFunction } from "./function"; const EVENTS = { [cloud.BucketEventType.DELETE]: EventType.OBJECT_REMOVED, @@ -59,24 +59,22 @@ export class Bucket extends cloud.Bucket implements IAwsBucket { event: string, inflight: cloud.IBucketEventHandler, opts?: cloud.BucketOnCreateOptions - ): Function { + ): IAwsCdkFunction { const functionHandler = convertBetweenHandlers( inflight, this.eventHandlerLocation(), `BucketEventHandlerClient` ); - const fn = new Function( + const fn = new cloud.Function( this.node.scope!, // ok since we're not a tree root App.of(this).makeId(this, `${this.node.id}-${event}`), functionHandler, opts ); - if (!(fn instanceof Function)) { - throw new Error( - "Bucket only supports creating awscdk.Function right now" - ); + if (!isAwsCdkFunction(fn)) { + throw new Error("Expected function to implement IAwsCdkFunction"); } return fn; @@ -117,7 +115,7 @@ export class Bucket extends cloud.Bucket implements IAwsBucket { this.bucket.addEventNotification( EVENTS[cloud.BucketEventType.CREATE], - new LambdaDestination(fn._function) + new LambdaDestination(fn.awscdkFunction) ); } @@ -135,7 +133,7 @@ export class Bucket extends cloud.Bucket implements IAwsBucket { this.bucket.addEventNotification( EVENTS[cloud.BucketEventType.DELETE], - new LambdaDestination(fn._function) + new LambdaDestination(fn.awscdkFunction) ); } @@ -153,7 +151,7 @@ export class Bucket extends cloud.Bucket implements IAwsBucket { this.bucket.addEventNotification( EVENTS[cloud.BucketEventType.UPDATE], - new LambdaDestination(fn._function) + new LambdaDestination(fn.awscdkFunction) ); } @@ -170,7 +168,7 @@ export class Bucket extends cloud.Bucket implements IAwsBucket { }); this.bucket.addEventNotification( EVENTS[cloud.BucketEventType.CREATE], - new LambdaDestination(fn._function) + new LambdaDestination(fn.awscdkFunction) ); std.Node.of(this).addConnection({ @@ -180,7 +178,7 @@ export class Bucket extends cloud.Bucket implements IAwsBucket { }); this.bucket.addEventNotification( EVENTS[cloud.BucketEventType.DELETE], - new LambdaDestination(fn._function) + new LambdaDestination(fn.awscdkFunction) ); std.Node.of(this).addConnection({ @@ -190,18 +188,16 @@ export class Bucket extends cloud.Bucket implements IAwsBucket { }); this.bucket.addEventNotification( EVENTS[cloud.BucketEventType.UPDATE], - new LambdaDestination(fn._function) + new LambdaDestination(fn.awscdkFunction) ); } public onLift(host: std.IInflightHost, ops: string[]): void { - if (!(host instanceof Function)) { - throw new Error("buckets can only be bound by tfaws.Function for now"); + if (!isAwsCdkFunction(host)) { + throw new Error("Expected 'host' to implement IAwsCdkFunction"); } - host.addPolicyStatements( - ...calculateBucketPermissions(this.bucket.bucketArn, ops) - ); + addPolicyStatements(host.awscdkFunction, calculateBucketPermissions(this.bucket.bucketArn, ops)); // The bucket name needs to be passed through an environment variable since // it may not be resolved until deployment time. diff --git a/libs/awscdk/src/counter.ts b/libs/awscdk/src/counter.ts index 2f441d7f6be..e474c28bfb5 100644 --- a/libs/awscdk/src/counter.ts +++ b/libs/awscdk/src/counter.ts @@ -1,11 +1,11 @@ import { RemovalPolicy } from "aws-cdk-lib"; import { AttributeType, BillingMode, Table } from "aws-cdk-lib/aws-dynamodb"; import { Construct } from "constructs"; -import { Function } from "./function"; import { cloud, core, std } from "@winglang/sdk"; import { COUNTER_HASH_KEY } from "@winglang/sdk/lib/shared-aws/commons"; import { calculateCounterPermissions } from "@winglang/sdk/lib/shared-aws/permissions"; import { IAwsCounter } from "@winglang/sdk/lib/shared-aws/counter"; +import { addPolicyStatements, isAwsCdkFunction } from "./function"; /** * AWS implementation of `cloud.Counter`. @@ -36,13 +36,11 @@ export class Counter extends cloud.Counter implements IAwsCounter { } public onLift(host: std.IInflightHost, ops: string[]): void { - if (!(host instanceof Function)) { - throw new Error("counters can only be bound by awscdk.Function for now"); + if (!isAwsCdkFunction(host)) { + throw new Error("Expected 'host' to implement 'isAwsCdkFunction' method"); } - host.addPolicyStatements( - ...calculateCounterPermissions(this.table.tableArn, ops) - ); + addPolicyStatements(host.awscdkFunction, calculateCounterPermissions(this.table.tableArn, ops)); host.addEnvironment(this.envName(), this.table.tableName); diff --git a/libs/awscdk/src/dynamodb-table.ts b/libs/awscdk/src/dynamodb-table.ts index b9b110a6f79..2ca0ddb3580 100644 --- a/libs/awscdk/src/dynamodb-table.ts +++ b/libs/awscdk/src/dynamodb-table.ts @@ -1,7 +1,7 @@ import { RemovalPolicy } from "aws-cdk-lib"; import { AttributeType, BillingMode, Table } from "aws-cdk-lib/aws-dynamodb"; import { Construct } from "constructs"; -import { Function } from "./function"; +import { addPolicyStatements, isAwsCdkFunction } from "./function"; import { core, ex, std } from "@winglang/sdk"; import { ResourceNames } from "@winglang/sdk/lib/shared/resource-names"; import { IAwsDynamodbTable, NAME_OPTS } from "@winglang/sdk/lib/shared-aws/dynamodb-table"; @@ -41,15 +41,11 @@ export class DynamodbTable extends ex.DynamodbTable implements IAwsDynamodbTable } public onLift(host: std.IInflightHost, ops: string[]): void { - if (!(host instanceof Function)) { - throw new Error( - "Dynamodb tables can only be bound by tfaws.Function for now" - ); + if (!isAwsCdkFunction(host)) { + throw new Error("Expected 'host' to implement 'isAwsCdkFunction' method"); } - host.addPolicyStatements( - ...calculateDynamodbTablePermissions(this.table.tableArn, ops) - ); + addPolicyStatements(host.awscdkFunction, calculateDynamodbTablePermissions(this.table.tableArn, ops)); host.addEnvironment(this.envName(), this.table.tableName); diff --git a/libs/awscdk/src/endpoint.ts b/libs/awscdk/src/endpoint.ts index 2edbb4ae1cd..63924a1d03f 100644 --- a/libs/awscdk/src/endpoint.ts +++ b/libs/awscdk/src/endpoint.ts @@ -16,12 +16,7 @@ export class Endpoint extends cloud.Endpoint { /** @internal */ public onLift(host: std.IInflightHost, ops: string[]): void { - if (!(host instanceof Function)) { - throw new Error("endpoints can only be bound by awscdk.Function for now"); - } - host.addEnvironment(this.urlEnvName(), this.url); - super.onLift(host, ops); } diff --git a/libs/awscdk/src/function.ts b/libs/awscdk/src/function.ts index aba1d6a2e61..3526bbcfc0b 100644 --- a/libs/awscdk/src/function.ts +++ b/libs/awscdk/src/function.ts @@ -4,14 +4,13 @@ import { Architecture, Function as CdkFunction, Code, - IEventSource, Runtime, } from "aws-cdk-lib/aws-lambda"; import { LogGroup, RetentionDays } from "aws-cdk-lib/aws-logs"; import { Asset } from "aws-cdk-lib/aws-s3-assets"; -import { Construct } from "constructs"; +import { Construct, IConstruct } from "constructs"; import { cloud, std, core } from "@winglang/sdk"; import { createBundle } from "@winglang/sdk/lib/shared/bundling"; import { IAwsFunction, PolicyStatement } from "@winglang/sdk/lib/shared-aws"; @@ -19,12 +18,32 @@ import { resolve } from "path"; import { renameSync, rmSync, writeFileSync } from "fs"; import { App } from "./app"; +/** + * Implementation of `awscdk.Function` are expected to implement this + */ +export interface IAwsCdkFunction extends IConstruct { + awscdkFunction: CdkFunction; +} + +export function isAwsCdkFunction(x: any): x is IAwsCdkFunction { + return typeof(x['awscdkFunction']) === "object"; +} + +/** + * Adds a bunch of policy statements to the function's role. + */ +export function addPolicyStatements(fn: CdkFunction, statements: PolicyStatement[]) { + for (const statement of statements) { + fn.addToRolePolicy(new CdkPolicyStatement(statement)); + } +} + /** * AWS implementation of `cloud.Function`. * * @inflight `@winglang/sdk.cloud.IFunctionClient` */ -export class Function extends cloud.Function implements IAwsFunction { +export class Function extends cloud.Function implements IAwsCdkFunction, IAwsFunction { private readonly function: CdkFunction; private readonly assetPath: string; @@ -44,34 +63,9 @@ export class Function extends cloud.Function implements IAwsFunction { writeFileSync(this.entrypoint, inflightCodeApproximation); const bundle = createBundle(this.entrypoint); - const logRetentionDays = - props.logRetentionDays === undefined - ? 30 - : props.logRetentionDays < 0 - ? RetentionDays.INFINITE // Negative value means Infinite retention - : props.logRetentionDays; - const code = Code.fromAsset(resolve(bundle.directory)); - const logs = new LogGroup(this, "LogGroup", { - retention: logRetentionDays - }); - - this.function = new CdkFunction(this, "Default", { - handler: "index.handler", - code, - runtime: Runtime.NODEJS_20_X, - environment: { - NODE_OPTIONS: "--enable-source-maps", - ...this.env - }, - timeout: props.timeout - ? Duration.seconds(props.timeout.seconds) - : Duration.minutes(1), - memorySize: props.memory ?? 1024, - architecture: Architecture.ARM_64, - logGroup: logs - }); + this.function = this.createFunction(code, props); // hack: accessing private field from aws_lambda.AssetCode // https://github.com/aws/aws-cdk/blob/109b2abe4c713624e731afa1b82c3c1a3ba064c9/packages/aws-cdk-lib/aws-lambda/lib/code.ts#L266 @@ -105,15 +99,15 @@ export class Function extends cloud.Function implements IAwsFunction { } public onLift(host: std.IInflightHost, ops: string[]): void { - if (!(host instanceof Function)) { - throw new Error("functions can only be bound by awscdk.Function for now"); + if (!isAwsCdkFunction(host)) { + throw new Error("Expected host to implement IAwsCdkFunction"); } if (ops.includes(cloud.FunctionInflightMethods.INVOKE)) { - host.addPolicyStatements({ + host.awscdkFunction.addToRolePolicy(new CdkPolicyStatement({ actions: ["lambda:InvokeFunction"], resources: [`${this.function.functionArn}`], - }); + })); } // The function name needs to be passed through an environment variable since @@ -133,6 +127,41 @@ export class Function extends cloud.Function implements IAwsFunction { ); } + /** + * Can be overridden by subclasses to customize the AWS CDK function creation. + * @param code The AWS Lambda `Code` object that represents the inflight closure defined for this function. + * @param props Cloud function properties. + * @returns an object that implements `aws-lambda.IFunction`. + */ + protected createFunction(code: Code, props: cloud.FunctionProps): CdkFunction { + const logRetentionDays = + props.logRetentionDays === undefined + ? 30 + : props.logRetentionDays < 0 + ? RetentionDays.INFINITE // Negative value means Infinite retention + : props.logRetentionDays; + + const logs = new LogGroup(this, "LogGroup", { + retention: logRetentionDays + }); + + return new CdkFunction(this, "Default", { + handler: "index.handler", + code, + runtime: Runtime.NODEJS_20_X, + environment: { + NODE_OPTIONS: "--enable-source-maps", + ...this.env + }, + timeout: props.timeout + ? Duration.seconds(props.timeout.seconds) + : Duration.minutes(1), + memorySize: props.memory ?? 1024, + architecture: Architecture.ARM_64, + logGroup: logs + }); + } + /** * Add environment variable to the function. */ @@ -154,17 +183,11 @@ export class Function extends cloud.Function implements IAwsFunction { } } - /** @internal */ - public _addEventSource(eventSource: IEventSource) { - this.function.addEventSource(eventSource); - } - private envName(): string { return `FUNCTION_NAME_${this.node.addr.slice(-8)}`; } - /** @internal */ - get _function() { + public get awscdkFunction() { return this.function; } diff --git a/libs/awscdk/src/on-deploy.ts b/libs/awscdk/src/on-deploy.ts index 19ab1101f47..22b40f14ae1 100644 --- a/libs/awscdk/src/on-deploy.ts +++ b/libs/awscdk/src/on-deploy.ts @@ -1,7 +1,7 @@ import { Trigger } from "aws-cdk-lib/triggers"; import { Construct } from "constructs"; -import { Function as AwsFunction } from "./function"; import { cloud, core } from "@winglang/sdk"; +import { isAwsCdkFunction } from "./function"; /** * AWS implementation of `cloud.OnDeploy`. @@ -18,10 +18,13 @@ export class OnDeploy extends cloud.OnDeploy { super(scope, id, handler, props); let fn = new cloud.Function(this, "Function", handler as cloud.IFunctionHandler, props); - const awsFn = fn as AwsFunction; + + if (!isAwsCdkFunction(fn)) { + throw new Error("Expected function to implement 'IAwsCdkFunction' method"); + } let trigger = new Trigger(this, "Trigger", { - handler: awsFn._function, + handler: fn.awscdkFunction, }); trigger.executeAfter(...(props.executeAfter ?? [])); diff --git a/libs/awscdk/src/queue.ts b/libs/awscdk/src/queue.ts index 4a9d4092326..248fe7b3ca2 100644 --- a/libs/awscdk/src/queue.ts +++ b/libs/awscdk/src/queue.ts @@ -3,12 +3,12 @@ import { Duration } from "aws-cdk-lib"; import { SqsEventSource } from "aws-cdk-lib/aws-lambda-event-sources"; import { Queue as SQSQueue } from "aws-cdk-lib/aws-sqs"; import { Construct } from "constructs"; -import { Function } from "./function"; import { App } from "./app"; import { std, core, cloud } from "@winglang/sdk"; import { convertBetweenHandlers } from "@winglang/sdk/lib/shared/convert"; import { calculateQueuePermissions } from "@winglang/sdk/lib/shared-aws/permissions"; import { IAwsQueue } from "@winglang/sdk/lib/shared-aws/queue"; +import { addPolicyStatements, isAwsCdkFunction } from "./function"; /** * AWS implementation of `cloud.Queue`. @@ -46,7 +46,7 @@ export class Queue extends cloud.Queue implements IAwsQueue { "QueueSetConsumerHandlerClient" ); - const fn = new Function( + const fn = new cloud.Function( // ok since we're not a tree root this.node.scope!, App.of(this).makeId(this, `${this.node.id}-SetConsumer`), @@ -57,15 +57,15 @@ export class Queue extends cloud.Queue implements IAwsQueue { } ); - // TODO: remove this constraint by adding generic permission APIs to cloud.Function - if (!(fn instanceof Function)) { - throw new Error("Queue only supports creating awscdk.Function right now"); + if (!isAwsCdkFunction(fn)) { + throw new Error("Queue only supports creating IAwsCdkFunction right now"); } const eventSource = new SqsEventSource(this.queue, { batchSize: props.batchSize ?? 1, }); - fn._addEventSource(eventSource); + + fn.awscdkFunction.addEventSource(eventSource); std.Node.of(this).addConnection({ source: this, @@ -87,15 +87,13 @@ export class Queue extends cloud.Queue implements IAwsQueue { } public onLift(host: std.IInflightHost, ops: string[]): void { - if (!(host instanceof Function)) { - throw new Error("queues can only be bound by tfaws.Function for now"); + if (!isAwsCdkFunction(host)) { + throw new Error("Expected 'host' to implement IAwsCdkFunction"); } const env = this.envName(); - host.addPolicyStatements( - ...calculateQueuePermissions(this.queue.queueArn, ops) - ); + addPolicyStatements(host.awscdkFunction, calculateQueuePermissions(this.queue.queueArn, ops)); // The queue url needs to be passed through an environment variable since // it may not be resolved until deployment time. diff --git a/libs/awscdk/src/schedule.ts b/libs/awscdk/src/schedule.ts index a992ed84ee2..6bdb8471ccb 100644 --- a/libs/awscdk/src/schedule.ts +++ b/libs/awscdk/src/schedule.ts @@ -6,10 +6,10 @@ import { addLambdaPermission, } from "aws-cdk-lib/aws-events-targets"; import { Construct } from "constructs"; -import { Function } from "./function"; import { App } from "./app"; import { cloud, core, std } from "@winglang/sdk"; import { convertBetweenHandlers } from "@winglang/sdk/lib/shared/convert"; +import { isAwsCdkFunction } from "./function"; /** * AWS implementation of `cloud.Schedule`. @@ -73,7 +73,7 @@ export class Schedule extends cloud.Schedule { "ScheduleOnTickHandlerClient" ); - const fn = new Function( + const fn = new cloud.Function( // ok since we're not a tree root this.node.scope!, App.of(this).makeId(this, `${this.node.id}-OnTick`), @@ -81,15 +81,12 @@ export class Schedule extends cloud.Schedule { props ); - // TODO: remove this constraint by adding generic permission APIs to cloud.Function - if (!(fn instanceof Function)) { - throw new Error( - "Schedule only supports creating awscdk.Function right now" - ); + if (!isAwsCdkFunction(fn)) { + throw new Error("Expected function to implement 'isAwsCdkFunction' method"); } - this.rule.addTarget(new LambdaFunction(fn._function)); - addLambdaPermission(this.rule, fn._function); + this.rule.addTarget(new LambdaFunction(fn.awscdkFunction)); + addLambdaPermission(this.rule, fn.awscdkFunction); std.Node.of(this).addConnection({ source: this, diff --git a/libs/awscdk/src/secret.ts b/libs/awscdk/src/secret.ts index 027e01f462a..5e660fffa0f 100644 --- a/libs/awscdk/src/secret.ts +++ b/libs/awscdk/src/secret.ts @@ -4,7 +4,7 @@ import { Secret as CdkSecret, } from "aws-cdk-lib/aws-secretsmanager"; import { Construct } from "constructs"; -import { Function } from "./function"; +import { addPolicyStatements, isAwsCdkFunction } from "./function"; import { cloud, core, std } from "@winglang/sdk"; import { calculateSecretPermissions } from "@winglang/sdk/lib/shared-aws/permissions"; @@ -48,13 +48,11 @@ export class Secret extends cloud.Secret { } public onLift(host: std.IInflightHost, ops: string[]): void { - if (!(host instanceof Function)) { - throw new Error("secrets can only be bound by awscdk.Function for now"); + if (!isAwsCdkFunction(host)) { + throw new Error("Expected 'host' to implement 'isAwsCdkFunction' method"); } - host.addPolicyStatements( - ...calculateSecretPermissions(this.arnForPolicies, ops) - ); + addPolicyStatements(host.awscdkFunction, calculateSecretPermissions(this.arnForPolicies, ops)); host.addEnvironment(this.envName(), this.secret.secretArn); diff --git a/libs/awscdk/src/test-runner.ts b/libs/awscdk/src/test-runner.ts index 30b6c9c0d5a..23915daea7c 100644 --- a/libs/awscdk/src/test-runner.ts +++ b/libs/awscdk/src/test-runner.ts @@ -1,7 +1,7 @@ import { CfnOutput, Lazy } from "aws-cdk-lib"; import { Construct } from "constructs"; -import { Function as AwsFunction } from "./function"; import { core, std } from "@winglang/sdk"; +import { isAwsCdkFunction } from "./function"; const OUTPUT_TEST_RUNNER_FUNCTION_ARNS = "WingTestRunnerFunctionArns"; @@ -28,10 +28,6 @@ export class TestRunner extends std.TestRunner { } public onLift(host: std.IInflightHost, ops: string[]): void { - if (!(host instanceof AwsFunction)) { - throw new Error("TestRunner can only be bound by tfaws.Function for now"); - } - // Collect all of the test functions and their ARNs, and pass them to the // test engine so they can be invoked inflight. // TODO: are we going to run into AWS's 4KB environment variable limit here? @@ -66,12 +62,12 @@ export class TestRunner extends std.TestRunner { const arns = new Map(); for (const test of this.findTests()) { if (test._fn) { - if (!(test._fn instanceof AwsFunction)) { + if (!(isAwsCdkFunction(test._fn))) { throw new Error( `Unsupported test function type, ${test._fn.node.path} was not a tfaws.Function` ); } - arns.set(test.node.path, (test._fn as AwsFunction).functionArn); + arns.set(test.node.path, test._fn.awscdkFunction.functionArn); } } return arns; diff --git a/libs/awscdk/src/tokens.ts b/libs/awscdk/src/tokens.ts index 34247080433..6184860ad0a 100644 --- a/libs/awscdk/src/tokens.ts +++ b/libs/awscdk/src/tokens.ts @@ -1,7 +1,7 @@ import { Fn, Token } from "aws-cdk-lib"; -import { Function } from "@winglang/sdk/lib/cloud"; import { tokenEnvName, ITokenResolver } from "@winglang/sdk/lib/core/tokens"; import { IInflightHost } from "@winglang/sdk/lib/std"; +import { isAwsCdkFunction } from "./function"; /** * Represents values that can only be resolved after the app is synthesized. @@ -41,8 +41,8 @@ export class CdkTokens implements ITokenResolver { * Binds the given token to the host. */ public onLiftValue(host: IInflightHost, value: any) { - if (!(host instanceof Function)) { - throw new Error(`Tokens can only be bound by a Function for now`); + if (!isAwsCdkFunction(host)) { + throw new Error("Expected 'host' to implement 'isAwsCdkFunction' method"); } let envValue; @@ -65,9 +65,8 @@ export class CdkTokens implements ITokenResolver { } const envName = tokenEnvName(value.toString()); + // the same token might be bound multiple times by different variables/inflight contexts - if (host.env[envName] === undefined) { - host.addEnvironment(envName, envValue); - } + host.addEnvironment(envName, envValue); } } diff --git a/libs/awscdk/src/topic.ts b/libs/awscdk/src/topic.ts index 77d594cb808..93a5b6758b0 100644 --- a/libs/awscdk/src/topic.ts +++ b/libs/awscdk/src/topic.ts @@ -2,12 +2,12 @@ import { join } from "path"; import { Topic as SNSTopic } from "aws-cdk-lib/aws-sns"; import { LambdaSubscription } from "aws-cdk-lib/aws-sns-subscriptions"; import { Construct } from "constructs"; -import { Function } from "./function"; import { App } from "./app"; import { cloud, core, std } from "@winglang/sdk"; import { convertBetweenHandlers } from "@winglang/sdk/lib/shared/convert"; import { calculateTopicPermissions } from "@winglang/sdk/lib/shared-aws/permissions"; import { IAwsTopic } from "@winglang/sdk/lib/shared-aws/topic"; +import { addPolicyStatements, isAwsCdkFunction } from "./function"; /** * AWS Implementation of `cloud.Topic`. @@ -35,19 +35,18 @@ export class Topic extends cloud.Topic implements IAwsTopic { "TopicOnMessageHandlerClient" ); - const fn = new Function( + const fn = new cloud.Function( this.node.scope!, // ok since we're not a tree root App.of(this).makeId(this, `${this.node.id}-OnMessage`), functionHandler, props ); - // TODO: remove this constraint by adding geric permission APIs to cloud.Function - if (!(fn instanceof Function)) { - throw new Error("Topic only supports creating awscdk.Function right now"); + if (!isAwsCdkFunction(fn)) { + throw new Error("Expected function to implement 'IAwsCdkFunction' method"); } - const subscription = new LambdaSubscription(fn._function); + const subscription = new LambdaSubscription(fn.awscdkFunction); this.topic.addSubscription(subscription); std.Node.of(this).addConnection({ @@ -60,13 +59,11 @@ export class Topic extends cloud.Topic implements IAwsTopic { } public onLift(host: std.IInflightHost, ops: string[]): void { - if (!(host instanceof Function)) { - throw new Error("topics can only be bound by awscdk.Function for now"); + if (!isAwsCdkFunction(host)) { + throw new Error("Expected 'host' to implement 'IAwsCdkFunction' method"); } - host.addPolicyStatements( - ...calculateTopicPermissions(this.topic.topicArn, ops) - ); + addPolicyStatements(host.awscdkFunction, calculateTopicPermissions(this.topic.topicArn, ops)); host.addEnvironment(this.envName(), this.topic.topicArn); diff --git a/libs/awscdk/test/app.test.ts b/libs/awscdk/test/app.test.ts new file mode 100644 index 00000000000..408ca6a415b --- /dev/null +++ b/libs/awscdk/test/app.test.ts @@ -0,0 +1,48 @@ +import { test, expect } from "vitest"; +import * as awscdk from "../src"; +import { CDK_APP_OPTS } from "./util"; +import { Duration, Stack } from "aws-cdk-lib"; +import { mkdtemp } from "@winglang/sdk/test/util"; +import { cloud, simulator } from "@winglang/sdk"; +import { Code, Function, Runtime } from "aws-cdk-lib/aws-lambda"; + +test("custom stack", async () => { + const app = new awscdk.App({ + ...CDK_APP_OPTS, + outdir: mkdtemp(), + stackFactory: (app, stackName) => { + return new Stack(app, stackName, { + description: "This is a custom stack description" + }); + } + }); + + const out = JSON.parse(app.synth()); + expect(out.Description, "This is a custom stack description"); +}); + +test("custom Functions", async () => { + const app = new awscdk.App({ + ...CDK_APP_OPTS, + outdir: mkdtemp(), + }); + + class CustomFunction extends awscdk.Function { + protected createFunction(code: Code, props: cloud.FunctionProps): Function { + return new Function(this, "Function", { + code, + handler: "index.handler", + runtime: Runtime.NODEJS_LATEST, + environment: { + BOOM: "BAR" + } + }); + } + } + + new CustomFunction(app, "MyFunction", simulator.Testing.makeHandler("async handle(name) { console.log('hello'); }")); + + const cfn = JSON.parse(app.synth()); + + expect(cfn.Resources.MyFunctionDBE6350A.Properties.Environment.Variables).toStrictEqual({ BOOM: 'BAR' }); +}); \ No newline at end of file diff --git a/libs/awscdk/test/bucket.test.ts b/libs/awscdk/test/bucket.test.ts index 208ca4949bd..4c212a43739 100644 --- a/libs/awscdk/test/bucket.test.ts +++ b/libs/awscdk/test/bucket.test.ts @@ -3,12 +3,7 @@ import { test, expect } from "vitest"; import { cloud, simulator } from "@winglang/sdk"; import * as awscdk from "../src"; import { mkdtemp } from "@winglang/sdk/test/util"; -import { awscdkSanitize } from "./util"; - -const CDK_APP_OPTS = { - stackName: "my-project", - entrypointDir: __dirname, -}; +import { awscdkSanitize, CDK_APP_OPTS } from "./util"; test("create a bucket", async () => { // GIVEN diff --git a/libs/awscdk/test/counter.test.ts b/libs/awscdk/test/counter.test.ts index 613b3494f63..5611c43c04e 100644 --- a/libs/awscdk/test/counter.test.ts +++ b/libs/awscdk/test/counter.test.ts @@ -3,12 +3,7 @@ import { test, expect } from "vitest"; import { cloud, simulator } from "@winglang/sdk"; import * as awscdk from "../src"; import { mkdtemp } from "@winglang/sdk/test/util"; -import { sanitizeCode, awscdkSanitize } from "./util"; - -const CDK_APP_OPTS = { - stackName: "my-project", - entrypointDir: __dirname, -}; +import { sanitizeCode, awscdkSanitize, CDK_APP_OPTS } from "./util"; test("default counter behavior", () => { const app = new awscdk.App({ outdir: mkdtemp(), ...CDK_APP_OPTS }); diff --git a/libs/awscdk/test/dynamodb-table.test.ts b/libs/awscdk/test/dynamodb-table.test.ts index 38d73b10825..fb296f32441 100644 --- a/libs/awscdk/test/dynamodb-table.test.ts +++ b/libs/awscdk/test/dynamodb-table.test.ts @@ -3,12 +3,7 @@ import { test, expect } from "vitest"; import { cloud, simulator, ex } from "@winglang/sdk"; import * as awscdk from "../src"; import { mkdtemp } from "@winglang/sdk/test/util"; -import { awscdkSanitize } from "./util"; - -const CDK_APP_OPTS = { - stackName: "my-project", - entrypointDir: __dirname, -}; +import { awscdkSanitize, CDK_APP_OPTS } from "./util"; test("default dynamodb table behavior", () => { // GIVEN diff --git a/libs/awscdk/test/function.test.ts b/libs/awscdk/test/function.test.ts index ec6dcb20620..c93e7cdce46 100644 --- a/libs/awscdk/test/function.test.ts +++ b/libs/awscdk/test/function.test.ts @@ -3,12 +3,7 @@ import { test, expect } from "vitest"; import { cloud, simulator, std } from "@winglang/sdk"; import * as awscdk from "../src"; import { mkdtemp } from "@winglang/sdk/test/util"; -import { awscdkSanitize } from "./util"; - -const CDK_APP_OPTS = { - stackName: "my-project", - entrypointDir: __dirname, -}; +import { awscdkSanitize, CDK_APP_OPTS } from "./util"; const INFLIGHT_CODE = `async handle(name) { console.log("Hello, " + name); }`; diff --git a/libs/awscdk/test/on-deploy.test.ts b/libs/awscdk/test/on-deploy.test.ts index 48d94936c3f..3c2f95b22f2 100644 --- a/libs/awscdk/test/on-deploy.test.ts +++ b/libs/awscdk/test/on-deploy.test.ts @@ -3,11 +3,7 @@ import { expect, test } from "vitest"; import { cloud, simulator } from "@winglang/sdk"; import * as awscdk from "../src"; import { mkdtemp } from "@winglang/sdk/test/util"; -import { awscdkSanitize } from "./util"; - -const CDK_APP_OPTS = { - stackName: "my-project", -}; +import { awscdkSanitize, CDK_APP_OPTS } from "./util"; const INFLIGHT_CODE = `async handle(name) { console.log("Hello, " + name); }`; @@ -15,7 +11,6 @@ test("create an OnDeploy", () => { // GIVEN const app = new awscdk.App({ outdir: mkdtemp(), - entrypointDir: __dirname, ...CDK_APP_OPTS, }); const handler = simulator.Testing.makeHandler(INFLIGHT_CODE); @@ -32,7 +27,6 @@ test("execute OnDeploy after other resources", () => { // GIVEN const app = new awscdk.App({ outdir: mkdtemp(), - entrypointDir: __dirname, ...CDK_APP_OPTS, }); const bucket = new cloud.Bucket(app, "my_bucket"); @@ -55,7 +49,6 @@ test("execute OnDeploy before other resources", () => { // GIVEN const app = new awscdk.App({ outdir: mkdtemp(), - entrypointDir: __dirname, ...CDK_APP_OPTS, }); const bucket = new cloud.Bucket(app, "my_bucket"); diff --git a/libs/awscdk/test/queue.test.ts b/libs/awscdk/test/queue.test.ts index abca96e738f..641b43abb0e 100644 --- a/libs/awscdk/test/queue.test.ts +++ b/libs/awscdk/test/queue.test.ts @@ -3,12 +3,7 @@ import { test, expect } from "vitest"; import { std, simulator, cloud } from "@winglang/sdk"; import * as awscdk from "../src"; import { mkdtemp } from "@winglang/sdk/test/util"; -import { sanitizeCode, awscdkSanitize } from "./util"; - -const CDK_APP_OPTS = { - stackName: "my-project", - entrypointDir: __dirname, -}; +import { sanitizeCode, awscdkSanitize, CDK_APP_OPTS } from "./util"; test("default queue behavior", () => { // GIVEN diff --git a/libs/awscdk/test/schedule.test.ts b/libs/awscdk/test/schedule.test.ts index 6ec765850dd..5092f3c4f30 100644 --- a/libs/awscdk/test/schedule.test.ts +++ b/libs/awscdk/test/schedule.test.ts @@ -3,12 +3,7 @@ import { test, expect } from "vitest"; import { simulator, cloud, std } from "@winglang/sdk"; import * as awscdk from "../src"; import { mkdtemp } from "@winglang/sdk/test/util"; -import { awscdkSanitize } from "./util"; - -const CDK_APP_OPTS = { - stackName: "my-project", - entrypointDir: __dirname, -}; +import { awscdkSanitize, CDK_APP_OPTS } from "./util"; test("schedule behavior with rate", () => { // GIVEN diff --git a/libs/awscdk/test/secret.test.ts b/libs/awscdk/test/secret.test.ts index d30a34e0639..af636c5b280 100644 --- a/libs/awscdk/test/secret.test.ts +++ b/libs/awscdk/test/secret.test.ts @@ -4,11 +4,7 @@ import { test, expect } from "vitest"; import { cloud } from "@winglang/sdk"; import * as awscdk from "../src"; import { mkdtemp } from "@winglang/sdk/test/util"; - -const CDK_APP_OPTS = { - stackName: "my-project", - entrypointDir: __dirname, -}; +import { CDK_APP_OPTS} from "./util"; test("default secret behavior", () => { // GIVEN diff --git a/libs/awscdk/test/topic.test.ts b/libs/awscdk/test/topic.test.ts index 56a24d74544..50c8c288933 100644 --- a/libs/awscdk/test/topic.test.ts +++ b/libs/awscdk/test/topic.test.ts @@ -3,12 +3,7 @@ import { test, expect } from "vitest"; import { cloud, simulator } from "@winglang/sdk"; import * as awscdk from "../src"; import { mkdtemp } from "@winglang/sdk/test/util"; -import { sanitizeCode, awscdkSanitize } from "./util"; - -const CDK_APP_OPTS = { - stackName: "my-project", - entrypointDir: __dirname, -}; +import { sanitizeCode, awscdkSanitize, CDK_APP_OPTS } from "./util"; test("default topic behavior", () => { // GIVEN diff --git a/libs/awscdk/test/util.ts b/libs/awscdk/test/util.ts index ff0f03c53a2..d57deadd97c 100644 --- a/libs/awscdk/test/util.ts +++ b/libs/awscdk/test/util.ts @@ -31,4 +31,9 @@ export function awscdkSanitize(template: Template): any { ); return JSON.parse(jsonString); -} \ No newline at end of file +} + +export const CDK_APP_OPTS = { + stackName: "my-project", + entrypointDir: __dirname, +}; From dc29f192a9723b7c87a0e72a026706e672923d4c Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Mon, 11 Mar 2024 21:21:16 +0200 Subject: [PATCH 2/2] feat(sdk): expose `endpoint` for `sim.DynamodbTable` (#5865) Allow obtaining the endpoint used by the local DynamoDB service. This is needed in order to be able to create clients. ## Checklist - [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted) - [x] Description explains motivation and solution - [x] Tests added (always) - [x] Docs updated (only required for features) - [x] Added `pr/e2e-full` label if this feature requires end-to-end testing *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*. --- .../src/target-sim/dynamodb-table.inflight.ts | 17 ++++++++++++++++- .../test/target-sim/dynamodb-table.test.ts | 4 ++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/libs/wingsdk/src/target-sim/dynamodb-table.inflight.ts b/libs/wingsdk/src/target-sim/dynamodb-table.inflight.ts index c11f70834b5..c28e40653a3 100644 --- a/libs/wingsdk/src/target-sim/dynamodb-table.inflight.ts +++ b/libs/wingsdk/src/target-sim/dynamodb-table.inflight.ts @@ -30,6 +30,7 @@ export class DynamodbTable process.env.WING_DYNAMODB_IMAGE ?? "amazon/dynamodb-local:2.0.0"; private readonly context: ISimulatorContext; private client?: DynamoDBClient; + private _endpoint?: string; public constructor( private props: DynamodbTableSchema["props"], @@ -51,6 +52,8 @@ export class DynamodbTable containerPort: "8000", }); + this._endpoint = `http://0.0.0.0:${hostPort}`; + // dynamodb url based on host port this.client = new DynamoDBClient({ region: "local", @@ -58,7 +61,7 @@ export class DynamodbTable accessKeyId: "x", secretAccessKey: "y", }, - endpoint: `http://0.0.0.0:${hostPort}`, + endpoint: this._endpoint, }); await this.createTable(); @@ -75,10 +78,22 @@ export class DynamodbTable this.client?.destroy(); // stop the dynamodb container await runCommand("docker", ["rm", "-f", this.containerName]); + this._endpoint = undefined; } public async save(): Promise {} + /** + * Returns the local endpoint of the DynamoDB table. + */ + public async endpoint(): Promise { + if (!this._endpoint) { + throw new Error("DynamoDB hasn't been started"); + } + + return this._endpoint; + } + public async _rawClient(): Promise { if (this.client) { return this.client; diff --git a/libs/wingsdk/test/target-sim/dynamodb-table.test.ts b/libs/wingsdk/test/target-sim/dynamodb-table.test.ts index 1d8f9095865..6060462b52d 100644 --- a/libs/wingsdk/test/target-sim/dynamodb-table.test.ts +++ b/libs/wingsdk/test/target-sim/dynamodb-table.test.ts @@ -13,6 +13,10 @@ test("create a table", async () => { }); const s = await app.startSimulator(); + + const endpoint = await s.getResource("/create_table").endpoint(); + expect(endpoint.startsWith("http://0.0.0.0:")).toBeTruthy(); + expect(s.getResourceConfig("/create_table")).toEqual({ attrs: { handle: expect.any(String),