From a93ede6dfefccada439fcd11640c040186b85d49 Mon Sep 17 00:00:00 2001 From: Chris Rybicki Date: Mon, 20 May 2024 20:54:56 -0700 Subject: [PATCH] feat(sdk): aws.Function.context() for lambda functions (#6424) Closes https://github.com/winglang/wing/issues/6412 ## 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)*. --- .../04-standard-library/aws/api-reference.md | 135 ++++++++++++++++++ .../04-standard-library/cloud/function.md | 19 +++ .../sdk_tests/function/aws-function.test.w | 41 +++++- libs/awscdk/src/function.ts | 19 ++- libs/wingc/src/type_check/jsii_importer.rs | 3 - libs/wingsdk/src/cloud/function.md | 19 +++ .../src/expect/{assert.ts => expect.ts} | 0 libs/wingsdk/src/expect/index.ts | 2 +- libs/wingsdk/src/shared-aws/function-util.ts | 34 +++++ .../src/shared-aws/function.inflight.ts | 11 ++ libs/wingsdk/src/shared-aws/function.ts | 66 +++++++++ libs/wingsdk/src/target-tf-aws/function.ts | 16 +-- .../aws-function.test.w_compile_tf-aws.md | 81 +++++++++++ .../function/aws-function.test.w_test_sim.md | 3 +- 14 files changed, 426 insertions(+), 23 deletions(-) rename libs/wingsdk/src/expect/{assert.ts => expect.ts} (100%) create mode 100644 libs/wingsdk/src/shared-aws/function-util.ts diff --git a/docs/docs/04-standard-library/aws/api-reference.md b/docs/docs/04-standard-library/aws/api-reference.md index ff388b56434..a8a4c731b1d 100644 --- a/docs/docs/04-standard-library/aws/api-reference.md +++ b/docs/docs/04-standard-library/aws/api-reference.md @@ -1053,10 +1053,23 @@ new aws.Function(); | **Name** | **Description** | | --- | --- | +| context | Returns the current Lambda invocation context, if the host is an AWS Lambda. | | from | If the inflight host is an AWS Lambda, return a helper interface for working with it. | --- +##### `context` + +```wing +bring aws; + +aws.Function.context(); +``` + +Returns the current Lambda invocation context, if the host is an AWS Lambda. + +> [https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html) + ##### `from` ```wing @@ -2001,6 +2014,128 @@ AWS Bucket name. --- +### ILambdaContext + +- *Implemented By:* ILambdaContext + +The AWS Lambda context object. + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| remainingTimeInMillis | Returns the number of milliseconds left before the execution times out. | + +--- + +##### `remainingTimeInMillis` + +```wing +remainingTimeInMillis(): num +``` + +Returns the number of milliseconds left before the execution times out. + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| awsRequestId | str | The identifier of the invocation request. | +| functionName | str | The name of the Lambda function. | +| functionVersion | str | The version of the function. | +| invokedFunctionArn | str | The Amazon Resource Name (ARN) that's used to invoke the function. | +| logGroupName | str | The log group for the function. | +| logStreamName | str | The log stream for the function instance. | +| memoryLimitInMB | str | The amount of memory that's allocated for the function. | + +--- + +##### `awsRequestId`Required + +```wing +awsRequestId: str; +``` + +- *Type:* str + +The identifier of the invocation request. + +--- + +##### `functionName`Required + +```wing +functionName: str; +``` + +- *Type:* str + +The name of the Lambda function. + +--- + +##### `functionVersion`Required + +```wing +functionVersion: str; +``` + +- *Type:* str + +The version of the function. + +--- + +##### `invokedFunctionArn`Required + +```wing +invokedFunctionArn: str; +``` + +- *Type:* str + +The Amazon Resource Name (ARN) that's used to invoke the function. + +Indicates if the invoker specified a version number or alias. + +--- + +##### `logGroupName`Required + +```wing +logGroupName: str; +``` + +- *Type:* str + +The log group for the function. + +--- + +##### `logStreamName`Required + +```wing +logStreamName: str; +``` + +- *Type:* str + +The log stream for the function instance. + +--- + +##### `memoryLimitInMB`Required + +```wing +memoryLimitInMB: str; +``` + +- *Type:* str + +The amount of memory that's allocated for the function. + +--- + ## Enums ### Effect diff --git a/docs/docs/04-standard-library/cloud/function.md b/docs/docs/04-standard-library/cloud/function.md index 93fc14087e9..5165c72ae8e 100644 --- a/docs/docs/04-standard-library/cloud/function.md +++ b/docs/docs/04-standard-library/cloud/function.md @@ -127,6 +127,25 @@ if let lambdaFn = aws.Function.from(f) { } ``` +To access the AWS Lambda context object, you can use the `aws.Function` class as shown below. + +```ts playground +bring aws; +bring cloud; + +let f = new cloud.Function(inflight () => { + if let ctx = aws.Function.context() { + log(ctx.logGroupName); // prints the log group name + log(ctx.logStreamName); // prints the log stream name + + let remainingTime = ctx.remainingTimeInMillis(); + assert(remainingTime > 0); + } +}); +``` + +The `context()` method returns `nil` when ran on non-AWS targets. + ### Azure (`tf-azure`) The Azure implementation of `cloud.Function` uses [Azure Functions](https://azure.microsoft.com/en-us/products/functions). diff --git a/examples/tests/sdk_tests/function/aws-function.test.w b/examples/tests/sdk_tests/function/aws-function.test.w index 7bcd7a7e0fe..0c0414ef77a 100644 --- a/examples/tests/sdk_tests/function/aws-function.test.w +++ b/examples/tests/sdk_tests/function/aws-function.test.w @@ -1,5 +1,6 @@ -bring cloud; bring aws; +bring cloud; +bring expect; bring util; let target = util.env("WING_TARGET"); @@ -35,4 +36,40 @@ test "validates the AWS Function" { // If the test is not on AWS, it should not fail, so I am returning true. assert(true); } -} \ No newline at end of file +} + +let fn = new cloud.Function(inflight (msg: str?) => { + if msg == "error" { + throw "fake error"; + } + + if let ctx = aws.Function.context() { + log(Json.stringify(ctx)); + expect.equal(ctx.functionVersion, "$LATEST"); + + let remainingTime = ctx.remainingTimeInMillis(); + assert(remainingTime > 0); + } else { + if target == "tf-aws" || target == "awscdk" { + expect.fail("Expected to have a context object"); + } + } + + return msg; +}) as "FunctionAccessingContext"; + +test "can access lambda context" { + let result = fn.invoke("hello"); + expect.equal(result, "hello"); + + let result2 = fn.invoke("hello2"); + expect.equal(result2, "hello2"); + + let var msg = ""; + try { + fn.invoke("error"); + } catch err { + msg = err; + } + expect.ok(msg.contains("fake error"), "Expected fake error message"); +} diff --git a/libs/awscdk/src/function.ts b/libs/awscdk/src/function.ts index c96d50bbc04..14d71f3af0b 100644 --- a/libs/awscdk/src/function.ts +++ b/libs/awscdk/src/function.ts @@ -12,7 +12,13 @@ import { Construct, IConstruct } from "constructs"; import { cloud, std, core } from "@winglang/sdk"; import { NotImplementedError } from "@winglang/sdk/lib/core/errors"; import { createBundle } from "@winglang/sdk/lib/shared/bundling"; -import { IAwsFunction, NetworkConfig, PolicyStatement, externalLibraries } from "@winglang/sdk/lib/shared-aws"; +import { + IAwsFunction, + NetworkConfig, + PolicyStatement, + externalLibraries, +} from "@winglang/sdk/lib/shared-aws"; +import { makeAwsLambdaHandler } from "@winglang/sdk/lib/shared-aws/function-util"; import { resolve } from "path"; import { renameSync, rmSync, writeFileSync } from "fs"; import { App } from "./app"; @@ -201,7 +207,9 @@ export class Function public addNetwork(config: NetworkConfig): void { config; - throw new Error("The AWS CDK platform provider does not support adding network configurations to AWS Lambda functions at the moment."); + throw new Error( + "The AWS CDK platform provider does not support adding network configurations to AWS Lambda functions at the moment." + ); } private envName(): string { @@ -219,4 +227,11 @@ export class Function public get functionName(): string { return this.function.functionName; } + + /** + * @internal + */ + protected _getCodeLines(handler: cloud.IFunctionHandler): string[] { + return makeAwsLambdaHandler(handler); + } } diff --git a/libs/wingc/src/type_check/jsii_importer.rs b/libs/wingc/src/type_check/jsii_importer.rs index b709b2142c3..65c99014da2 100644 --- a/libs/wingc/src/type_check/jsii_importer.rs +++ b/libs/wingc/src/type_check/jsii_importer.rs @@ -583,9 +583,6 @@ impl<'a> JsiiImporter<'a> { if let Some(properties) = jsii_interface.properties() { for p in properties { debug!("Found property {} with type {:?}", p.name.green(), p.type_); - if member_phase == Phase::Inflight { - todo!("No support for inflight properties yet"); - } let base_wing_type = self.type_ref_to_wing_type(&p.type_); let is_optional = if let Some(true) = p.optional { true } else { false }; let is_static = if let Some(true) = p.static_ { true } else { false }; diff --git a/libs/wingsdk/src/cloud/function.md b/libs/wingsdk/src/cloud/function.md index c236b5647b8..0c292df201d 100644 --- a/libs/wingsdk/src/cloud/function.md +++ b/libs/wingsdk/src/cloud/function.md @@ -127,6 +127,25 @@ if let lambdaFn = aws.Function.from(f) { } ``` +To access the AWS Lambda context object, you can use the `aws.Function` class as shown below. + +```ts playground +bring aws; +bring cloud; + +let f = new cloud.Function(inflight () => { + if let ctx = aws.Function.context() { + log(ctx.logGroupName); // prints the log group name + log(ctx.logStreamName); // prints the log stream name + + let remainingTime = ctx.remainingTimeInMillis(); + assert(remainingTime > 0); + } +}); +``` + +The `context()` method returns `nil` when ran on non-AWS targets. + ### Azure (`tf-azure`) The Azure implementation of `cloud.Function` uses [Azure Functions](https://azure.microsoft.com/en-us/products/functions). diff --git a/libs/wingsdk/src/expect/assert.ts b/libs/wingsdk/src/expect/expect.ts similarity index 100% rename from libs/wingsdk/src/expect/assert.ts rename to libs/wingsdk/src/expect/expect.ts diff --git a/libs/wingsdk/src/expect/index.ts b/libs/wingsdk/src/expect/index.ts index 997f6591a88..ca16728fac1 100644 --- a/libs/wingsdk/src/expect/index.ts +++ b/libs/wingsdk/src/expect/index.ts @@ -1 +1 @@ -export * from "./assert"; +export * from "./expect"; diff --git a/libs/wingsdk/src/shared-aws/function-util.ts b/libs/wingsdk/src/shared-aws/function-util.ts new file mode 100644 index 00000000000..c4237e3f263 --- /dev/null +++ b/libs/wingsdk/src/shared-aws/function-util.ts @@ -0,0 +1,34 @@ +import * as cloud from "../cloud"; + +export function makeAwsLambdaHandler( + handler: cloud.IFunctionHandler +): string[] { + const inflightClient = handler._toInflight(); + const lines = new Array(); + const client = "$handler"; + + lines.push('"use strict";'); + lines.push(`var ${client} = undefined;`); + lines.push("exports.handler = async function(event, context) {"); + lines.push(" try {"); + lines.push(" if (globalThis.$awsLambdaContext === undefined) {"); + lines.push(" globalThis.$awsLambdaContext = context;"); + lines.push(` ${client} = ${client} ?? (${inflightClient});`); + lines.push(" } else {"); + lines.push(" throw new Error("); + lines.push(" 'An AWS Lambda context object was already defined.'"); + lines.push(" );"); + lines.push(" }"); + + // important: we're calling handle() within a try block, but there's no catch block + // because we want to let the error propagate to the AWS Lambda runtime + lines.push( + ` return await ${client}.handle(event === null ? undefined : event);` + ); + lines.push(" } finally {"); + lines.push(" globalThis.$awsLambdaContext = undefined;"); + lines.push(" }"); + lines.push("};"); + + return lines; +} diff --git a/libs/wingsdk/src/shared-aws/function.inflight.ts b/libs/wingsdk/src/shared-aws/function.inflight.ts index e7e20382e59..c53424b7b1e 100644 --- a/libs/wingsdk/src/shared-aws/function.inflight.ts +++ b/libs/wingsdk/src/shared-aws/function.inflight.ts @@ -5,10 +5,21 @@ import { LogType, } from "@aws-sdk/client-lambda"; import { fromUtf8, toUtf8 } from "@smithy/util-utf8"; +import { ILambdaContext } from "./function"; import { IFunctionClient } from "../cloud"; import { LogLevel, Trace, TraceType } from "../std"; export class FunctionClient implements IFunctionClient { + public static async context(): Promise { + const obj = (globalThis as any).$awsLambdaContext; + if (!obj) { + return undefined; + } + // workaround for the fact that JSII doesn't allow methods to start with "get" + obj.remainingTimeInMillis = obj.getRemainingTimeInMillis; + return obj; + } + constructor( private readonly functionArn: string, private readonly constructPath: string, diff --git a/libs/wingsdk/src/shared-aws/function.ts b/libs/wingsdk/src/shared-aws/function.ts index b96e45914ea..b603eebf6f7 100644 --- a/libs/wingsdk/src/shared-aws/function.ts +++ b/libs/wingsdk/src/shared-aws/function.ts @@ -36,6 +36,25 @@ export interface IAwsFunction extends IAwsInflightHost { * A helper class for working with AWS functions. */ export class Function { + /** @internal */ + public static _toInflightType(): string { + return InflightClient.forType( + __filename.replace("function", "function.inflight"), + "FunctionClient" + ); + } + + /** + * Returns the current Lambda invocation context, if the host is an AWS Lambda. + * @see https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html + * @inflight + * @returns The current Lambda invocation context. + */ + public static async context(): Promise { + // The implementation of this method is in function.inflight.ts + throw new Error("Not implemented"); + } + /** * If the inflight host is an AWS Lambda, return a helper interface for * working with it. @@ -56,6 +75,53 @@ export class Function { } } +/** + * The AWS Lambda context object. + * @inflight + */ +export interface ILambdaContext { + /** + * The name of the Lambda function. + */ + readonly functionName: string; + + /** + * The version of the function. + */ + readonly functionVersion: string; + + /** + * The Amazon Resource Name (ARN) that's used to invoke the function. + * Indicates if the invoker specified a version number or alias. + */ + readonly invokedFunctionArn: string; + + /** + * The amount of memory that's allocated for the function. + */ + readonly memoryLimitInMB: string; + + /** + * The identifier of the invocation request. + */ + readonly awsRequestId: string; + + /** + * The log group for the function. + */ + readonly logGroupName: string; + + /** + * The log stream for the function instance. + */ + readonly logStreamName: string; + + /** + * Returns the number of milliseconds left before the execution times out. + */ + remainingTimeInMillis(): number; +} + /** * A reference to an external Lambda function. * diff --git a/libs/wingsdk/src/target-tf-aws/function.ts b/libs/wingsdk/src/target-tf-aws/function.ts index ca380b73b13..f49ff67b557 100644 --- a/libs/wingsdk/src/target-tf-aws/function.ts +++ b/libs/wingsdk/src/target-tf-aws/function.ts @@ -23,6 +23,7 @@ import { PolicyStatement, externalLibraries, } from "../shared-aws"; +import { makeAwsLambdaHandler } from "../shared-aws/function-util"; import { IInflightHost, Resource } from "../std"; import { Duration } from "../std/duration"; @@ -407,19 +408,6 @@ export class Function extends cloud.Function implements IAwsFunction { * @internal */ protected _getCodeLines(handler: cloud.IFunctionHandler): string[] { - const inflightClient = handler._toInflight(); - const lines = new Array(); - const client = "$handler"; - - lines.push('"use strict";'); - lines.push(`var ${client} = undefined;`); - lines.push("exports.handler = async function(event) {"); - lines.push(` ${client} = ${client} ?? (${inflightClient});`); - lines.push( - ` return await ${client}.handle(event === null ? undefined : event);` - ); - lines.push("};"); - - return lines; + return makeAwsLambdaHandler(handler); } } diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/function/aws-function.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/function/aws-function.test.w_compile_tf-aws.md index 2b2c42ccd45..c35fb9840c5 100644 --- a/tools/hangar/__snapshots__/test_corpus/sdk_tests/function/aws-function.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/function/aws-function.test.w_compile_tf-aws.md @@ -18,6 +18,16 @@ }, "resource": { "aws_cloudwatch_log_group": { + "FunctionAccessingContext_CloudwatchLogGroup_A7EDE513": { + "//": { + "metadata": { + "path": "root/Default/Default/FunctionAccessingContext/CloudwatchLogGroup", + "uniqueId": "FunctionAccessingContext_CloudwatchLogGroup_A7EDE513" + } + }, + "name": "/aws/lambda/FunctionAccessingContext-c84d6117", + "retention_in_days": 30 + }, "aws-wing-function_CloudwatchLogGroup_2CCFCD44": { "//": { "metadata": { @@ -30,6 +40,15 @@ } }, "aws_iam_role": { + "FunctionAccessingContext_IamRole_6926384F": { + "//": { + "metadata": { + "path": "root/Default/Default/FunctionAccessingContext/IamRole", + "uniqueId": "FunctionAccessingContext_IamRole_6926384F" + } + }, + "assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Effect\":\"Allow\"}]}" + }, "aws-wing-function_IamRole_705FDD7E": { "//": { "metadata": { @@ -41,6 +60,16 @@ } }, "aws_iam_role_policy": { + "FunctionAccessingContext_IamRolePolicy_80298DED": { + "//": { + "metadata": { + "path": "root/Default/Default/FunctionAccessingContext/IamRolePolicy", + "uniqueId": "FunctionAccessingContext_IamRolePolicy_80298DED" + } + }, + "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"none:null\",\"Resource\":\"*\"}]}", + "role": "${aws_iam_role.FunctionAccessingContext_IamRole_6926384F.name}" + }, "aws-wing-function_IamRolePolicy_CF2194BD": { "//": { "metadata": { @@ -53,6 +82,16 @@ } }, "aws_iam_role_policy_attachment": { + "FunctionAccessingContext_IamRolePolicyAttachment_5D73CD5C": { + "//": { + "metadata": { + "path": "root/Default/Default/FunctionAccessingContext/IamRolePolicyAttachment", + "uniqueId": "FunctionAccessingContext_IamRolePolicyAttachment_5D73CD5C" + } + }, + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "${aws_iam_role.FunctionAccessingContext_IamRole_6926384F.name}" + }, "aws-wing-function_IamRolePolicyAttachment_F788B7D7": { "//": { "metadata": { @@ -65,6 +104,37 @@ } }, "aws_lambda_function": { + "FunctionAccessingContext": { + "//": { + "metadata": { + "path": "root/Default/Default/FunctionAccessingContext/Default", + "uniqueId": "FunctionAccessingContext" + } + }, + "architectures": [ + "arm64" + ], + "environment": { + "variables": { + "NODE_OPTIONS": "--enable-source-maps", + "WING_FUNCTION_NAME": "FunctionAccessingContext-c84d6117", + "WING_TARGET": "tf-aws" + } + }, + "function_name": "FunctionAccessingContext-c84d6117", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "${aws_iam_role.FunctionAccessingContext_IamRole_6926384F.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "${aws_s3_bucket.Code.bucket}", + "s3_key": "${aws_s3_object.FunctionAccessingContext_S3Object_FCA6F30A.key}", + "timeout": 60, + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [] + } + }, "aws-wing-function": { "//": { "metadata": { @@ -109,6 +179,17 @@ } }, "aws_s3_object": { + "FunctionAccessingContext_S3Object_FCA6F30A": { + "//": { + "metadata": { + "path": "root/Default/Default/FunctionAccessingContext/S3Object", + "uniqueId": "FunctionAccessingContext_S3Object_FCA6F30A" + } + }, + "bucket": "${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "" + }, "aws-wing-function_S3Object_9678073C": { "//": { "metadata": { diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/function/aws-function.test.w_test_sim.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/function/aws-function.test.w_test_sim.md index 093449a157a..0354a7858e7 100644 --- a/tools/hangar/__snapshots__/test_corpus/sdk_tests/function/aws-function.test.w_test_sim.md +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/function/aws-function.test.w_test_sim.md @@ -3,8 +3,9 @@ ## stdout.log ```log pass ─ aws-function.test.wsim » root/env0/test:validates the AWS Function +pass ─ aws-function.test.wsim » root/env1/test:can access lambda context -Tests 1 passed (1) +Tests 2 passed (2) Snapshots 1 skipped Test Files 1 passed (1) Duration