Skip to content

Commit

Permalink
fix(sdk): cannot reference api.url within cloud.Api route on AWS (#5637)
Browse files Browse the repository at this point in the history
Fixes #2740

In this bug, when the user creates an API route whose inflight code references the API's url...

```js
let api = new cloud.Api();

api.get("/my_url", inflight () => {
  return {
    status: 200,
    body: api.url
  };
});
```

... when they deploy to the `tf-aws` platform, it causes resources to be created in a dependency cycle, making it impossible to deploy the app.

The reason this happens is that first, the `cloud.Api` is turned into an API Gateway, whose OpenAPI specification (list of all of the API routes) needs to reference the physical name of the `cloud.Function` (usually called the "ARN" in AWS-speak). Second, the `cloud.Function` is turned into an AWS Lambda function, which needs an environment variable to provide the URL of the API Gateway. Both resources want to use a property of the other resource that seems to only be available once it's created.

To unravel this dependency cycle, I updated the implementation of `cloud.Api` to take leverage of the fact that the ARNs of functions are well-defined in the CDK model, and can almost entirely[1] be determined at compile time. This allows the API Gateway to be deployed before the Lambda function without any issues.

[1] The only exception is that the AWS account name and region is needed, but these values can be obtained freely at deploy time.

## 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)
- [ ] 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)*.
  • Loading branch information
Chriscbr authored Feb 7, 2024
1 parent 8c90c6f commit f1f65b4
Show file tree
Hide file tree
Showing 32 changed files with 596 additions and 66 deletions.
22 changes: 22 additions & 0 deletions examples/tests/sdk_tests/api/cycle.test.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
bring cloud;
bring http;
bring util;

// This test checks that an API can have a route whose handler
// references the API's URL.

if ["sim", "tf-aws", "awscdk"].contains(util.env("WING_TARGET")) {
let api = new cloud.Api();

api.get("/my_url", inflight () => {
return {
status: 200,
body: api.url
};
});

test "GET /my_url" {
let resp = http.get("{api.url}/my_url");
assert(resp.status == 200);
}
}
12 changes: 11 additions & 1 deletion libs/wingsdk/src/target-tf-aws/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ class WingRestApi extends Construct {
public readonly stage: ApiGatewayStage;
public readonly deployment: ApiGatewayDeployment;
private readonly region: string;
private readonly accountId: string;

constructor(
scope: Construct,
Expand All @@ -320,6 +321,7 @@ class WingRestApi extends Construct {
) {
super(scope, id);
this.region = (App.of(this) as App).region;
this.accountId = (App.of(this) as App).accountId;

const defaultResponse = API_CORS_DEFAULT_RESPONSE(props.cors);

Expand Down Expand Up @@ -386,9 +388,17 @@ class WingRestApi extends Construct {
* @returns OpenApi extension object for the endpoint and handler
*/
private createApiSpecExtension(handler: Function) {
// The ARN of the Lambda function is constructed by hand so that it can be calculated
// during preflight, instead of being resolved at deploy time.
//
// By doing this, the API Gateway does not need to take a dependency on its Lambda functions,
// making it possible to write Lambda functions that reference the
// API Gateway's URL in their inflight code.
const functionArn = `arn:aws:lambda:${this.region}:${this.accountId}:function:${handler.name}`;

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/${functionArn}/invocations`,
type: "aws_proxy",
httpMethod: "POST",
responses: {
Expand Down
11 changes: 7 additions & 4 deletions libs/wingsdk/src/target-tf-aws/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ export class Function extends cloud.Function implements IAwsFunction {
/** Permissions */
public permissions!: LambdaPermission;

/** Name of the AWS Lambda function in the account/region */
public readonly name: string;

private assetPath: string | undefined; // posix path
private bundleHash: string | undefined;

Expand Down Expand Up @@ -167,7 +170,7 @@ export class Function extends cloud.Function implements IAwsFunction {
role: this.role.name,
});

const name = ResourceNames.generateName(this, FUNCTION_NAME_OPTS);
this.name = ResourceNames.generateName(this, FUNCTION_NAME_OPTS);

// validate memory size
if (props.memory && (props.memory < 128 || props.memory > 10240)) {
Expand All @@ -178,7 +181,7 @@ export class Function extends cloud.Function implements IAwsFunction {

if (!props.logRetentionDays || props.logRetentionDays >= 0) {
new CloudwatchLogGroup(this, "CloudwatchLogGroup", {
name: `/aws/lambda/${name}`,
name: `/aws/lambda/${this.name}`,
retentionInDays: props.logRetentionDays ?? 30,
});
} else {
Expand All @@ -187,7 +190,7 @@ export class Function extends cloud.Function implements IAwsFunction {

// Create Lambda function
this.function = new LambdaFunction(this, "Default", {
functionName: name,
functionName: this.name,
s3Bucket: bucket.bucket,
s3Key: lambdaArchive.key,
handler: "index.handler",
Expand Down Expand Up @@ -229,7 +232,7 @@ export class Function extends cloud.Function implements IAwsFunction {
this.invokeArn = this.function.invokeArn;

// terraform rejects templates with zero environment variables
this.addEnvironment("WING_FUNCTION_NAME", name);
this.addEnvironment("WING_FUNCTION_NAME", this.name);
}

/** @internal */
Expand Down
48 changes: 24 additions & 24 deletions libs/wingsdk/test/target-tf-aws/__snapshots__/api.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ exports[`api configured for cors 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_get_0_244A7BA4.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:get_0-c86d29bb/invocations",
},
},
},
Expand Down Expand Up @@ -174,7 +174,7 @@ exports[`api with 'name' & 'age' parameter 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_get_name_age0_EC545125.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:get_-name_-age0-c8483afe/invocations",
},
},
},
Expand Down Expand Up @@ -261,7 +261,7 @@ exports[`api with 'name' parameter 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_get_name0_9A24C973.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:get_-name0-c8d13803/invocations",
},
},
},
Expand Down Expand Up @@ -339,7 +339,7 @@ exports[`api with CONNECT route 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_connect_0_E4C99808.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:connect_0-c83420d1/invocations",
},
},
},
Expand Down Expand Up @@ -417,7 +417,7 @@ exports[`api with DELETE route 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_delete_0_139A6C69.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:delete_0-c85ab39c/invocations",
},
},
},
Expand Down Expand Up @@ -495,7 +495,7 @@ exports[`api with GET route at root 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_get_0_244A7BA4.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:get_0-c86d29bb/invocations",
},
},
},
Expand Down Expand Up @@ -573,7 +573,7 @@ exports[`api with GET routes with common prefix 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_get_hello_bat0_00BC2BC0.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:get_hello_bat0-c8a08a1c/invocations",
},
},
},
Expand All @@ -597,7 +597,7 @@ exports[`api with GET routes with common prefix 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_get_hello_foo0_20F6E2D7.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:get_hello_foo0-c82c3421/invocations",
},
},
},
Expand Down Expand Up @@ -675,7 +675,7 @@ exports[`api with GET routes with different prefix 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_get_foo_bar0_0B963693.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:get_foo_bar0-c8d108f9/invocations",
},
},
},
Expand All @@ -699,7 +699,7 @@ exports[`api with GET routes with different prefix 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_get_hello_foo0_20F6E2D7.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:get_hello_foo0-c82c3421/invocations",
},
},
},
Expand Down Expand Up @@ -777,7 +777,7 @@ exports[`api with HEAD route 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_head_0_87BD9B1E.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:head_0-c857e3ff/invocations",
},
},
},
Expand Down Expand Up @@ -855,7 +855,7 @@ exports[`api with OPTIONS route 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_options_0_46D3D933.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:options_0-c8aab680/invocations",
},
},
},
Expand Down Expand Up @@ -933,7 +933,7 @@ exports[`api with PATCH route 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_patch_0_EE9D62D9.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:patch_0-c8eaba1e/invocations",
},
},
},
Expand Down Expand Up @@ -1011,7 +1011,7 @@ exports[`api with POST route 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_post_0_211FC41C.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:post_0-c8d25f85/invocations",
},
},
},
Expand Down Expand Up @@ -1089,7 +1089,7 @@ exports[`api with PUT route 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_put_0_30B8C61B.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:put_0-c80fcd0a/invocations",
},
},
},
Expand Down Expand Up @@ -1167,7 +1167,7 @@ exports[`api with multiple GET route and one lambda 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_get_hello_foo0_20F6E2D7.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:get_hello_foo0-c82c3421/invocations",
},
},
},
Expand All @@ -1191,7 +1191,7 @@ exports[`api with multiple GET route and one lambda 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_get_hello_foo0_20F6E2D7.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:get_hello_foo0-c82c3421/invocations",
},
},
},
Expand Down Expand Up @@ -1269,7 +1269,7 @@ exports[`api with multiple methods and multiple lambda 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_post_hello_bat0_A57A0898.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:post_hello_bat0-c8596849/invocations",
},
},
},
Expand All @@ -1293,7 +1293,7 @@ exports[`api with multiple methods and multiple lambda 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_get_hello_foo0_20F6E2D7.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:get_hello_foo0-c82c3421/invocations",
},
},
},
Expand Down Expand Up @@ -1371,7 +1371,7 @@ exports[`api with multiple methods and one lambda 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_get_hello_foo0_20F6E2D7.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:get_hello_foo0-c82c3421/invocations",
},
},
},
Expand All @@ -1395,7 +1395,7 @@ exports[`api with multiple methods and one lambda 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_get_hello_foo0_20F6E2D7.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:get_hello_foo0-c82c3421/invocations",
},
},
},
Expand Down Expand Up @@ -1473,7 +1473,7 @@ exports[`api with multiple methods on same route 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_get_0_244A7BA4.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:get_0-c86d29bb/invocations",
},
},
"put": {
Expand All @@ -1495,7 +1495,7 @@ exports[`api with multiple methods on same route 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_get_0_244A7BA4.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:get_0-c86d29bb/invocations",
},
},
},
Expand Down Expand Up @@ -1582,7 +1582,7 @@ exports[`api with path parameter 1`] = `
},
},
"type": "aws_proxy",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/\${aws_lambda_function.Api_get_hello_world0_EF7450D4.arn}/invocations",
"uri": "arn:aws:apigateway:\${data.aws_region.Region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:\${data.aws_region.Region.name}:\${data.aws_caller_identity.account.account_id}:function:get_hello_-world0-c81e38f8/invocations",
},
},
},
Expand Down
Loading

0 comments on commit f1f65b4

Please sign in to comment.