From 7eadd1809816faa34760d3c48a1ed24985041b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Thu, 7 Nov 2024 17:06:23 +0100 Subject: [PATCH 01/13] increase clarity of verbose output --- .gitignore | 1 + Plugins/AWSLambdaPackager/Plugin.swift | 2 +- Plugins/AWSLambdaPackager/PluginUtils.swift | 7 +++++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index a5baf2c5..f56422fc 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ Package.resolved .serverless .vscode Makefile +.devcontainer \ No newline at end of file diff --git a/Plugins/AWSLambdaPackager/Plugin.swift b/Plugins/AWSLambdaPackager/Plugin.swift index 3be4b1ba..a8693945 100644 --- a/Plugins/AWSLambdaPackager/Plugin.swift +++ b/Plugins/AWSLambdaPackager/Plugin.swift @@ -74,7 +74,7 @@ struct AWSLambdaPackager: CommandPlugin { "\(archives.count > 0 ? archives.count.description : "no") archive\(archives.count != 1 ? "s" : "") created" ) for (product, archivePath) in archives { - print(" * \(product.name) at \(archivePath)") + print(" * \(product.name) at \(archivePath.path())") } } diff --git a/Plugins/AWSLambdaPackager/PluginUtils.swift b/Plugins/AWSLambdaPackager/PluginUtils.swift index 52d1b2be..f4e8cb02 100644 --- a/Plugins/AWSLambdaPackager/PluginUtils.swift +++ b/Plugins/AWSLambdaPackager/PluginUtils.swift @@ -28,6 +28,9 @@ struct Utils { ) throws -> String { if logLevel >= .debug { print("\(executable.path()) \(arguments.joined(separator: " "))") + if let customWorkingDirectory { + print("Working directory: \(customWorkingDirectory.path())") + } } let fd = dup(1) @@ -85,8 +88,8 @@ struct Utils { process.standardError = pipe process.executableURL = executable process.arguments = arguments - if let workingDirectory = customWorkingDirectory { - process.currentDirectoryURL = URL(fileURLWithPath: workingDirectory.path()) + if let customWorkingDirectory { + process.currentDirectoryURL = URL(fileURLWithPath: customWorkingDirectory.path()) } process.terminationHandler = { _ in outputQueue.async { From bde3d0d2393d2d343ef0f4055fc626320132fa47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Thu, 7 Nov 2024 17:06:39 +0100 Subject: [PATCH 02/13] ad hellojson sample --- Examples/HelloJSON/.gitignore | 4 ++ Examples/HelloJSON/Package.swift | 59 ++++++++++++++++++++ Examples/HelloJSON/README.md | 78 +++++++++++++++++++++++++++ Examples/HelloJSON/Sources/main.swift | 43 +++++++++++++++ Examples/HelloJSON/out.txt | 1 + 5 files changed, 185 insertions(+) create mode 100644 Examples/HelloJSON/.gitignore create mode 100644 Examples/HelloJSON/Package.swift create mode 100644 Examples/HelloJSON/README.md create mode 100644 Examples/HelloJSON/Sources/main.swift create mode 100644 Examples/HelloJSON/out.txt diff --git a/Examples/HelloJSON/.gitignore b/Examples/HelloJSON/.gitignore new file mode 100644 index 00000000..e41d0be5 --- /dev/null +++ b/Examples/HelloJSON/.gitignore @@ -0,0 +1,4 @@ +response.json +samconfig.toml +template.yaml +Makefile diff --git a/Examples/HelloJSON/Package.swift b/Examples/HelloJSON/Package.swift new file mode 100644 index 00000000..506f0678 --- /dev/null +++ b/Examples/HelloJSON/Package.swift @@ -0,0 +1,59 @@ +// swift-tools-version:6.0 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +#if os(macOS) +let platforms: [PackageDescription.SupportedPlatform]? = [.macOS(.v15)] +#else +let platforms: [PackageDescription.SupportedPlatform]? = nil +#endif + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: platforms, + products: [ + .executable(name: "HelloJSON", targets: ["HelloJSON"]) + ], + dependencies: [ + // during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main") + ], + targets: [ + .executableTarget( + name: "HelloJSON", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ] + ) + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} diff --git a/Examples/HelloJSON/README.md b/Examples/HelloJSON/README.md new file mode 100644 index 00000000..d9cc1f01 --- /dev/null +++ b/Examples/HelloJSON/README.md @@ -0,0 +1,78 @@ +# Hello JSON + +This is a simple example of an AWS Lambda function that takes a JSON structure as input parameter and returns a JSON structure as response. + +## Code + +The code defines a `HelloRequest` and `HelloResponse` data structure to represent the input and outpout payload. These structures are typically shared with a client project, such as an iOS application. + +The code creates a `LambdaRuntime` struct. In it's simplest form, the initializer takes a function as argument. The function is the handler that will be invoked when an event triggers the Lambda function. + +The handler is `(event: HelloRequest, context: LambdaContext)`. The function takes two arguments: +- the event argument is a `HelloRequest`. It is the parameter passed when invoking the function. +- the context argument is a `Lambda Context`. It is a description of the runtime context. + +The function return value will be encoded to an `HelloResponse` as your Lambda function response. + +## Build & Package + +To build & archive the package, type the following commands. + +```bash +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/HelloJSON/HelloJSON.zip` + +## Deploy + +Here is how to deploy using the `aws` command line. + +```bash +# Replace with your AWS Account ID +AWS_ACCOUNT_ID=012345678901 + +aws lambda create-function \ +--function-name HelloJSON \ +--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/HelloJSON/HelloJSON.zip \ +--runtime provided.al2 \ +--handler provided \ +--architectures arm64 \ +--role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution +``` + +The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. + +Be sure to define the `AWS_ACCOUNT_ID` environment variable with your actual AWS account ID (for example: 012345678901). + +## Invoke your Lambda function + +To invoke the Lambda function, use this `aws` command line. + +```bash +aws lambda invoke \ +--function-name HelloJSON \ +--payload $(echo '{ "name" : "Seb", "age" : 50 }' | base64) \ +out.txt && cat out.txt && rm out.txt +``` + +Note that the payload is expected to be a valid JSON string. + +This should output the following result. + +``` +{ + "StatusCode": 200, + "ExecutedVersion": "$LATEST" +} +{"greetings":"Hello Seb. You look younger than your age."} +``` + +## Undeploy + +When done testing, you can delete the Lambda function with this command. + +```bash +aws lambda delete-function --function-name HelloJSON +``` \ No newline at end of file diff --git a/Examples/HelloJSON/Sources/main.swift b/Examples/HelloJSON/Sources/main.swift new file mode 100644 index 00000000..9c375ae2 --- /dev/null +++ b/Examples/HelloJSON/Sources/main.swift @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime +import Foundation + +// in this example we are receiving and responding with a JSON structure + +// the data structure to represent the input parameter +struct HelloRequest: Decodable { + let name: String + let age: Int +} + +// the data structure to represent the output response +struct HelloResponse: Encodable { + let greetings: String +} + +// the JSON encoder. It is created outside of the handler. +// this instance is reused at each invocation. +let encoder = JSONEncoder() + +// the Lambda runtime +let runtime = LambdaRuntime { + (event: HelloRequest, context: LambdaContext) in + + return HelloResponse(greetings: "Hello \(event.name). You look \(event.age > 30 ? "younger" : "older") than your age.") +} + +// start the loop +try await runtime.run() diff --git a/Examples/HelloJSON/out.txt b/Examples/HelloJSON/out.txt new file mode 100644 index 00000000..42d75248 --- /dev/null +++ b/Examples/HelloJSON/out.txt @@ -0,0 +1 @@ +{"greetings":"Hello Seb. You look younger than your age."} \ No newline at end of file From 931468dd1e995d8bbe1a0404d00f96faa8176d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Thu, 7 Nov 2024 20:35:22 +0100 Subject: [PATCH 03/13] add entry in the example README and integration tests --- .github/workflows/pull_request.yml | 2 +- Examples/README.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 640f6dc2..8146bb4f 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -35,7 +35,7 @@ jobs: # We pass the list of examples here, but we can't pass an array as argument # Instead, we pass a String with a valid JSON array. # The workaround is mentioned here https://github.com/orgs/community/discussions/11692 - examples: "[ 'HelloWorld', 'APIGateway','S3_AWSSDK', 'S3_Soto', 'Streaming', 'BackgroundTasks' ]" + examples: "[ 'APIGateway', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'S3_AWSSDK', 'S3_Soto', 'Streaming' ]" archive_plugin_enabled: true diff --git a/Examples/README.md b/Examples/README.md index 4e597305..06c75a16 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -18,6 +18,8 @@ This directory contains example code for Lambda functions. - **[BackgroundTasks](BackgroundTasks/README.md)**: a Lambda function that continues to run background tasks after having sent the response (requires [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)). +- **[HelloJSON](HelloJSON/README.md)**: a Lambda function that accepts a JSON as input parameter and responds with a JSON output (requires [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)). + - **[HelloWorld](HelloWorld/README.md)**: a simple Lambda function (requires [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)). - **[S3_AWSSDK](S3_AWSSDK/README.md)**: a Lambda function that uses the [AWS SDK for Swift](https://docs.aws.amazon.com/sdk-for-swift/latest/developer-guide/getting-started.html) to invoke an [Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html) API (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). From 1e087c197b2c1bace26d09ae04458ad39873e55e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Thu, 7 Nov 2024 20:36:40 +0100 Subject: [PATCH 04/13] remove the encoder --- Examples/HelloJSON/README.md | 2 ++ Examples/HelloJSON/Sources/main.swift | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Examples/HelloJSON/README.md b/Examples/HelloJSON/README.md index d9cc1f01..ef0569df 100644 --- a/Examples/HelloJSON/README.md +++ b/Examples/HelloJSON/README.md @@ -2,6 +2,8 @@ This is a simple example of an AWS Lambda function that takes a JSON structure as input parameter and returns a JSON structure as response. +The runtime takes care of decoding the input and encoding the output. + ## Code The code defines a `HelloRequest` and `HelloResponse` data structure to represent the input and outpout payload. These structures are typically shared with a client project, such as an iOS application. diff --git a/Examples/HelloJSON/Sources/main.swift b/Examples/HelloJSON/Sources/main.swift index 9c375ae2..e135d206 100644 --- a/Examples/HelloJSON/Sources/main.swift +++ b/Examples/HelloJSON/Sources/main.swift @@ -28,10 +28,6 @@ struct HelloResponse: Encodable { let greetings: String } -// the JSON encoder. It is created outside of the handler. -// this instance is reused at each invocation. -let encoder = JSONEncoder() - // the Lambda runtime let runtime = LambdaRuntime { (event: HelloRequest, context: LambdaContext) in From 1dd1c5154760186ee726f8a68d81778be7a6b895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 8 Nov 2024 00:34:36 +0100 Subject: [PATCH 05/13] remove out.txt from example --- Examples/HelloJSON/out.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 Examples/HelloJSON/out.txt diff --git a/Examples/HelloJSON/out.txt b/Examples/HelloJSON/out.txt deleted file mode 100644 index 42d75248..00000000 --- a/Examples/HelloJSON/out.txt +++ /dev/null @@ -1 +0,0 @@ -{"greetings":"Hello Seb. You look younger than your age."} \ No newline at end of file From 27c4175be0d2c28f9fa71941367a8c1cfbdfdac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 8 Nov 2024 00:36:35 +0100 Subject: [PATCH 06/13] fix formafix format --- Examples/HelloJSON/Sources/main.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Examples/HelloJSON/Sources/main.swift b/Examples/HelloJSON/Sources/main.swift index e135d206..a23bf3a4 100644 --- a/Examples/HelloJSON/Sources/main.swift +++ b/Examples/HelloJSON/Sources/main.swift @@ -31,8 +31,10 @@ struct HelloResponse: Encodable { // the Lambda runtime let runtime = LambdaRuntime { (event: HelloRequest, context: LambdaContext) in - - return HelloResponse(greetings: "Hello \(event.name). You look \(event.age > 30 ? "younger" : "older") than your age.") + + HelloResponse( + greetings: "Hello \(event.name). You look \(event.age > 30 ? "younger" : "older") than your age." + ) } // start the loop From ddc95ddfc6a8aa35e813114a342a53c6683b0407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 8 Nov 2024 00:48:17 +0100 Subject: [PATCH 07/13] temp commenting out to see a green CI --- .../Lambda+LocalServer.swift | 604 +++++++++--------- 1 file changed, 303 insertions(+), 301 deletions(-) diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift index dc0165d4..b85affd6 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift @@ -12,306 +12,308 @@ // //===----------------------------------------------------------------------===// -#if DEBUG -import Dispatch -import Logging -import NIOConcurrencyHelpers -import NIOCore -import NIOHTTP1 -import NIOPosix - -// This functionality is designed for local testing hence beind a #if DEBUG flag. -// For example: -// -// try Lambda.withLocalServer { -// Lambda.run { (context: LambdaContext, event: String, callback: @escaping (Result) -> Void) in -// callback(.success("Hello, \(event)!")) +// commented out as long as we have a fix for Swift 6 language mode CI + +// #if DEBUG +// import Dispatch +// import Logging +// import NIOConcurrencyHelpers +// import NIOCore +// import NIOHTTP1 +// import NIOPosix + +// // This functionality is designed for local testing hence beind a #if DEBUG flag. +// // For example: +// // +// // try Lambda.withLocalServer { +// // Lambda.run { (context: LambdaContext, event: String, callback: @escaping (Result) -> Void) in +// // callback(.success("Hello, \(event)!")) +// // } +// // } +// extension Lambda { +// /// Execute code in the context of a mock Lambda server. +// /// +// /// - parameters: +// /// - invocationEndpoint: The endpoint to post events to. +// /// - body: Code to run within the context of the mock server. Typically this would be a Lambda.run function call. +// /// +// /// - note: This API is designed strictly for local testing and is behind a DEBUG flag +// static func withLocalServer(invocationEndpoint: String? = nil, _ body: @escaping () -> Value) throws -> Value +// { +// let server = LocalLambda.Server(invocationEndpoint: invocationEndpoint) +// try server.start().wait() +// defer { try! server.stop() } +// return body() +// } +// } + +// // MARK: - Local Mock Server + +// private enum LocalLambda { +// struct Server { +// private let logger: Logger +// private let group: EventLoopGroup +// private let host: String +// private let port: Int +// private let invocationEndpoint: String + +// init(invocationEndpoint: String?) { +// var logger = Logger(label: "LocalLambdaServer") +// logger.logLevel = .info +// self.logger = logger +// self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) +// self.host = "127.0.0.1" +// self.port = 0 +// self.invocationEndpoint = invocationEndpoint ?? "/invoke" +// } + +// func start() -> EventLoopFuture { +// let bootstrap = ServerBootstrap(group: group) +// .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) +// .childChannelInitializer { channel in +// channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap { _ in +// channel.pipeline.addHandler( +// HTTPHandler(logger: self.logger, invocationEndpoint: self.invocationEndpoint) +// ) +// } +// } +// return bootstrap.bind(host: self.host, port: self.port).flatMap { channel -> EventLoopFuture in +// guard channel.localAddress != nil else { +// return channel.eventLoop.makeFailedFuture(ServerError.cantBind) +// } +// self.logger.info( +// "LocalLambdaServer started and listening on \(self.host):\(self.port), receiving events on \(self.invocationEndpoint)" +// ) +// return channel.eventLoop.makeSucceededFuture(()) +// } +// } + +// func stop() throws { +// try self.group.syncShutdownGracefully() +// } +// } + +// final class HTTPHandler: ChannelInboundHandler { +// public typealias InboundIn = HTTPServerRequestPart +// public typealias OutboundOut = HTTPServerResponsePart + +// private var pending = CircularBuffer<(head: HTTPRequestHead, body: ByteBuffer?)>() + +// private static var invocations = CircularBuffer() +// private static var invocationState = InvocationState.waitingForLambdaRequest + +// private let logger: Logger +// private let invocationEndpoint: String + +// init(logger: Logger, invocationEndpoint: String) { +// self.logger = logger +// self.invocationEndpoint = invocationEndpoint +// } + +// func channelRead(context: ChannelHandlerContext, data: NIOAny) { +// let requestPart = unwrapInboundIn(data) + +// switch requestPart { +// case .head(let head): +// self.pending.append((head: head, body: nil)) +// case .body(var buffer): +// var request = self.pending.removeFirst() +// if request.body == nil { +// request.body = buffer +// } else { +// request.body!.writeBuffer(&buffer) +// } +// self.pending.prepend(request) +// case .end: +// let request = self.pending.removeFirst() +// self.processRequest(context: context, request: request) +// } +// } + +// func processRequest(context: ChannelHandlerContext, request: (head: HTTPRequestHead, body: ByteBuffer?)) { +// switch (request.head.method, request.head.uri) { +// // this endpoint is called by the client invoking the lambda +// case (.POST, let url) where url.hasSuffix(self.invocationEndpoint): +// guard let work = request.body else { +// return self.writeResponse(context: context, response: .init(status: .badRequest)) +// } +// let requestID = "\(DispatchTime.now().uptimeNanoseconds)" // FIXME: +// let promise = context.eventLoop.makePromise(of: Response.self) +// promise.futureResult.whenComplete { result in +// switch result { +// case .failure(let error): +// self.logger.error("invocation error: \(error)") +// self.writeResponse(context: context, response: .init(status: .internalServerError)) +// case .success(let response): +// self.writeResponse(context: context, response: response) +// } +// } +// let invocation = Invocation(requestID: requestID, request: work, responsePromise: promise) +// switch Self.invocationState { +// case .waitingForInvocation(let promise): +// promise.succeed(invocation) +// case .waitingForLambdaRequest, .waitingForLambdaResponse: +// Self.invocations.append(invocation) +// } + +// // lambda invocation using the wrong http method +// case (_, let url) where url.hasSuffix(self.invocationEndpoint): +// self.writeResponse(context: context, status: .methodNotAllowed) + +// // /next endpoint is called by the lambda polling for work +// case (.GET, let url) where url.hasSuffix(Consts.getNextInvocationURLSuffix): +// // check if our server is in the correct state +// guard case .waitingForLambdaRequest = Self.invocationState else { +// self.logger.error("invalid invocation state \(Self.invocationState)") +// self.writeResponse(context: context, response: .init(status: .unprocessableEntity)) +// return +// } + +// // pop the first task from the queue +// switch Self.invocations.popFirst() { +// case .none: +// // if there is nothing in the queue, +// // create a promise that we can fullfill when we get a new task +// let promise = context.eventLoop.makePromise(of: Invocation.self) +// promise.futureResult.whenComplete { result in +// switch result { +// case .failure(let error): +// self.logger.error("invocation error: \(error)") +// self.writeResponse(context: context, status: .internalServerError) +// case .success(let invocation): +// Self.invocationState = .waitingForLambdaResponse(invocation) +// self.writeResponse(context: context, response: invocation.makeResponse()) +// } +// } +// Self.invocationState = .waitingForInvocation(promise) +// case .some(let invocation): +// // if there is a task pending, we can immediatly respond with it. +// Self.invocationState = .waitingForLambdaResponse(invocation) +// self.writeResponse(context: context, response: invocation.makeResponse()) +// } + +// // :requestID/response endpoint is called by the lambda posting the response +// case (.POST, let url) where url.hasSuffix(Consts.postResponseURLSuffix): +// let parts = request.head.uri.split(separator: "/") +// guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { +// // the request is malformed, since we were expecting a requestId in the path +// return self.writeResponse(context: context, status: .badRequest) +// } +// guard case .waitingForLambdaResponse(let invocation) = Self.invocationState else { +// // a response was send, but we did not expect to receive one +// self.logger.error("invalid invocation state \(Self.invocationState)") +// return self.writeResponse(context: context, status: .unprocessableEntity) +// } +// guard requestID == invocation.requestID else { +// // the request's requestId is not matching the one we are expecting +// self.logger.error( +// "invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)" +// ) +// return self.writeResponse(context: context, status: .badRequest) +// } + +// invocation.responsePromise.succeed(.init(status: .ok, body: request.body)) +// self.writeResponse(context: context, status: .accepted) +// Self.invocationState = .waitingForLambdaRequest + +// // :requestID/error endpoint is called by the lambda posting an error response +// case (.POST, let url) where url.hasSuffix(Consts.postErrorURLSuffix): +// let parts = request.head.uri.split(separator: "/") +// guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { +// // the request is malformed, since we were expecting a requestId in the path +// return self.writeResponse(context: context, status: .badRequest) +// } +// guard case .waitingForLambdaResponse(let invocation) = Self.invocationState else { +// // a response was send, but we did not expect to receive one +// self.logger.error("invalid invocation state \(Self.invocationState)") +// return self.writeResponse(context: context, status: .unprocessableEntity) +// } +// guard requestID == invocation.requestID else { +// // the request's requestId is not matching the one we are expecting +// self.logger.error( +// "invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)" +// ) +// return self.writeResponse(context: context, status: .badRequest) +// } + +// invocation.responsePromise.succeed(.init(status: .internalServerError, body: request.body)) +// self.writeResponse(context: context, status: .accepted) +// Self.invocationState = .waitingForLambdaRequest + +// // unknown call +// default: +// self.writeResponse(context: context, status: .notFound) +// } +// } + +// func writeResponse(context: ChannelHandlerContext, status: HTTPResponseStatus) { +// self.writeResponse(context: context, response: .init(status: status)) +// } + +// func writeResponse(context: ChannelHandlerContext, response: Response) { +// var headers = HTTPHeaders(response.headers ?? []) +// headers.add(name: "content-length", value: "\(response.body?.readableBytes ?? 0)") +// let head = HTTPResponseHead( +// version: HTTPVersion(major: 1, minor: 1), +// status: response.status, +// headers: headers +// ) + +// context.write(wrapOutboundOut(.head(head))).whenFailure { error in +// self.logger.error("\(self) write error \(error)") +// } + +// if let buffer = response.body { +// context.write(wrapOutboundOut(.body(.byteBuffer(buffer)))).whenFailure { error in +// self.logger.error("\(self) write error \(error)") +// } +// } + +// context.writeAndFlush(wrapOutboundOut(.end(nil))).whenComplete { result in +// if case .failure(let error) = result { +// self.logger.error("\(self) write error \(error)") +// } +// } +// } + +// struct Response { +// var status: HTTPResponseStatus = .ok +// var headers: [(String, String)]? +// var body: ByteBuffer? +// } + +// struct Invocation { +// let requestID: String +// let request: ByteBuffer +// let responsePromise: EventLoopPromise + +// func makeResponse() -> Response { +// var response = Response() +// response.body = self.request +// // required headers +// response.headers = [ +// (AmazonHeaders.requestID, self.requestID), +// ( +// AmazonHeaders.invokedFunctionARN, +// "arn:aws:lambda:us-east-1:\(Int16.random(in: Int16.min ... Int16.max)):function:custom-runtime" +// ), +// (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), +// (AmazonHeaders.deadline, "\(DispatchWallTime.distantFuture.millisSinceEpoch)"), +// ] +// return response +// } +// } + +// enum InvocationState { +// case waitingForInvocation(EventLoopPromise) +// case waitingForLambdaRequest +// case waitingForLambdaResponse(Invocation) +// } +// } + +// enum ServerError: Error { +// case notReady +// case cantBind // } // } -extension Lambda { - /// Execute code in the context of a mock Lambda server. - /// - /// - parameters: - /// - invocationEndpoint: The endpoint to post events to. - /// - body: Code to run within the context of the mock server. Typically this would be a Lambda.run function call. - /// - /// - note: This API is designed strictly for local testing and is behind a DEBUG flag - static func withLocalServer(invocationEndpoint: String? = nil, _ body: @escaping () -> Value) throws -> Value - { - let server = LocalLambda.Server(invocationEndpoint: invocationEndpoint) - try server.start().wait() - defer { try! server.stop() } - return body() - } -} - -// MARK: - Local Mock Server - -private enum LocalLambda { - struct Server { - private let logger: Logger - private let group: EventLoopGroup - private let host: String - private let port: Int - private let invocationEndpoint: String - - init(invocationEndpoint: String?) { - var logger = Logger(label: "LocalLambdaServer") - logger.logLevel = .info - self.logger = logger - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - self.host = "127.0.0.1" - self.port = 0 - self.invocationEndpoint = invocationEndpoint ?? "/invoke" - } - - func start() -> EventLoopFuture { - let bootstrap = ServerBootstrap(group: group) - .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) - .childChannelInitializer { channel in - channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap { _ in - channel.pipeline.addHandler( - HTTPHandler(logger: self.logger, invocationEndpoint: self.invocationEndpoint) - ) - } - } - return bootstrap.bind(host: self.host, port: self.port).flatMap { channel -> EventLoopFuture in - guard channel.localAddress != nil else { - return channel.eventLoop.makeFailedFuture(ServerError.cantBind) - } - self.logger.info( - "LocalLambdaServer started and listening on \(self.host):\(self.port), receiving events on \(self.invocationEndpoint)" - ) - return channel.eventLoop.makeSucceededFuture(()) - } - } - - func stop() throws { - try self.group.syncShutdownGracefully() - } - } - - final class HTTPHandler: ChannelInboundHandler { - public typealias InboundIn = HTTPServerRequestPart - public typealias OutboundOut = HTTPServerResponsePart - - private var pending = CircularBuffer<(head: HTTPRequestHead, body: ByteBuffer?)>() - - private static var invocations = CircularBuffer() - private static var invocationState = InvocationState.waitingForLambdaRequest - - private let logger: Logger - private let invocationEndpoint: String - - init(logger: Logger, invocationEndpoint: String) { - self.logger = logger - self.invocationEndpoint = invocationEndpoint - } - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - let requestPart = unwrapInboundIn(data) - - switch requestPart { - case .head(let head): - self.pending.append((head: head, body: nil)) - case .body(var buffer): - var request = self.pending.removeFirst() - if request.body == nil { - request.body = buffer - } else { - request.body!.writeBuffer(&buffer) - } - self.pending.prepend(request) - case .end: - let request = self.pending.removeFirst() - self.processRequest(context: context, request: request) - } - } - - func processRequest(context: ChannelHandlerContext, request: (head: HTTPRequestHead, body: ByteBuffer?)) { - switch (request.head.method, request.head.uri) { - // this endpoint is called by the client invoking the lambda - case (.POST, let url) where url.hasSuffix(self.invocationEndpoint): - guard let work = request.body else { - return self.writeResponse(context: context, response: .init(status: .badRequest)) - } - let requestID = "\(DispatchTime.now().uptimeNanoseconds)" // FIXME: - let promise = context.eventLoop.makePromise(of: Response.self) - promise.futureResult.whenComplete { result in - switch result { - case .failure(let error): - self.logger.error("invocation error: \(error)") - self.writeResponse(context: context, response: .init(status: .internalServerError)) - case .success(let response): - self.writeResponse(context: context, response: response) - } - } - let invocation = Invocation(requestID: requestID, request: work, responsePromise: promise) - switch Self.invocationState { - case .waitingForInvocation(let promise): - promise.succeed(invocation) - case .waitingForLambdaRequest, .waitingForLambdaResponse: - Self.invocations.append(invocation) - } - - // lambda invocation using the wrong http method - case (_, let url) where url.hasSuffix(self.invocationEndpoint): - self.writeResponse(context: context, status: .methodNotAllowed) - - // /next endpoint is called by the lambda polling for work - case (.GET, let url) where url.hasSuffix(Consts.getNextInvocationURLSuffix): - // check if our server is in the correct state - guard case .waitingForLambdaRequest = Self.invocationState else { - self.logger.error("invalid invocation state \(Self.invocationState)") - self.writeResponse(context: context, response: .init(status: .unprocessableEntity)) - return - } - - // pop the first task from the queue - switch Self.invocations.popFirst() { - case .none: - // if there is nothing in the queue, - // create a promise that we can fullfill when we get a new task - let promise = context.eventLoop.makePromise(of: Invocation.self) - promise.futureResult.whenComplete { result in - switch result { - case .failure(let error): - self.logger.error("invocation error: \(error)") - self.writeResponse(context: context, status: .internalServerError) - case .success(let invocation): - Self.invocationState = .waitingForLambdaResponse(invocation) - self.writeResponse(context: context, response: invocation.makeResponse()) - } - } - Self.invocationState = .waitingForInvocation(promise) - case .some(let invocation): - // if there is a task pending, we can immediatly respond with it. - Self.invocationState = .waitingForLambdaResponse(invocation) - self.writeResponse(context: context, response: invocation.makeResponse()) - } - - // :requestID/response endpoint is called by the lambda posting the response - case (.POST, let url) where url.hasSuffix(Consts.postResponseURLSuffix): - let parts = request.head.uri.split(separator: "/") - guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { - // the request is malformed, since we were expecting a requestId in the path - return self.writeResponse(context: context, status: .badRequest) - } - guard case .waitingForLambdaResponse(let invocation) = Self.invocationState else { - // a response was send, but we did not expect to receive one - self.logger.error("invalid invocation state \(Self.invocationState)") - return self.writeResponse(context: context, status: .unprocessableEntity) - } - guard requestID == invocation.requestID else { - // the request's requestId is not matching the one we are expecting - self.logger.error( - "invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)" - ) - return self.writeResponse(context: context, status: .badRequest) - } - - invocation.responsePromise.succeed(.init(status: .ok, body: request.body)) - self.writeResponse(context: context, status: .accepted) - Self.invocationState = .waitingForLambdaRequest - - // :requestID/error endpoint is called by the lambda posting an error response - case (.POST, let url) where url.hasSuffix(Consts.postErrorURLSuffix): - let parts = request.head.uri.split(separator: "/") - guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { - // the request is malformed, since we were expecting a requestId in the path - return self.writeResponse(context: context, status: .badRequest) - } - guard case .waitingForLambdaResponse(let invocation) = Self.invocationState else { - // a response was send, but we did not expect to receive one - self.logger.error("invalid invocation state \(Self.invocationState)") - return self.writeResponse(context: context, status: .unprocessableEntity) - } - guard requestID == invocation.requestID else { - // the request's requestId is not matching the one we are expecting - self.logger.error( - "invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)" - ) - return self.writeResponse(context: context, status: .badRequest) - } - - invocation.responsePromise.succeed(.init(status: .internalServerError, body: request.body)) - self.writeResponse(context: context, status: .accepted) - Self.invocationState = .waitingForLambdaRequest - - // unknown call - default: - self.writeResponse(context: context, status: .notFound) - } - } - - func writeResponse(context: ChannelHandlerContext, status: HTTPResponseStatus) { - self.writeResponse(context: context, response: .init(status: status)) - } - - func writeResponse(context: ChannelHandlerContext, response: Response) { - var headers = HTTPHeaders(response.headers ?? []) - headers.add(name: "content-length", value: "\(response.body?.readableBytes ?? 0)") - let head = HTTPResponseHead( - version: HTTPVersion(major: 1, minor: 1), - status: response.status, - headers: headers - ) - - context.write(wrapOutboundOut(.head(head))).whenFailure { error in - self.logger.error("\(self) write error \(error)") - } - - if let buffer = response.body { - context.write(wrapOutboundOut(.body(.byteBuffer(buffer)))).whenFailure { error in - self.logger.error("\(self) write error \(error)") - } - } - - context.writeAndFlush(wrapOutboundOut(.end(nil))).whenComplete { result in - if case .failure(let error) = result { - self.logger.error("\(self) write error \(error)") - } - } - } - - struct Response { - var status: HTTPResponseStatus = .ok - var headers: [(String, String)]? - var body: ByteBuffer? - } - - struct Invocation { - let requestID: String - let request: ByteBuffer - let responsePromise: EventLoopPromise - - func makeResponse() -> Response { - var response = Response() - response.body = self.request - // required headers - response.headers = [ - (AmazonHeaders.requestID, self.requestID), - ( - AmazonHeaders.invokedFunctionARN, - "arn:aws:lambda:us-east-1:\(Int16.random(in: Int16.min ... Int16.max)):function:custom-runtime" - ), - (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), - (AmazonHeaders.deadline, "\(DispatchWallTime.distantFuture.millisSinceEpoch)"), - ] - return response - } - } - - enum InvocationState { - case waitingForInvocation(EventLoopPromise) - case waitingForLambdaRequest - case waitingForLambdaResponse(Invocation) - } - } - - enum ServerError: Error { - case notReady - case cantBind - } -} -#endif +// #endif From f43569356a18ef23ed2838da03dfe48390e92548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 12 Nov 2024 10:21:25 +0100 Subject: [PATCH 08/13] add .index-build to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f56422fc..f7a26a78 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store *.build +*.index-build /.xcodeproj *.pem .podspecs From 0afd9349c213bb0efc552a87a0e60bdc01a2e17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 12 Nov 2024 10:21:37 +0100 Subject: [PATCH 09/13] remove Foundation from example --- Examples/HelloJSON/Sources/main.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Examples/HelloJSON/Sources/main.swift b/Examples/HelloJSON/Sources/main.swift index a23bf3a4..547f8a13 100644 --- a/Examples/HelloJSON/Sources/main.swift +++ b/Examples/HelloJSON/Sources/main.swift @@ -13,7 +13,6 @@ //===----------------------------------------------------------------------===// import AWSLambdaRuntime -import Foundation // in this example we are receiving and responding with a JSON structure From 3f0cd25a17862b3191ae52d89c59d7e3b5a1207f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 12 Nov 2024 21:46:15 +0100 Subject: [PATCH 10/13] Add HelloJSON example to main README --- readme.md | 115 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 76 insertions(+), 39 deletions(-) diff --git a/readme.md b/readme.md index 24bd135c..23247d5d 100644 --- a/readme.md +++ b/readme.md @@ -4,6 +4,18 @@ > [!WARNING] > The Swift AWS Runtime v2 is work in progress. We will add more documentation and code examples over time. +## The Swift AWS Lambda Runtime + +Many modern systems have client components like iOS, macOS or watchOS applications as well as server components that those clients interact with. Serverless functions are often the easiest and most efficient way for client application developers to extend their applications into the cloud. + +Serverless functions are increasingly becoming a popular choice for running event-driven or otherwise ad-hoc compute tasks in the cloud. They power mission critical microservices and data intensive workloads. In many cases, serverless functions allow developers to more easily scale and control compute costs given their on-demand nature. + +When using serverless functions, attention must be given to resource utilization as it directly impacts the costs of the system. This is where Swift shines! With its low memory footprint, deterministic performance, and quick start time, Swift is a fantastic match for the serverless functions architecture. + +Combine this with Swift's developer friendliness, expressiveness, and emphasis on safety, and we have a solution that is great for developers at all skill levels, scalable, and cost effective. + +Swift AWS Lambda Runtime was designed to make building Lambda functions in Swift simple and safe. The library is an implementation of the [AWS Lambda Runtime API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) and uses an embedded asynchronous HTTP Client based on [SwiftNIO](http://github.com/apple/swift-nio) that is fine-tuned for performance in the AWS Runtime context. The library provides a multi-tier API that allows building a range of Lambda functions: From quick and simple closures to complex, performance-sensitive event handlers. + ## Pre-requisites - Ensure you have the Swift 6.x toolchain installed. You can [install Swift toolchains](https://www.swift.org/install/macos/) from Swift.org @@ -16,7 +28,11 @@ - Some examples are using [AWS SAM](https://aws.amazon.com/serverless/sam/). Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) before deploying these examples. -## TL;DR +## Getting started + +To get started, read [the Swift AWS Lambda runtime v1 tutorial](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/1.0.0-alpha.3/tutorials/table-of-content). It provides developers with detailed step-by-step instructions to develop, build, and deploy a Lambda function. + +Or, if you're impatient to start with runtime v2, try these six steps: 1. Create a new Swift executable project @@ -128,53 +144,42 @@ This should print "dlroW olleH" ``` -## Tutorial - -[The Swift AWS Lambda Runtime docc tutorial](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/1.0.0-alpha.3/tutorials/table-of-content) provides developers with detailed step-by-step instructions to develop, build, and deploy a Lambda function. - -## Swift AWS Lambda Runtime - -Many modern systems have client components like iOS, macOS or watchOS applications as well as server components that those clients interact with. Serverless functions are often the easiest and most efficient way for client application developers to extend their applications into the cloud. - -Serverless functions are increasingly becoming a popular choice for running event-driven or otherwise ad-hoc compute tasks in the cloud. They power mission critical microservices and data intensive workloads. In many cases, serverless functions allow developers to more easily scale and control compute costs given their on-demand nature. - -When using serverless functions, attention must be given to resource utilization as it directly impacts the costs of the system. This is where Swift shines! With its low memory footprint, deterministic performance, and quick start time, Swift is a fantastic match for the serverless functions architecture. +## Developing your Swift Lambda functions -Combine this with Swift's developer friendliness, expressiveness, and emphasis on safety, and we have a solution that is great for developers at all skill levels, scalable, and cost effective. - -Swift AWS Lambda Runtime was designed to make building Lambda functions in Swift simple and safe. The library is an implementation of the [AWS Lambda Runtime API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) and uses an embedded asynchronous HTTP Client based on [SwiftNIO](http://github.com/apple/swift-nio) that is fine-tuned for performance in the AWS Runtime context. The library provides a multi-tier API that allows building a range of Lambda functions: From quick and simple closures to complex, performance-sensitive event handlers. - -## Design Principles - -The [design document](Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md) details the v2 API proposal for the swift-aws-lambda-runtime library, which aims to enhance the developer experience for building serverless functions in Swift. - -The proposal has been reviewed and [incorporated feedback from the community](https://forums.swift.org/t/aws-lambda-v2-api-proposal/73819). The full v2 API design document is available [in this repository](Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md). - -### Key Design Principles - -The v2 API prioritizes the following principles: - -- Readability and Maintainability: Extensive use of `async`/`await` improves code clarity and simplifies maintenance. - -- Developer Control: Developers own the `main()` function and have the flexibility to inject dependencies into the `LambdaRuntime`. This allows you to manage service lifecycles efficiently using [Swift Service Lifecycle](https://github.com/swift-server/swift-service-lifecycle) for structured concurrency. +### Receive and respond with JSON objects -- Simplified Codable Support: The `LambdaCodableAdapter` struct eliminates the need for verbose boilerplate code when encoding and decoding events and responses. +Typically, your Lambda functions will receive an input parameter expressed as JSON and will respond with another JSON. The Swift AWS Lambda runtime automatically takes care of encoding and decoding JSON objects when your Lambda function handler accepts `Decodable` and returns `Encodable` conforming `struct`. -### New Capabilities +Here is an example of a minimal function that accepts a JSON object as input and responds with another JSON object. -The v2 API introduces two new features: +```swift +import AWSLambdaRuntime -[Response Streaming](https://aws.amazon.com/blogs/compute/introducing-aws-lambda-response-streaming/]): This functionality is ideal for handling large responses that need to be sent incrementally.   +// the data structure to represent the input parameter +struct HelloRequest: Decodable { + let name: String + let age: Int +} -[Background Work](https://aws.amazon.com/blogs/compute/running-code-after-returning-a-response-from-an-aws-lambda-function/): Schedule tasks to run after returning a response to the AWS Lambda control plane. +// the data structure to represent the output response +struct HelloResponse: Encodable { + let greetings: String +} -These new capabilities provide greater flexibility and control when building serverless functions in Swift with the swift-aws-lambda-runtime library. +// the Lambda runtime +let runtime = LambdaRuntime { + (event: HelloRequest, context: LambdaContext) in -## AWSLambdaRuntime API + HelloResponse( + greetings: "Hello \(event.name). You look \(event.age > 30 ? "younger" : "older") than your age." + ) +} -### Receive and respond with JSON objects +// start the loop +try await runtime.run() +``` -tbd + link to docc +You can learn how to deploy and invoke this function in [the example README file](Examples/HelloJSON/README.md). ### Lambda Streaming Response @@ -264,4 +269,36 @@ let runtime = LambdaRuntime.init(handler: adapter) try await runtime.run() ``` -You can learn how to deploy and invoke this function in [the example README file](Examples/BackgroundTasks/README.md). \ No newline at end of file +You can learn how to deploy and invoke this function in [the example README file](Examples/BackgroundTasks/README.md). + +## Deploying your Swift Lambda functions + + +TODO + + +## Swift AWS Lambda Runtime - Design Principles + +The [design document](Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md) details the v2 API proposal for the swift-aws-lambda-runtime library, which aims to enhance the developer experience for building serverless functions in Swift. + +The proposal has been reviewed and [incorporated feedback from the community](https://forums.swift.org/t/aws-lambda-v2-api-proposal/73819). The full v2 API design document is available [in this repository](Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md). + +### Key Design Principles + +The v2 API prioritizes the following principles: + +- Readability and Maintainability: Extensive use of `async`/`await` improves code clarity and simplifies maintenance. + +- Developer Control: Developers own the `main()` function and have the flexibility to inject dependencies into the `LambdaRuntime`. This allows you to manage service lifecycles efficiently using [Swift Service Lifecycle](https://github.com/swift-server/swift-service-lifecycle) for structured concurrency. + +- Simplified Codable Support: The `LambdaCodableAdapter` struct eliminates the need for verbose boilerplate code when encoding and decoding events and responses. + +### New Capabilities + +The v2 API introduces two new features: + +[Response Streaming](https://aws.amazon.com/blogs/compute/introducing-aws-lambda-response-streaming/]): This functionality is ideal for handling large responses that need to be sent incrementally.   + +[Background Work](https://aws.amazon.com/blogs/compute/running-code-after-returning-a-response-from-an-aws-lambda-function/): Schedule tasks to run after returning a response to the AWS Lambda control plane. + +These new capabilities provide greater flexibility and control when building serverless functions in Swift with the swift-aws-lambda-runtime library. \ No newline at end of file From c936a7ea432bb64f47adada90c18a3a41c07e28e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 12 Nov 2024 22:15:03 +0100 Subject: [PATCH 11/13] add an lambda event example in the readme file --- readme.md | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 23247d5d..b5805d80 100644 --- a/readme.md +++ b/readme.md @@ -179,7 +179,7 @@ let runtime = LambdaRuntime { try await runtime.run() ``` -You can learn how to deploy and invoke this function in [the example README file](Examples/HelloJSON/README.md). +You can learn how to deploy and invoke this function in [the Hello JSON example README file](Examples/HelloJSON/README.md). ### Lambda Streaming Response @@ -216,7 +216,30 @@ let runtime = LambdaRuntime.init(handler: SendNumbersWithPause()) try await runtime.run() ``` -You can learn how to deploy and invoke this function in [the example README file](Examples/Streaming/README.md). +You can learn how to deploy and invoke this function in [the streaming example README file](Examples/Streaming/README.md). + +### Integration with AWS Services + + Most Lambda functions are triggered by events originating in other AWS services such as `Amazon SNS`, `Amazon SQS` or `AWS APIGateway`. + + The [Swift AWS Lambda Events](http://github.com/swift-server/swift-aws-lambda-events) package includes an `AWSLambdaEvents` module that provides implementations for most common AWS event types further simplifying writing Lambda functions. + + Here is an example Lambda function invoked when the AWS APIGateway receives an HTTP request. + + ```swift +import AWSLambdaEvents +import AWSLambdaRuntime + +let runtime = LambdaRuntime { + (event: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response in + + APIGatewayV2Response(statusCode: .ok, body: "Hello World!") +} + +try await runtime.run() +``` + + You can learn how to deploy and invoke this function in [the API Gateway example README file](Examples/APIGateway/README.md). ### Integration with Swift Service LifeCycle @@ -269,7 +292,7 @@ let runtime = LambdaRuntime.init(handler: adapter) try await runtime.run() ``` -You can learn how to deploy and invoke this function in [the example README file](Examples/BackgroundTasks/README.md). +You can learn how to deploy and invoke this function in [the background tasks example README file](Examples/BackgroundTasks/README.md). ## Deploying your Swift Lambda functions From f5d6888608e4eb3a8ee663ccff44b7b34f4ae1d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 13 Nov 2024 09:03:27 +0100 Subject: [PATCH 12/13] add support for localserver --- .../Lambda+LocalServer.swift | 604 +++++++++--------- .../AWSLambdaRuntimeCore/LambdaRuntime.swift | 55 +- 2 files changed, 341 insertions(+), 318 deletions(-) diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift index b85affd6..9eed4389 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift @@ -14,306 +14,308 @@ // commented out as long as we have a fix for Swift 6 language mode CI -// #if DEBUG -// import Dispatch -// import Logging -// import NIOConcurrencyHelpers -// import NIOCore -// import NIOHTTP1 -// import NIOPosix - -// // This functionality is designed for local testing hence beind a #if DEBUG flag. -// // For example: -// // -// // try Lambda.withLocalServer { -// // Lambda.run { (context: LambdaContext, event: String, callback: @escaping (Result) -> Void) in -// // callback(.success("Hello, \(event)!")) -// // } -// // } -// extension Lambda { -// /// Execute code in the context of a mock Lambda server. -// /// -// /// - parameters: -// /// - invocationEndpoint: The endpoint to post events to. -// /// - body: Code to run within the context of the mock server. Typically this would be a Lambda.run function call. -// /// -// /// - note: This API is designed strictly for local testing and is behind a DEBUG flag -// static func withLocalServer(invocationEndpoint: String? = nil, _ body: @escaping () -> Value) throws -> Value -// { -// let server = LocalLambda.Server(invocationEndpoint: invocationEndpoint) -// try server.start().wait() -// defer { try! server.stop() } -// return body() -// } -// } - -// // MARK: - Local Mock Server - -// private enum LocalLambda { -// struct Server { -// private let logger: Logger -// private let group: EventLoopGroup -// private let host: String -// private let port: Int -// private let invocationEndpoint: String - -// init(invocationEndpoint: String?) { -// var logger = Logger(label: "LocalLambdaServer") -// logger.logLevel = .info -// self.logger = logger -// self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) -// self.host = "127.0.0.1" -// self.port = 0 -// self.invocationEndpoint = invocationEndpoint ?? "/invoke" -// } - -// func start() -> EventLoopFuture { -// let bootstrap = ServerBootstrap(group: group) -// .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) -// .childChannelInitializer { channel in -// channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap { _ in -// channel.pipeline.addHandler( -// HTTPHandler(logger: self.logger, invocationEndpoint: self.invocationEndpoint) -// ) -// } -// } -// return bootstrap.bind(host: self.host, port: self.port).flatMap { channel -> EventLoopFuture in -// guard channel.localAddress != nil else { -// return channel.eventLoop.makeFailedFuture(ServerError.cantBind) -// } -// self.logger.info( -// "LocalLambdaServer started and listening on \(self.host):\(self.port), receiving events on \(self.invocationEndpoint)" -// ) -// return channel.eventLoop.makeSucceededFuture(()) -// } -// } - -// func stop() throws { -// try self.group.syncShutdownGracefully() -// } -// } - -// final class HTTPHandler: ChannelInboundHandler { -// public typealias InboundIn = HTTPServerRequestPart -// public typealias OutboundOut = HTTPServerResponsePart - -// private var pending = CircularBuffer<(head: HTTPRequestHead, body: ByteBuffer?)>() - -// private static var invocations = CircularBuffer() -// private static var invocationState = InvocationState.waitingForLambdaRequest - -// private let logger: Logger -// private let invocationEndpoint: String - -// init(logger: Logger, invocationEndpoint: String) { -// self.logger = logger -// self.invocationEndpoint = invocationEndpoint -// } - -// func channelRead(context: ChannelHandlerContext, data: NIOAny) { -// let requestPart = unwrapInboundIn(data) - -// switch requestPart { -// case .head(let head): -// self.pending.append((head: head, body: nil)) -// case .body(var buffer): -// var request = self.pending.removeFirst() -// if request.body == nil { -// request.body = buffer -// } else { -// request.body!.writeBuffer(&buffer) -// } -// self.pending.prepend(request) -// case .end: -// let request = self.pending.removeFirst() -// self.processRequest(context: context, request: request) -// } -// } - -// func processRequest(context: ChannelHandlerContext, request: (head: HTTPRequestHead, body: ByteBuffer?)) { -// switch (request.head.method, request.head.uri) { -// // this endpoint is called by the client invoking the lambda -// case (.POST, let url) where url.hasSuffix(self.invocationEndpoint): -// guard let work = request.body else { -// return self.writeResponse(context: context, response: .init(status: .badRequest)) -// } -// let requestID = "\(DispatchTime.now().uptimeNanoseconds)" // FIXME: -// let promise = context.eventLoop.makePromise(of: Response.self) -// promise.futureResult.whenComplete { result in -// switch result { -// case .failure(let error): -// self.logger.error("invocation error: \(error)") -// self.writeResponse(context: context, response: .init(status: .internalServerError)) -// case .success(let response): -// self.writeResponse(context: context, response: response) -// } -// } -// let invocation = Invocation(requestID: requestID, request: work, responsePromise: promise) -// switch Self.invocationState { -// case .waitingForInvocation(let promise): -// promise.succeed(invocation) -// case .waitingForLambdaRequest, .waitingForLambdaResponse: -// Self.invocations.append(invocation) -// } - -// // lambda invocation using the wrong http method -// case (_, let url) where url.hasSuffix(self.invocationEndpoint): -// self.writeResponse(context: context, status: .methodNotAllowed) - -// // /next endpoint is called by the lambda polling for work -// case (.GET, let url) where url.hasSuffix(Consts.getNextInvocationURLSuffix): -// // check if our server is in the correct state -// guard case .waitingForLambdaRequest = Self.invocationState else { -// self.logger.error("invalid invocation state \(Self.invocationState)") -// self.writeResponse(context: context, response: .init(status: .unprocessableEntity)) -// return -// } - -// // pop the first task from the queue -// switch Self.invocations.popFirst() { -// case .none: -// // if there is nothing in the queue, -// // create a promise that we can fullfill when we get a new task -// let promise = context.eventLoop.makePromise(of: Invocation.self) -// promise.futureResult.whenComplete { result in -// switch result { -// case .failure(let error): -// self.logger.error("invocation error: \(error)") -// self.writeResponse(context: context, status: .internalServerError) -// case .success(let invocation): -// Self.invocationState = .waitingForLambdaResponse(invocation) -// self.writeResponse(context: context, response: invocation.makeResponse()) -// } -// } -// Self.invocationState = .waitingForInvocation(promise) -// case .some(let invocation): -// // if there is a task pending, we can immediatly respond with it. -// Self.invocationState = .waitingForLambdaResponse(invocation) -// self.writeResponse(context: context, response: invocation.makeResponse()) -// } - -// // :requestID/response endpoint is called by the lambda posting the response -// case (.POST, let url) where url.hasSuffix(Consts.postResponseURLSuffix): -// let parts = request.head.uri.split(separator: "/") -// guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { -// // the request is malformed, since we were expecting a requestId in the path -// return self.writeResponse(context: context, status: .badRequest) -// } -// guard case .waitingForLambdaResponse(let invocation) = Self.invocationState else { -// // a response was send, but we did not expect to receive one -// self.logger.error("invalid invocation state \(Self.invocationState)") -// return self.writeResponse(context: context, status: .unprocessableEntity) -// } -// guard requestID == invocation.requestID else { -// // the request's requestId is not matching the one we are expecting -// self.logger.error( -// "invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)" -// ) -// return self.writeResponse(context: context, status: .badRequest) -// } - -// invocation.responsePromise.succeed(.init(status: .ok, body: request.body)) -// self.writeResponse(context: context, status: .accepted) -// Self.invocationState = .waitingForLambdaRequest - -// // :requestID/error endpoint is called by the lambda posting an error response -// case (.POST, let url) where url.hasSuffix(Consts.postErrorURLSuffix): -// let parts = request.head.uri.split(separator: "/") -// guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { -// // the request is malformed, since we were expecting a requestId in the path -// return self.writeResponse(context: context, status: .badRequest) -// } -// guard case .waitingForLambdaResponse(let invocation) = Self.invocationState else { -// // a response was send, but we did not expect to receive one -// self.logger.error("invalid invocation state \(Self.invocationState)") -// return self.writeResponse(context: context, status: .unprocessableEntity) -// } -// guard requestID == invocation.requestID else { -// // the request's requestId is not matching the one we are expecting -// self.logger.error( -// "invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)" -// ) -// return self.writeResponse(context: context, status: .badRequest) -// } - -// invocation.responsePromise.succeed(.init(status: .internalServerError, body: request.body)) -// self.writeResponse(context: context, status: .accepted) -// Self.invocationState = .waitingForLambdaRequest - -// // unknown call -// default: -// self.writeResponse(context: context, status: .notFound) -// } -// } - -// func writeResponse(context: ChannelHandlerContext, status: HTTPResponseStatus) { -// self.writeResponse(context: context, response: .init(status: status)) -// } - -// func writeResponse(context: ChannelHandlerContext, response: Response) { -// var headers = HTTPHeaders(response.headers ?? []) -// headers.add(name: "content-length", value: "\(response.body?.readableBytes ?? 0)") -// let head = HTTPResponseHead( -// version: HTTPVersion(major: 1, minor: 1), -// status: response.status, -// headers: headers -// ) - -// context.write(wrapOutboundOut(.head(head))).whenFailure { error in -// self.logger.error("\(self) write error \(error)") -// } - -// if let buffer = response.body { -// context.write(wrapOutboundOut(.body(.byteBuffer(buffer)))).whenFailure { error in -// self.logger.error("\(self) write error \(error)") -// } -// } - -// context.writeAndFlush(wrapOutboundOut(.end(nil))).whenComplete { result in -// if case .failure(let error) = result { -// self.logger.error("\(self) write error \(error)") -// } -// } -// } - -// struct Response { -// var status: HTTPResponseStatus = .ok -// var headers: [(String, String)]? -// var body: ByteBuffer? -// } - -// struct Invocation { -// let requestID: String -// let request: ByteBuffer -// let responsePromise: EventLoopPromise - -// func makeResponse() -> Response { -// var response = Response() -// response.body = self.request -// // required headers -// response.headers = [ -// (AmazonHeaders.requestID, self.requestID), -// ( -// AmazonHeaders.invokedFunctionARN, -// "arn:aws:lambda:us-east-1:\(Int16.random(in: Int16.min ... Int16.max)):function:custom-runtime" -// ), -// (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), -// (AmazonHeaders.deadline, "\(DispatchWallTime.distantFuture.millisSinceEpoch)"), -// ] -// return response -// } -// } - -// enum InvocationState { -// case waitingForInvocation(EventLoopPromise) -// case waitingForLambdaRequest -// case waitingForLambdaResponse(Invocation) -// } -// } - -// enum ServerError: Error { -// case notReady -// case cantBind +#if DEBUG +import Dispatch +import Logging +import NIOConcurrencyHelpers +import NIOCore +import NIOHTTP1 +import NIOPosix + +// This functionality is designed for local testing hence being a #if DEBUG flag. +// For example: +// +// try Lambda.withLocalServer { +// Lambda.run { (context: LambdaContext, event: String, callback: @escaping (Result) -> Void) in +// callback(.success("Hello, \(event)!")) // } // } -// #endif +extension Lambda { + /// Execute code in the context of a mock Lambda server. + /// + /// - parameters: + /// - invocationEndpoint: The endpoint to post events to. + /// - body: Code to run within the context of the mock server. Typically this would be a Lambda.run function call. + /// + /// - note: This API is designed strictly for local testing and is behind a DEBUG flag + static func withLocalServer( + invocationEndpoint: String? = nil, + _ body: @escaping () async throws -> Value + ) async throws -> Value { + let server = LocalLambda.Server(invocationEndpoint: invocationEndpoint) + try await server.start().get() + defer { try! server.stop() } + return try await body() + } +} + +// MARK: - Local Mock Server + +private enum LocalLambda { + struct Server { + private let logger: Logger + private let group: EventLoopGroup + private let host: String + private let port: Int + private let invocationEndpoint: String + + init(invocationEndpoint: String?) { + var logger = Logger(label: "LocalLambdaServer") + logger.logLevel = .info + self.logger = logger + self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + self.host = "127.0.0.1" + self.port = 7000 + self.invocationEndpoint = invocationEndpoint ?? "/invoke" + } + + func start() -> EventLoopFuture { + let bootstrap = ServerBootstrap(group: group) + .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) + .childChannelInitializer { channel in + channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap { _ in + channel.pipeline.addHandler( + HTTPHandler(logger: self.logger, invocationEndpoint: self.invocationEndpoint) + ) + } + } + return bootstrap.bind(host: self.host, port: self.port).flatMap { channel -> EventLoopFuture in + guard channel.localAddress != nil else { + return channel.eventLoop.makeFailedFuture(ServerError.cantBind) + } + self.logger.info( + "LocalLambdaServer started and listening on \(self.host):\(self.port), receiving events on \(self.invocationEndpoint)" + ) + return channel.eventLoop.makeSucceededFuture(()) + } + } + + func stop() throws { + try self.group.syncShutdownGracefully() + } + } + + final class HTTPHandler: ChannelInboundHandler { + public typealias InboundIn = HTTPServerRequestPart + public typealias OutboundOut = HTTPServerResponsePart + + private var pending = CircularBuffer<(head: HTTPRequestHead, body: ByteBuffer?)>() + + private static var invocations = CircularBuffer() + private static var invocationState = InvocationState.waitingForLambdaRequest + + private let logger: Logger + private let invocationEndpoint: String + + init(logger: Logger, invocationEndpoint: String) { + self.logger = logger + self.invocationEndpoint = invocationEndpoint + } + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let requestPart = unwrapInboundIn(data) + + switch requestPart { + case .head(let head): + self.pending.append((head: head, body: nil)) + case .body(var buffer): + var request = self.pending.removeFirst() + if request.body == nil { + request.body = buffer + } else { + request.body!.writeBuffer(&buffer) + } + self.pending.prepend(request) + case .end: + let request = self.pending.removeFirst() + self.processRequest(context: context, request: request) + } + } + + func processRequest(context: ChannelHandlerContext, request: (head: HTTPRequestHead, body: ByteBuffer?)) { + switch (request.head.method, request.head.uri) { + // this endpoint is called by the client invoking the lambda + case (.POST, let url) where url.hasSuffix(self.invocationEndpoint): + guard let work = request.body else { + return self.writeResponse(context: context, response: .init(status: .badRequest)) + } + let requestID = "\(DispatchTime.now().uptimeNanoseconds)" // FIXME: + let promise = context.eventLoop.makePromise(of: Response.self) + promise.futureResult.whenComplete { result in + switch result { + case .failure(let error): + self.logger.error("invocation error: \(error)") + self.writeResponse(context: context, response: .init(status: .internalServerError)) + case .success(let response): + self.writeResponse(context: context, response: response) + } + } + let invocation = Invocation(requestID: requestID, request: work, responsePromise: promise) + switch Self.invocationState { + case .waitingForInvocation(let promise): + promise.succeed(invocation) + case .waitingForLambdaRequest, .waitingForLambdaResponse: + Self.invocations.append(invocation) + } + + // lambda invocation using the wrong http method + case (_, let url) where url.hasSuffix(self.invocationEndpoint): + self.writeResponse(context: context, status: .methodNotAllowed) + + // /next endpoint is called by the lambda polling for work + case (.GET, let url) where url.hasSuffix(Consts.getNextInvocationURLSuffix): + // check if our server is in the correct state + guard case .waitingForLambdaRequest = Self.invocationState else { + self.logger.error("invalid invocation state \(Self.invocationState)") + self.writeResponse(context: context, response: .init(status: .unprocessableEntity)) + return + } + + // pop the first task from the queue + switch Self.invocations.popFirst() { + case .none: + // if there is nothing in the queue, + // create a promise that we can fullfill when we get a new task + let promise = context.eventLoop.makePromise(of: Invocation.self) + promise.futureResult.whenComplete { result in + switch result { + case .failure(let error): + self.logger.error("invocation error: \(error)") + self.writeResponse(context: context, status: .internalServerError) + case .success(let invocation): + Self.invocationState = .waitingForLambdaResponse(invocation) + self.writeResponse(context: context, response: invocation.makeResponse()) + } + } + Self.invocationState = .waitingForInvocation(promise) + case .some(let invocation): + // if there is a task pending, we can immediately respond with it. + Self.invocationState = .waitingForLambdaResponse(invocation) + self.writeResponse(context: context, response: invocation.makeResponse()) + } + + // :requestID/response endpoint is called by the lambda posting the response + case (.POST, let url) where url.hasSuffix(Consts.postResponseURLSuffix): + let parts = request.head.uri.split(separator: "/") + guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { + // the request is malformed, since we were expecting a requestId in the path + return self.writeResponse(context: context, status: .badRequest) + } + guard case .waitingForLambdaResponse(let invocation) = Self.invocationState else { + // a response was send, but we did not expect to receive one + self.logger.error("invalid invocation state \(Self.invocationState)") + return self.writeResponse(context: context, status: .unprocessableEntity) + } + guard requestID == invocation.requestID else { + // the request's requestId is not matching the one we are expecting + self.logger.error( + "invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)" + ) + return self.writeResponse(context: context, status: .badRequest) + } + + invocation.responsePromise.succeed(.init(status: .ok, body: request.body)) + self.writeResponse(context: context, status: .accepted) + Self.invocationState = .waitingForLambdaRequest + + // :requestID/error endpoint is called by the lambda posting an error response + case (.POST, let url) where url.hasSuffix(Consts.postErrorURLSuffix): + let parts = request.head.uri.split(separator: "/") + guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { + // the request is malformed, since we were expecting a requestId in the path + return self.writeResponse(context: context, status: .badRequest) + } + guard case .waitingForLambdaResponse(let invocation) = Self.invocationState else { + // a response was send, but we did not expect to receive one + self.logger.error("invalid invocation state \(Self.invocationState)") + return self.writeResponse(context: context, status: .unprocessableEntity) + } + guard requestID == invocation.requestID else { + // the request's requestId is not matching the one we are expecting + self.logger.error( + "invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)" + ) + return self.writeResponse(context: context, status: .badRequest) + } + + invocation.responsePromise.succeed(.init(status: .internalServerError, body: request.body)) + self.writeResponse(context: context, status: .accepted) + Self.invocationState = .waitingForLambdaRequest + + // unknown call + default: + self.writeResponse(context: context, status: .notFound) + } + } + + func writeResponse(context: ChannelHandlerContext, status: HTTPResponseStatus) { + self.writeResponse(context: context, response: .init(status: status)) + } + + func writeResponse(context: ChannelHandlerContext, response: Response) { + var headers = HTTPHeaders(response.headers ?? []) + headers.add(name: "content-length", value: "\(response.body?.readableBytes ?? 0)") + let head = HTTPResponseHead( + version: HTTPVersion(major: 1, minor: 1), + status: response.status, + headers: headers + ) + + context.write(wrapOutboundOut(.head(head))).whenFailure { error in + self.logger.error("\(self) write error \(error)") + } + + if let buffer = response.body { + context.write(wrapOutboundOut(.body(.byteBuffer(buffer)))).whenFailure { error in + self.logger.error("\(self) write error \(error)") + } + } + + context.writeAndFlush(wrapOutboundOut(.end(nil))).whenComplete { result in + if case .failure(let error) = result { + self.logger.error("\(self) write error \(error)") + } + } + } + + struct Response { + var status: HTTPResponseStatus = .ok + var headers: [(String, String)]? + var body: ByteBuffer? + } + + struct Invocation { + let requestID: String + let request: ByteBuffer + let responsePromise: EventLoopPromise + + func makeResponse() -> Response { + var response = Response() + response.body = self.request + // required headers + response.headers = [ + (AmazonHeaders.requestID, self.requestID), + ( + AmazonHeaders.invokedFunctionARN, + "arn:aws:lambda:us-east-1:\(Int16.random(in: Int16.min ... Int16.max)):function:custom-runtime" + ), + (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), + (AmazonHeaders.deadline, "\(DispatchWallTime.distantFuture.millisSinceEpoch)"), + ] + return response + } + } + + enum InvocationState { + case waitingForInvocation(EventLoopPromise) + case waitingForLambdaRequest + case waitingForLambdaResponse(Invocation) + } + } + + enum ServerError: Error { + case notReady + case cantBind + } +} +#endif diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift index 726c39a4..86274836 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift @@ -43,14 +43,6 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St } public func run() async throws { - guard let runtimeEndpoint = Lambda.env("AWS_LAMBDA_RUNTIME_API") else { - throw LambdaRuntimeError(code: .missingLambdaRuntimeAPIEnvironmentVariable) - } - - let ipAndPort = runtimeEndpoint.split(separator: ":", maxSplits: 1) - let ip = String(ipAndPort[0]) - guard let port = Int(ipAndPort[1]) else { throw LambdaRuntimeError(code: .invalidPort) } - let handler = self.handlerMutex.withLockedValue { handler in let result = handler handler = nil @@ -61,16 +53,45 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St throw LambdaRuntimeError(code: .runtimeCanOnlyBeStartedOnce) } - try await LambdaRuntimeClient.withRuntimeClient( - configuration: .init(ip: ip, port: port), - eventLoop: self.eventLoop, - logger: self.logger - ) { runtimeClient in - try await Lambda.runLoop( - runtimeClient: runtimeClient, - handler: handler, + // are we running inside an AWS Lambda runtime environment ? + // AWS_LAMBDA_RUNTIME_API is set when running on Lambda + // https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html + if let runtimeEndpoint = Lambda.env("AWS_LAMBDA_RUNTIME_API") { + + let ipAndPort = runtimeEndpoint.split(separator: ":", maxSplits: 1) + let ip = String(ipAndPort[0]) + guard let port = Int(ipAndPort[1]) else { throw LambdaRuntimeError(code: .invalidPort) } + + try await LambdaRuntimeClient.withRuntimeClient( + configuration: .init(ip: ip, port: port), + eventLoop: self.eventLoop, logger: self.logger - ) + ) { runtimeClient in + try await Lambda.runLoop( + runtimeClient: runtimeClient, + handler: handler, + logger: self.logger + ) + } + + } else { + + // we're not running on Lambda, let's start a local server for testing + try await Lambda.withLocalServer(invocationEndpoint: Lambda.env("LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT")) + { + + try await LambdaRuntimeClient.withRuntimeClient( + configuration: .init(ip: "127.0.0.1", port: 7000), + eventLoop: self.eventLoop, + logger: self.logger + ) { runtimeClient in + try await Lambda.runLoop( + runtimeClient: runtimeClient, + handler: handler, + logger: self.logger + ) + } + } } } } From 2397b0063a40ff5a9d0824bced65299e6462ed8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 13 Nov 2024 09:10:52 +0100 Subject: [PATCH 13/13] add doc readme --- readme.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/readme.md b/readme.md index b5805d80..973e6b8e 100644 --- a/readme.md +++ b/readme.md @@ -294,6 +294,49 @@ try await runtime.run() You can learn how to deploy and invoke this function in [the background tasks example README file](Examples/BackgroundTasks/README.md). +## Testing Locally + +Before deploying your code to AWS Lambda, you can test it locally by running the executable target on your local machine. It will look like this on CLI: + +```sh +swift run +``` + +When not running inside a Lambda execution environment, it starts a local HTTP server listening on port 7000. You can invoke your local Lambda function by sending an HTTP POST request to `http://127.0.0.1:7000/invoke`. + +The request must include the JSON payload expected as an `event` by your function. You can create a text file with the JSON payload documented by AWS or captured from a trace. In this example, we used [the APIGatewayv2 JSON payload from the documentation](https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html#apigateway-example-event), saved as `events/create-session.json` text file. + +Then we use curl to invoke the local endpoint with the test JSON payload. + +```sh +curl -v --header "Content-Type:\ application/json" --data @events/create-session.json http://127.0.0.1:7000/invoke +* Trying 127.0.0.1:7000... +* Connected to 127.0.0.1 (127.0.0.1) port 7000 +> POST /invoke HTTP/1.1 +> Host: 127.0.0.1:7000 +> User-Agent: curl/8.4.0 +> Accept: */* +> Content-Type:\ application/json +> Content-Length: 1160 +> +< HTTP/1.1 200 OK +< content-length: 247 +< +* Connection #0 to host 127.0.0.1 left intact +{"statusCode":200,"isBase64Encoded":false,"body":"...","headers":{"Access-Control-Allow-Origin":"*","Content-Type":"application\/json; charset=utf-8","Access-Control-Allow-Headers":"*"}} +``` +### Modifying the local endpoint + +By default, when using the local Lambda server, it listens on the `/invoke` endpoint. + +Some testing tools, such as the [AWS Lambda runtime interface emulator](https://docs.aws.amazon.com/lambda/latest/dg/images-test.html), require a different endpoint. In that case, you can use the `LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT` environment variable to force the runtime to listen on a different endpoint. + +Example: + +```sh +LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT=/2015-03-31/functions/function/invocations swift run +``` + ## Deploying your Swift Lambda functions