diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 468fc295..422ef82e 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -19,6 +19,11 @@ on: type: boolean description: "Boolean to enable the compilation of examples. Defaults to true." default: true + archive_plugin_examples: + type: string + description: "The list of examples to run through the archive plugin test. Pass a String with a valid JSON array such as \"[ 'HelloWorld', 'APIGateway' ]\"" + required: true + default: "" archive_plugin_enabled: type: boolean description: "Boolean to enable the test of the archive plugin. Defaults to true." @@ -33,8 +38,8 @@ on: required: true matrix_linux_swift_container_image: type: string - description: "Container image for the matrix job. Defaults to matching latest Swift Ubuntu image." - default: "swift:amazonlinux2" + description: "Container image for the matrix job. Defaults to matching latest Swift 6.1 Amazon Linux 2 image." + default: "swiftlang/swift:nightly-6.1-amazonlinux2" ## We are cancelling previously triggered workflow runs concurrency: @@ -54,7 +59,6 @@ jobs: # We are using only one Swift version swift: - image: ${{ inputs.matrix_linux_swift_container_image }} - swift_version: "6.0.1-amazonlinux2" container: image: ${{ matrix.swift.image }} steps: @@ -88,7 +92,6 @@ jobs: - name: Run matrix job working-directory: ${{ github.event.repository.name }} # until we can use action/checkout@v4 env: - SWIFT_VERSION: ${{ matrix.swift.swift_version }} COMMAND: ${{ inputs.matrix_linux_command }} EXAMPLE: ${{ matrix.examples }} run: | @@ -98,6 +101,10 @@ jobs: name: Test archive plugin if: ${{ inputs.archive_plugin_enabled }} runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + examples: ${{ fromJson(inputs.archive_plugin_examples) }} steps: - name: Checkout repository uses: actions/checkout@v4 @@ -107,8 +114,10 @@ jobs: # https://github.com/actions/checkout/issues/766 run: git config --global --add safe.directory ${GITHUB_WORKSPACE} - name: Test the archive plugin + env: + EXAMPLE: ${{ matrix.examples }} run: | - .github/workflows/scripts/check-archive-plugin.sh + .github/workflows/scripts/check-archive-plugin.sh check-foundation: name: No dependencies on Foundation diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 2051c091..520c539d 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -12,9 +12,9 @@ jobs: license_header_check_project_name: "SwiftAWSLambdaRuntime" shell_check_enabled: true python_lint_check_enabled: true - api_breakage_check_container_image: "swift:6.0-noble" + api_breakage_check_container_image: "swiftlang/swift:nightly-6.1-jammy" docs_check_container_image: "swift:6.0-noble" - format_check_container_image: "swiftlang/swift:nightly-6.0-jammy" + format_check_container_image: "swiftlang/swift:nightly-6.1-jammy" yamllint_check_enabled: true unit-tests: @@ -36,8 +36,8 @@ 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: "[ 'APIGateway', 'APIGateway+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Testing', 'Tutorial' ]" - + examples: "[ 'APIGateway', 'APIGateway+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Testing', 'Tutorial' ]" + archive_plugin_examples: "[ 'HelloWorld', 'ResourcesPackaging' ]" archive_plugin_enabled: true swift-6-language-mode: diff --git a/.github/workflows/scripts/check-archive-plugin.sh b/.github/workflows/scripts/check-archive-plugin.sh index 3a15c138..3c127be8 100755 --- a/.github/workflows/scripts/check-archive-plugin.sh +++ b/.github/workflows/scripts/check-archive-plugin.sh @@ -13,12 +13,17 @@ ## ##===----------------------------------------------------------------------===## -EXAMPLE=HelloWorld +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { error "$@"; exit 1; } + +test -n "${EXAMPLE:-}" || fatal "EXAMPLE unset" + OUTPUT_DIR=.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager OUTPUT_FILE=${OUTPUT_DIR}/MyLambda/bootstrap ZIP_FILE=${OUTPUT_DIR}/MyLambda/MyLambda.zip -pushd Examples/${EXAMPLE} || exit 1 +pushd "Examples/${EXAMPLE}" || exit 1 # package the example (docker and swift toolchain are installed on the GH runner) LAMBDA_USE_LOCAL_DEPS=../.. swift package archive --allow-network-connections docker || exit 1 @@ -33,5 +38,10 @@ file "${OUTPUT_FILE}" | grep --silent ELF # does the ZIP file contain the bootstrap? unzip -l "${ZIP_FILE}" | grep --silent bootstrap -echo "✅ The archive plugin is OK" +# if EXAMPLE is ResourcesPackaging, check if the ZIP file contains hello.txt +if [ "$EXAMPLE" == "ResourcesPackaging" ]; then + unzip -l "${ZIP_FILE}" | grep --silent hello.txt +fi + +echo "✅ The archive plugin is OK with example ${EXAMPLE}" popd || exit 1 diff --git a/.github/workflows/scripts/integration_tests.sh b/.github/workflows/scripts/integration_tests.sh index cb0e031b..8d11b313 100755 --- a/.github/workflows/scripts/integration_tests.sh +++ b/.github/workflows/scripts/integration_tests.sh @@ -19,6 +19,7 @@ log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } fatal() { error "$@"; exit 1; } +SWIFT_VERSION=$(swift --version) test -n "${SWIFT_VERSION:-}" || fatal "SWIFT_VERSION unset" test -n "${COMMAND:-}" || fatal "COMMAND unset" test -n "${EXAMPLE:-}" || fatal "EXAMPLE unset" diff --git a/.licenseignore b/.licenseignore index db42f1da..d47f45a2 100644 --- a/.licenseignore +++ b/.licenseignore @@ -33,4 +33,5 @@ Package.resolved *.yaml *.yml **/.npmignore -**/*.json \ No newline at end of file +**/*.json +**/*.txt \ No newline at end of file diff --git a/.spi.yml b/.spi.yml index 16792833..9c13e3e4 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,4 +1,4 @@ version: 1 builder: configs: - - documentation_targets: [AWSLambdaRuntimeCore, AWSLambdaRuntime] + - documentation_targets: [AWSLambdaRuntime] diff --git a/Examples/APIGateway+LambdaAuthorizer/Package.swift b/Examples/APIGateway+LambdaAuthorizer/Package.swift index 574bdbbe..03117835 100644 --- a/Examples/APIGateway+LambdaAuthorizer/Package.swift +++ b/Examples/APIGateway+LambdaAuthorizer/Package.swift @@ -5,12 +5,6 @@ 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: [.macOS(.v15)], @@ -21,7 +15,7 @@ let package = Package( 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"), - .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.0.0"), ], targets: [ .executableTarget( diff --git a/Examples/APIGateway/Package.swift b/Examples/APIGateway/Package.swift index df13380d..b2373801 100644 --- a/Examples/APIGateway/Package.swift +++ b/Examples/APIGateway/Package.swift @@ -14,7 +14,7 @@ let package = Package( 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"), - .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.0.0"), ], targets: [ .executableTarget( diff --git a/Examples/CDK/Package.swift b/Examples/CDK/Package.swift index df13380d..b2373801 100644 --- a/Examples/CDK/Package.swift +++ b/Examples/CDK/Package.swift @@ -14,7 +14,7 @@ let package = Package( 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"), - .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.0.0"), ], targets: [ .executableTarget( diff --git a/Examples/CDK/infra/package-lock.json b/Examples/CDK/infra/package-lock.json index f91106b7..75710aae 100644 --- a/Examples/CDK/infra/package-lock.json +++ b/Examples/CDK/infra/package-lock.json @@ -23,9 +23,9 @@ } }, "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.215", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.215.tgz", - "integrity": "sha512-D+Jzwpl+zlBGjJf7nuRcz6JFNwqDQ+IzwIq0VSC4LMRRvrkhGE/ZE+zab3EnjmVkipcQqtQe+PVKefgmxETbvA==", + "version": "2.2.220", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.220.tgz", + "integrity": "sha512-2eXZnnIgwWmXc7eRh8mRKPp6yHTKiQrLziRX/oVSfp4M6Jn2no0QFKJoHWqziF5MDQa5TF8qhD4FGsls/1nYPg==", "license": "Apache-2.0" }, "node_modules/@aws-cdk/asset-kubectl-v20": { @@ -41,16 +41,16 @@ "license": "Apache-2.0" }, "node_modules/@aws-cdk/cloud-assembly-schema": { - "version": "38.0.1", - "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-38.0.1.tgz", - "integrity": "sha512-KvPe+NMWAulfNVwY7jenFhzhuLhLqJ/OPy5jx7wUstbjnYnjRVLpUHPU3yCjXFE0J8cuJVdx95BJ4rOs66Pi9w==", + "version": "39.2.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-39.2.1.tgz", + "integrity": "sha512-cz18QG02p++ivQ5wdjLI8TyMk1p6kwQc1ExeL2A9RoUg1gCnwEBrkJMhiWgTroWBblT0GF0MqSvzkyeEdHqn2A==", "bundleDependencies": [ "jsonschema", "semver" ], "license": "Apache-2.0", "dependencies": { - "jsonschema": "^1.4.1", + "jsonschema": "~1.4.1", "semver": "^7.6.3" } }, @@ -356,9 +356,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.173.2", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.173.2.tgz", - "integrity": "sha512-cL9+z8Pl3VZGoO7BwdsrFAOeud/vSl3at7OvmhihbNprMN15XuFUx/rViAU5OI1m92NbV4NBzYSLbSeCwYLNyw==", + "version": "2.176.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.176.0.tgz", + "integrity": "sha512-6Gs2kBaq4elQ4fNAOiCgbD9oOLx/heb/Lp4OVE6Uf7FulYW0DikWJXxR5GWJslTJ4/sCf3UU91q415fc0bruLg==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -377,7 +377,7 @@ "@aws-cdk/asset-awscli-v1": "^2.2.208", "@aws-cdk/asset-kubectl-v20": "^2.1.3", "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", - "@aws-cdk/cloud-assembly-schema": "^38.0.1", + "@aws-cdk/cloud-assembly-schema": "^39.0.1", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", "fs-extra": "^11.2.0", diff --git a/Examples/HelloJSON/Package.swift b/Examples/HelloJSON/Package.swift index 506f0678..9f26ff9d 100644 --- a/Examples/HelloJSON/Package.swift +++ b/Examples/HelloJSON/Package.swift @@ -1,25 +1,25 @@ -// swift-tools-version:6.0 +// swift-tools-version:6.1 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, + platforms: [.macOS(.v15)], 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") + .package( + url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + branch: "ff-package-traits", + traits: [ + .trait(name: "FoundationJSONSupport") + ] + ) ], targets: [ .executableTarget( diff --git a/Examples/HelloWorld/README.md b/Examples/HelloWorld/README.md index 73b51595..c4eacfc2 100644 --- a/Examples/HelloWorld/README.md +++ b/Examples/HelloWorld/README.md @@ -12,6 +12,35 @@ The handler is `(event: String, context: LambdaContext)`. The function takes two The function return value will be encoded as your Lambda function response. +## Test locally + +You can test your function locally before deploying it to AWS Lambda. + +To start the local function, type the following commands: + +```bash +swift run +``` + +It will compile your code and start the local server. You know the local server is ready to accept connections when you see this message. + +```txt +Building for debugging... +[1/1] Write swift-version--644A47CB88185983.txt +Build of product 'MyLambda' complete! (0.31s) +2025-01-29T12:44:48+0100 info LocalServer : host="127.0.0.1" port=7000 [AWSLambdaRuntime] Server started and listening +``` + +Then, from another Terminal, send your payload with `curl`. Note that the payload must be a valid JSON string. In the case of this function that accepts a simple String, it means the String must be wrapped in between double quotes. + +```bash +curl -d '"seb"' http://127.0.0.1:7000/invoke +"Hello seb" +``` + +> [!IMPORTANT] +> The local server is only available in `DEBUG` mode. It will not start with `swift -c release run`. + ## Build & Package To build & archive the package, type the following commands. diff --git a/Examples/README.md b/Examples/README.md index 53ccc8bd..973df897 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -28,6 +28,8 @@ This directory contains example code for Lambda functions. - **[HelloWorld](HelloWorld/README.md)**: a simple Lambda function (requires [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)). +- **[S3EventNotifier](S3EventNotifier/README.md)**: a Lambda function that receives object-upload notifications from an [Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html) bucket. + - **[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/)). - **[S3_Soto](S3_Soto/README.md)**: a Lambda function that uses [Soto](https://github.com/soto-project/soto) 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/)). @@ -64,4 +66,4 @@ To obtain these keys, you need an AWS account: 4. **(Optional) Generate Temporary Security Credentials**: If you’re using temporary credentials (which are more secure for short-term access), use AWS Security Token Service (STS). You can call the `GetSessionToken` or `AssumeRole` API to generate temporary credentials, including a session token. -With these in hand, you can use AWS SigV4 to securely sign your requests and interact with AWS services from your Swift app. \ No newline at end of file +With these in hand, you can use AWS SigV4 to securely sign your requests and interact with AWS services from your Swift app. diff --git a/Examples/ResourcesPackaging/.gitignore b/Examples/ResourcesPackaging/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/Examples/ResourcesPackaging/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/ResourcesPackaging/Package.swift b/Examples/ResourcesPackaging/Package.swift new file mode 100644 index 00000000..4680b74a --- /dev/null +++ b/Examples/ResourcesPackaging/Package.swift @@ -0,0 +1,57 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "ResourcesPackaging", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "MyLambda", targets: ["MyLambda"]) + ], + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main") + ], + targets: [ + .executableTarget( + name: "MyLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: ".", + resources: [ + .process("hello.txt") + ] + ) + ] +) + +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/Tests/AWSLambdaTestingTests/LambdaTestRuntimeTests.swift b/Examples/ResourcesPackaging/Sources/main.swift similarity index 56% rename from Tests/AWSLambdaTestingTests/LambdaTestRuntimeTests.swift rename to Examples/ResourcesPackaging/Sources/main.swift index 47d29dfe..dccbd863 100644 --- a/Tests/AWSLambdaTestingTests/LambdaTestRuntimeTests.swift +++ b/Examples/ResourcesPackaging/Sources/main.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright (c) 2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -13,6 +13,14 @@ //===----------------------------------------------------------------------===// import AWSLambdaRuntime -import AWSLambdaTesting -import NIOCore -import Testing +import Foundation + +let runtime = LambdaRuntime { + (event: String, context: LambdaContext) in + guard let fileURL = Bundle.module.url(forResource: "hello", withExtension: "txt") else { + fatalError("no file url") + } + return try String(contentsOf: fileURL, encoding: .utf8) +} + +try await runtime.run() diff --git a/Examples/ResourcesPackaging/hello.txt b/Examples/ResourcesPackaging/hello.txt new file mode 100644 index 00000000..557db03d --- /dev/null +++ b/Examples/ResourcesPackaging/hello.txt @@ -0,0 +1 @@ +Hello World diff --git a/Examples/S3EventNotifier/.gitignore b/Examples/S3EventNotifier/.gitignore new file mode 100644 index 00000000..10edc03d --- /dev/null +++ b/Examples/S3EventNotifier/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/.index-build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/S3EventNotifier/Package.swift b/Examples/S3EventNotifier/Package.swift new file mode 100644 index 00000000..6554b385 --- /dev/null +++ b/Examples/S3EventNotifier/Package.swift @@ -0,0 +1,50 @@ +// swift-tools-version: 6.0 +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "S3EventNotifier", + platforms: [.macOS(.v15)], + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events", branch: "main"), + ], + targets: [ + .executableTarget( + name: "S3EventNotifier", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ] + ) + ] +) + +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/S3EventNotifier/README.md b/Examples/S3EventNotifier/README.md new file mode 100644 index 00000000..3ccee239 --- /dev/null +++ b/Examples/S3EventNotifier/README.md @@ -0,0 +1,94 @@ +# S3 Event Notifier + +This example demonstrates how to write a Lambda that is invoked by an event originating from Amazon S3, such as a new object being uploaded to a bucket. + +## Code + +In this example the Lambda function receives an `S3Event` object defined in the `AWSLambdaEvents` library as input object. The `S3Event` object contains all the information about the S3 event that triggered the function, but what we are interested in is the bucket name and the object key, which are inside of a notification `Record`. The object contains an array of records, however since the Lambda function is triggered by a single event, we can safely assume that there is only one record in the array: the first one. Inside of this record, we can find the bucket name and the object key: + +```swift +guard let s3NotificationRecord = event.records.first else { + throw LambdaError.noNotificationRecord +} + +let bucket = s3NotificationRecord.s3.bucket.name +let key = s3NotificationRecord.s3.object.key.replacingOccurrences(of: "+", with: " ") +``` + +The key is URL encoded, so we replace the `+` with a space. + +## Build & Package + +To build & archive the package you can use the following commands: + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there are no errors, a ZIP file should be ready to deploy, located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/S3EventNotifier/S3EventNotifier.zip`. + +## Deploy + +> [!IMPORTANT] +> The Lambda function and the S3 bucket must be located in the same AWS Region. In the code below, we use `eu-west-1` (Ireland). + +To deploy the Lambda function, you can use the `aws` command line: + +```bash +REGION=eu-west-1 +aws lambda create-function \ + --region "${REGION}" \ + --function-name S3EventNotifier \ + --zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/S3EventNotifier/S3EventNotifier.zip \ + --runtime provided.al2 \ + --handler provided \ + --architectures arm64 \ + --role arn:aws:iam:::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 `REGION` with the region where you want to deploy your Lambda function and replace `` with your actual AWS account ID (for example: 012345678901). + +Besides deploying the Lambda function you also need to create the S3 bucket and configure it to send events to the Lambda function. You can do this using the following commands: + +```bash +REGION=eu-west-1 + +aws s3api create-bucket \ + --region "${REGION}" \ + --bucket my-test-bucket \ + --create-bucket-configuration LocationConstraint="${REGION}" + +aws lambda add-permission \ + --region "${REGION}" \ + --function-name S3EventNotifier \ + --statement-id S3InvokeFunction \ + --action lambda:InvokeFunction \ + --principal s3.amazonaws.com \ + --source-arn arn:aws:s3:::my-test-bucket + +aws s3api put-bucket-notification-configuration \ + --region "${REGION}" \ + --bucket my-test-bucket \ + --notification-configuration '{ + "LambdaFunctionConfigurations": [{ + "LambdaFunctionArn": "arn:aws:lambda:${REGION}::function:S3EventNotifier", + "Events": ["s3:ObjectCreated:*"] + }] + }' + +touch testfile.txt && aws s3 cp testfile.txt s3://my-test-bucket/ +``` + +This will: + - create a bucket named `my-test-bucket` in the `$REGION` region; + - add a permission to the Lambda function to be invoked by Amazon S3; + - configure the bucket to send `s3:ObjectCreated:*` events to the Lambda function named `S3EventNotifier`; + - upload a file named `testfile.txt` to the bucket. + +Replace `my-test-bucket` with your bucket name (bucket names are unique globaly and this one is already taken). Also replace `REGION` environment variable with the AWS Region where you deployed the Lambda function and `` with your actual AWS account ID. + +> [!IMPORTANT] +> The Lambda function and the S3 bucket must be located in the same AWS Region. Adjust the code above according to your closest AWS Region. \ No newline at end of file diff --git a/Examples/S3EventNotifier/Sources/main.swift b/Examples/S3EventNotifier/Sources/main.swift new file mode 100644 index 00000000..9a55974e --- /dev/null +++ b/Examples/S3EventNotifier/Sources/main.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2025 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 AWSLambdaEvents +import AWSLambdaRuntime +import Foundation + +let runtime = LambdaRuntime { (event: S3Event, context: LambdaContext) async throws in + guard let s3NotificationRecord = event.records.first else { + context.logger.error("No S3 notification record found in the event") + return + } + + let bucket = s3NotificationRecord.s3.bucket.name + let key = s3NotificationRecord.s3.object.key.replacingOccurrences(of: "+", with: " ") + + context.logger.info("Received notification from S3 bucket '\(bucket)' for object with key '\(key)'") + + // Here you could, for example, notify an API or a messaging service +} + +try await runtime.run() diff --git a/Examples/S3_AWSSDK/Package.swift b/Examples/S3_AWSSDK/Package.swift index fa1426fd..0eec7c6b 100644 --- a/Examples/S3_AWSSDK/Package.swift +++ b/Examples/S3_AWSSDK/Package.swift @@ -14,7 +14,7 @@ let package = Package( 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"), - .package(url: "https://github.com/swift-server/swift-aws-lambda-events", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events", from: "1.0.0"), .package(url: "https://github.com/awslabs/aws-sdk-swift", from: "1.0.0"), ], targets: [ diff --git a/Examples/S3_Soto/Package.swift b/Examples/S3_Soto/Package.swift index 25b891a8..97e5a9fb 100644 --- a/Examples/S3_Soto/Package.swift +++ b/Examples/S3_Soto/Package.swift @@ -16,7 +16,7 @@ let package = Package( // 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"), - .package(url: "https://github.com/swift-server/swift-aws-lambda-events", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events", from: "1.0.0"), ], targets: [ .executableTarget( diff --git a/Examples/Testing/Package.swift b/Examples/Testing/Package.swift index 79aab087..db196325 100644 --- a/Examples/Testing/Package.swift +++ b/Examples/Testing/Package.swift @@ -14,7 +14,7 @@ let package = Package( 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"), - .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.0.0"), ], targets: [ .executableTarget( diff --git a/Examples/Testing/README.md b/Examples/Testing/README.md index 3f911b21..9bfd0e28 100644 --- a/Examples/Testing/README.md +++ b/Examples/Testing/README.md @@ -160,8 +160,8 @@ You must pass an event to the Lambda function. You can use the `event.json` file sam local invoke -e Tests/event.json START RequestId: 3270171f-46d3-45f9-9bb6-3c2e5e9dc625 Version: $LATEST -2024-12-21T16:49:31+0000 debug LambdaRuntime : [AWSLambdaRuntimeCore] LambdaRuntime initialized -2024-12-21T16:49:31+0000 trace LambdaRuntime : lambda_ip=127.0.0.1 lambda_port=9001 [AWSLambdaRuntimeCore] Connection to control plane created +2024-12-21T16:49:31+0000 debug LambdaRuntime : [AWSLambdaRuntime] LambdaRuntime initialized +2024-12-21T16:49:31+0000 trace LambdaRuntime : lambda_ip=127.0.0.1 lambda_port=9001 [AWSLambdaRuntime] Connection to control plane created 2024-12-21T16:49:31+0000 debug LambdaRuntime : [APIGatewayLambda] HTTP API Message received 2024-12-21T16:49:31+0000 trace LambdaRuntime : [APIGatewayLambda] Event: APIGatewayV2Request(version: "2.0", routeKey: "$default", rawPath: "/", rawQueryString: "", cookies: [], headers: ["x-forwarded-proto": "https", "host": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", "content-length": "0", "x-forwarded-for": "81.0.0.43", "accept": "*/*", "x-amzn-trace-id": "Root=1-66fb03de-07533930192eaf5f540db0cb", "x-forwarded-port": "443", "user-agent": "curl/8.7.1"], queryStringParameters: [:], pathParameters: [:], context: AWSLambdaEvents.APIGatewayV2Request.Context(accountId: "012345678901", apiId: "a5q74es3k2", domainName: "a5q74es3k2.execute-api.us-east-1.amazonaws.com", domainPrefix: "a5q74es3k2", stage: "$default", requestId: "e72KxgsRoAMEMSA=", http: AWSLambdaEvents.APIGatewayV2Request.Context.HTTP(method: GET, path: "/", protocol: "HTTP/1.1", sourceIp: "81.0.0.43", userAgent: "curl/8.7.1"), authorizer: nil, authentication: nil, time: "30/Sep/2024:20:02:38 +0000", timeEpoch: 1727726558220), stageVariables: [:], body: Optional("aGVsbG8gd29ybGQgb2YgU1dJRlQgTEFNQkRBIQ=="), isBase64Encoded: false) END RequestId: 5b71587a-39da-445e-855d-27a700e57efd diff --git a/Examples/Testing/Tests/HandlerTests.swift b/Examples/Testing/Tests/HandlerTests.swift index 7fa245f9..85cc4e4e 100644 --- a/Examples/Testing/Tests/HandlerTests.swift +++ b/Examples/Testing/Tests/HandlerTests.swift @@ -18,7 +18,7 @@ import Logging import Testing @testable import APIGatewayLambda // to access the business code -@testable import AWSLambdaRuntimeCore // to access the LambdaContext +@testable import AWSLambdaRuntime // to access the LambdaContext #if canImport(FoundationEssentials) import FoundationEssentials diff --git a/Package.swift b/Package.swift index d806277a..b83b04c8 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:6.0 +// swift-tools-version:6.1 import PackageDescription @@ -6,17 +6,8 @@ let package = Package( name: "swift-aws-lambda-runtime", platforms: [.macOS(.v15)], products: [ - - // - // The runtime library targets - // - - // this library exports `AWSLambdaRuntimeCore` and adds Foundation convenience methods .library(name: "AWSLambdaRuntime", targets: ["AWSLambdaRuntime"]), - // this has all the main functionality for lambda and it does not link Foundation - .library(name: "AWSLambdaRuntimeCore", targets: ["AWSLambdaRuntimeCore"]), - // // The plugins // 'lambda-init' creates a new Lambda function @@ -34,36 +25,40 @@ let package = Package( // plugin to deploy a Lambda function .plugin(name: "AWSLambdaDeployer", targets: ["AWSLambdaDeployer"]), - - // - // Testing targets - // - - // for testing only - .library(name: "AWSLambdaTesting", targets: ["AWSLambdaTesting"]), + ], + traits: [ + "FoundationJSONSupport", + "ServiceLifecycleSupport", + "LocalServerSupport", + .default( + enabledTraits: [ + "FoundationJSONSupport", + "ServiceLifecycleSupport", + "LocalServerSupport", + ] + ), ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", from: "2.76.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.4"), + .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.6.3"), ], targets: [ .target( name: "AWSLambdaRuntime", dependencies: [ - .byName(name: "AWSLambdaRuntimeCore"), .product(name: "NIOCore", package: "swift-nio"), - ] - ), - .target( - name: "AWSLambdaRuntimeCore", - dependencies: [ + .product(name: "DequeModule", package: "swift-collections"), .product(name: "Logging", package: "swift-log"), .product(name: "NIOHTTP1", package: "swift-nio"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), - ], - swiftSettings: [.swiftLanguageMode(.v5)] + .product( + name: "ServiceLifecycle", + package: "swift-service-lifecycle", + condition: .when(traits: ["ServiceLifecycleSupport"]) + ), + ] ), .plugin( name: "AWSLambdaInitializer", @@ -146,45 +141,23 @@ let package = Package( ], swiftSettings: [.swiftLanguageMode(.v6)] ), - .testTarget( - name: "AWSLambdaRuntimeCoreTests", - dependencies: [ - .byName(name: "AWSLambdaRuntimeCore"), - .product(name: "NIOTestUtils", package: "swift-nio"), - .product(name: "NIOFoundationCompat", package: "swift-nio"), - ] - ), .testTarget( name: "AWSLambdaRuntimeTests", dependencies: [ - .byName(name: "AWSLambdaRuntimeCore"), .byName(name: "AWSLambdaRuntime"), - ] - ), - // testing helper - .target( - name: "AWSLambdaTesting", - dependencies: [ - .byName(name: "AWSLambdaRuntime"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOPosix", package: "swift-nio"), - ] - ), - .testTarget( - name: "AWSLambdaTestingTests", - dependencies: [ - .byName(name: "AWSLambdaTesting") + .product(name: "NIOTestUtils", package: "swift-nio"), + .product(name: "NIOFoundationCompat", package: "swift-nio"), ] ), // for perf testing .executableTarget( name: "MockServer", dependencies: [ + .product(name: "Logging", package: "swift-log"), .product(name: "NIOHTTP1", package: "swift-nio"), .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), - ], - swiftSettings: [.swiftLanguageMode(.v5)] + ] ), .testTarget( name: "AWSLambdaPluginHelperTests", diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift new file mode 100644 index 00000000..a9f4892f --- /dev/null +++ b/Package@swift-6.0.swift @@ -0,0 +1,152 @@ +// swift-tools-version:6.0 + +import PackageDescription + +let package = Package( + name: "swift-aws-lambda-runtime", + platforms: [.macOS(.v15)], + products: [ + .library(name: "AWSLambdaRuntime", targets: ["AWSLambdaRuntime"]), + + // + // The plugins + // 'lambda-init' creates a new Lambda function + // 'lambda-build' packages the Lambda function + // 'lambda-deploy' deploys the Lambda function + // + // Plugins requires Linux or at least macOS v15 + // + + // plugin to create a new Lambda function, based on a template + .plugin(name: "AWSLambdaInitializer", targets: ["AWSLambdaInitializer"]), + + // plugin to package the lambda, creating an archive that can be uploaded to AWS + .plugin(name: "AWSLambdaBuilder", targets: ["AWSLambdaBuilder"]), + + // plugin to deploy a Lambda function + .plugin(name: "AWSLambdaDeployer", targets: ["AWSLambdaDeployer"]), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.4"), + .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.6.3"), + ], + targets: [ + .target( + name: "AWSLambdaRuntime", + dependencies: [ + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "DequeModule", package: "swift-collections"), + .product(name: "Logging", package: "swift-log"), + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "ServiceLifecycle", package: "swift-service-lifecycle"), + ], + swiftSettings: [ + .define("FoundationJSONSupport"), + .define("ServiceLifecycleSupport"), + .define("LocalServerSupport"), + ] + ), + .plugin( + name: "AWSLambdaInitializer", + capability: .command( + intent: .custom( + verb: "lambda-init", + description: + "Create a new Lambda function in the current project directory." + ), + permissions: [ + .writeToPackageDirectory(reason: "Create a file with an HelloWorld Lambda function.") + ] + ), + dependencies: [ + .target(name: "AWSLambdaPluginHelper") + ] + ), + // keep this one (with "archive") to not break workflows + // This will be deprecated at some point in the future + // .plugin( + // name: "AWSLambdaPackager", + // capability: .command( + // intent: .custom( + // verb: "archive", + // description: + // "Archive the Lambda binary and prepare it for uploading to AWS. Requires docker on macOS or non Amazonlinux 2 distributions." + // ), + // permissions: [ + // .allowNetworkConnections( + // scope: .docker, + // reason: "This plugin uses Docker to create the AWS Lambda ZIP package." + // ) + // ] + // ), + // path: "Plugins/AWSLambdaBuilder" // same sources as the new "lambda-build" plugin + // ), + .plugin( + name: "AWSLambdaBuilder", + capability: .command( + intent: .custom( + verb: "lambda-build", + description: + "Archive the Lambda binary and prepare it for uploading to AWS. Requires docker on macOS or non Amazonlinux 2 distributions." + ), + permissions: [ + .allowNetworkConnections( + scope: .docker, + reason: "This plugin uses Docker to create the AWS Lambda ZIP package." + ) + ] + ), + dependencies: [ + .target(name: "AWSLambdaPluginHelper") + ] + ), + .plugin( + name: "AWSLambdaDeployer", + capability: .command( + intent: .custom( + verb: "lambda-deploy", + description: + "Deploy the Lambda function. You must have an AWS account and know an access key and secret access key." + ), + permissions: [ + .allowNetworkConnections( + scope: .all(ports: [443]), + reason: "This plugin uses the AWS Lambda API to deploy the function." + ) + ] + ), + dependencies: [ + .target(name: "AWSLambdaPluginHelper") + ] + ), + .executableTarget( + name: "AWSLambdaPluginHelper", + dependencies: [ + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "NIOCore", package: "swift-nio"), + ], + swiftSettings: [.swiftLanguageMode(.v6)] + ), + .testTarget( + name: "AWSLambdaRuntimeTests", + dependencies: [ + .byName(name: "AWSLambdaRuntime"), + .product(name: "NIOTestUtils", package: "swift-nio"), + .product(name: "NIOFoundationCompat", package: "swift-nio"), + ] + ), + // for perf testing + .executableTarget( + name: "MockServer", + dependencies: [ + .product(name: "Logging", package: "swift-log"), + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + ] + ), + ] +) diff --git a/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift b/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift index acd01c28..d0eca2a2 100644 --- a/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift @@ -201,11 +201,29 @@ struct Builder { let resourcesDirectoryName = artifactURL.lastPathComponent let relocatedResourcesDirectory = workingDirectory.appending(path: resourcesDirectoryName) if FileManager.default.fileExists(atPath: artifactURL.path()) { - try FileManager.default.copyItem( - atPath: artifactURL.path(), - toPath: relocatedResourcesDirectory.path() - ) - arguments.append(resourcesDirectoryName) + do { + try FileManager.default.copyItem( + atPath: artifactURL.path(), + toPath: relocatedResourcesDirectory.path() + ) + arguments.append(resourcesDirectoryName) + } catch let error as CocoaError { + + // On Linux, when the build has been done with Docker, + // the source file are owned by root + // this causes a permission error **after** the files have been copied + // see https://github.com/swift-server/swift-aws-lambda-runtime/issues/449 + // see https://forums.swift.org/t/filemanager-copyitem-on-linux-fails-after-copying-the-files/77282 + + // because this error happens after the files have been copied, we can ignore it + // this code checks if the destination file exists + // if they do, just ignore error, otherwise throw it up to the caller. + if !(error.code == CocoaError.Code.fileWriteNoPermission + && FileManager.default.fileExists(atPath: relocatedResourcesDirectory.path())) + { + throw error + } // else just ignore it + } } } @@ -231,7 +249,7 @@ struct Builder { USAGE: swift package --allow-network-connections docker archive [--help] [--verbose] - [--output-directory ] + [--output-path ] [--products ] [--configuration debug | release] [--swift-version ] @@ -241,7 +259,7 @@ struct Builder { OPTIONS: --verbose Produce verbose output for debugging. - --output-directory The path of the binary package. + --output-path The path of the binary package. (default is `.build/plugins/AWSLambdaPackager/outputs/...`) --products The list of executable targets to build. (default is taken from Package.swift) diff --git a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift b/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift rename to Sources/AWSLambdaRuntime/ControlPlaneRequest.swift diff --git a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequestEncoder.swift b/Sources/AWSLambdaRuntime/ControlPlaneRequestEncoder.swift similarity index 97% rename from Sources/AWSLambdaRuntimeCore/ControlPlaneRequestEncoder.swift rename to Sources/AWSLambdaRuntime/ControlPlaneRequestEncoder.swift index 31e64d27..5848ec76 100644 --- a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequestEncoder.swift +++ b/Sources/AWSLambdaRuntime/ControlPlaneRequestEncoder.swift @@ -93,7 +93,8 @@ struct ControlPlaneRequestEncoder: _EmittingChannelHandler { extension String { static let CRLF: String = "\r\n" - static let userAgentHeader: String = "user-agent: Swift-Lambda/Unknown\r\n" + static let userAgent = "Swift-Lambda/Unknown" + static let userAgentHeader: String = "user-agent: \(userAgent)\r\n" static let unhandledErrorHeader: String = "lambda-runtime-function-error-type: Unhandled\r\n" static let nextInvocationRequestLine: String = diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Deployment.md b/Sources/AWSLambdaRuntime/Docs.docc/Deployment.md similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Deployment.md rename to Sources/AWSLambdaRuntime/Docs.docc/Deployment.md diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Documentation.md b/Sources/AWSLambdaRuntime/Docs.docc/Documentation.md similarity index 98% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Documentation.md rename to Sources/AWSLambdaRuntime/Docs.docc/Documentation.md index 4016bcbd..e820ce26 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Documentation.md +++ b/Sources/AWSLambdaRuntime/Docs.docc/Documentation.md @@ -1,4 +1,4 @@ -# ``AWSLambdaRuntimeCore`` +# ``AWSLambdaRuntime`` An AWS Lambda runtime for the Swift programming language diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md b/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md similarity index 99% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md rename to Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md index e4ff259b..0396d48d 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md +++ b/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md @@ -40,7 +40,7 @@ Server ecosystem is to shift to newer, more readable, Swift concurrency construc #### No ownership of the main() function A Lambda function can currently be implemented through conformance to the various handler protocols defined in -`AWSLambdaRuntimeCore/LambdaHandler`. Each of these protocols have an extension which implements a `static func main()`. +``AWSLambdaRuntime/LambdaHandler``. Each of these protocols have an extension which implements a `static func main()`. This allows users to annotate their `LambdaHandler` conforming object with `@main`. The `static func main()` calls the internal `Lambda.run()` function, which starts the Lambda function. Since the `Lambda.run()` method is internal, users cannot override the default implementation. This has proven challenging for users who want to @@ -80,7 +80,7 @@ initialized services: struct MyLambda: LambdaHandler { let pgClient: PostgresClient - init(context: AWSLambdaRuntimeCore.LambdaInitializationContext) async throws { + init(context: AWSLambdaRuntime.LambdaInitializationContext) async throws { /// Instantiate service let client = PostgresClient(configuration: ...) diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/.shellcheckrc b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/.shellcheckrc similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/.shellcheckrc rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/.shellcheckrc diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-01-package-init.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-01-package-init.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-01-package-init.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-01-package-init.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-02-package-init.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-02-package-init.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-02-package-init.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-02-package-init.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-03-package-init.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-03-package-init.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-03-package-init.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-03-package-init.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-04-package-init.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-04-package-init.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-04-package-init.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-04-package-init.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-01-package.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-01-package.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-01-package.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-01-package.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-02-package.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-02-package.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-02-package.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-02-package.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-03-package.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-03-package.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-03-package.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-03-package.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-04-package.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-04-package.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-04-package.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-04-package.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-05-package.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-05-package.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-05-package.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-05-package.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-01-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-01-main.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-01-main.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-01-main.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-02-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-02-main.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-02-main.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-02-main.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-03-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-03-main.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-03-main.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-03-main.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-04-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-04-main.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-04-main.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-04-main.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-05-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-05-main.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-05-main.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-05-main.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-06-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-06-main.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-06-main.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-06-main.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-07-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-07-main.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-07-main.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-07-main.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-02-console-output.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-02-console-output.sh similarity index 93% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-02-console-output.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-02-console-output.sh index 3d862f5d..11c59fb9 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-02-console-output.sh +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-02-console-output.sh @@ -1,2 +1,2 @@ -2025-01-02T14:59:29+0100 info LocalLambdaServer : [AWSLambdaRuntimeCore] +2025-01-02T14:59:29+0100 info LocalLambdaServer : [AWSLambdaRuntime] LocalLambdaServer started and listening on 127.0.0.1:7000, receiving events on /invoke diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-03-curl.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-03-curl.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-03-curl.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-03-curl.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-04-curl.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-04-curl.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-04-curl.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-04-curl.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-06-terminal.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-06-terminal.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-06-terminal.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-06-terminal.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-07-terminal.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-07-terminal.sh similarity index 67% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-07-terminal.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-07-terminal.sh index 6e4d6a3d..1348bddc 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-07-terminal.sh +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-07-terminal.sh @@ -3,4 +3,4 @@ swift run Building for debugging... [1/1] Write swift-version--58304C5D6DBC2206.txt Build of product 'PalindromeLambda' complete! (0.11s) -2025-01-02T15:12:49+0100 info LocalLambdaServer : [AWSLambdaRuntimeCore] LocalLambdaServer started and listening on 127.0.0.1:7000, receiving events on /invoke +2025-01-02T15:12:49+0100 info LocalLambdaServer : [AWSLambdaRuntime] LocalLambdaServer started and listening on 127.0.0.1:7000, receiving events on /invoke diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-02-plugin-archive.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-02-plugin-archive.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-02-plugin-archive.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-02-plugin-archive.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-03-plugin-archive.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-03-plugin-archive.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-03-plugin-archive.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-03-plugin-archive.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-04-plugin-archive.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-04-plugin-archive.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-04-plugin-archive.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-04-plugin-archive.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-01-aws-cli.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-01-aws-cli.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-01-aws-cli.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-01-aws-cli.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-02-lambda-invoke-hidden.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-02-lambda-invoke-hidden.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-02-lambda-invoke-hidden.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-02-lambda-invoke-hidden.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-02-lambda-invoke.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-02-lambda-invoke.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-02-lambda-invoke.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-02-lambda-invoke.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-03-lambda-invoke.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-03-lambda-invoke.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-03-lambda-invoke.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-03-lambda-invoke.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-04-lambda-invoke.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-04-lambda-invoke.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-04-lambda-invoke.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-04-lambda-invoke.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-05-lambda-invoke.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-05-lambda-invoke.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-05-lambda-invoke.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-05-lambda-invoke.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-10-regions.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-10-regions.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-10-regions.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-10-regions.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-20-dashboard.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-20-dashboard.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-20-dashboard.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-20-dashboard.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-30-create-function.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-30-create-function.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-30-create-function.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-30-create-function.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-40-select-zip-file.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-40-select-zip-file.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-40-select-zip-file.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-40-select-zip-file.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-50-upload-zip.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-50-upload-zip.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-50-upload-zip.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-50-upload-zip.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-60-prepare-test-event.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-60-prepare-test-event.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-60-prepare-test-event.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-60-prepare-test-event.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-70-view-invocation-response.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-70-view-invocation-response.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-70-view-invocation-response.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-70-view-invocation-response.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-80-delete-function.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-function.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-80-delete-function.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-function.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-80-delete-role.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-role.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-80-delete-role.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-role.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/00-swift_on_lambda.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/00-swift_on_lambda.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/00-swift_on_lambda.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/00-swift_on_lambda.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/01-swift_on_lambda.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/01-swift_on_lambda.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/01-swift_on_lambda.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/01-swift_on_lambda.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-terminal-package-init.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-terminal-package-init.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-terminal-package-init.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-terminal-package-init.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-xcode@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-xcode@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-xcode~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode~dark@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-xcode~dark@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode~dark@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-02-swift-package-manager.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-02-swift-package-manager.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-02-swift-package-manager.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-02-swift-package-manager.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-03-swift-code-xcode.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-03-swift-code-xcode.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-03-swift-code-xcode.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-03-swift-code-xcode.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-01-compile-run@2x.png.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-01-compile-run@2x.png.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-01-compile-run@2x.png.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-01-compile-run@2x.png.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-01-compile-run~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-01-compile-run~dark@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-01-compile-run~dark@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-01-compile-run~dark@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-test-locally.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-test-locally.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-test-locally.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-test-locally.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-swift_on_lambda.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-swift_on_lambda.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-swift_on_lambda.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-swift_on_lambda.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-01-01-docker-started@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-01-01-docker-started@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-01-01-docker-started@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-01-01-docker-started@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-01-compile-for-linux.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-01-compile-for-linux.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-01-compile-for-linux.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-01-compile-for-linux.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-01-console-login@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-01-console-login@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-01-console-login@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-01-console-login@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-02-console-login@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-02-console-login@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-02-console-login@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-02-console-login@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-03-select-region@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-03-select-region@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-03-select-region@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-03-select-region@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-04-select-lambda@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-04-select-lambda@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-04-select-lambda@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-04-select-lambda@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-04-select-lambda~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-04-select-lambda~dark@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-04-select-lambda~dark@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-04-select-lambda~dark@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-05-create-function@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-05-create-function@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-05-create-function@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-05-create-function@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-05-create-function~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-05-create-function~dark@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-05-create-function~dark@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-05-create-function~dark@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-06-create-function@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-06-create-function@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-06-create-function@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-06-create-function@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-06-create-function~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-06-create-function~dark@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-06-create-function~dark@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-06-create-function~dark@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-07-upload-zip@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-07-upload-zip@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-07-upload-zip@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-07-upload-zip@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-07-upload-zip~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-07-upload-zip~dark@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-07-upload-zip~dark@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-07-upload-zip~dark@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-08-upload-zip@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-08-upload-zip@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-08-upload-zip@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-08-upload-zip@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-08-upload-zip~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-08-upload-zip~dark@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-08-upload-zip~dark@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-08-upload-zip~dark@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-09-test-lambda@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-09-test-lambda@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-09-test-lambda@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-09-test-lambda@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-09-test-lambda~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-09-test-lambda~dark@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-09-test-lambda~dark@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-09-test-lambda~dark@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-10-test-lambda-result@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-10-test-lambda-result@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-10-test-lambda-result@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-10-test-lambda-result@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-10-test-lambda-result~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-10-test-lambda-result~dark@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-10-test-lambda-result~dark@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-10-test-lambda-result~dark@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-create-lambda.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-create-lambda.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-create-lambda.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-create-lambda.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-03-invoke-lambda.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-03-invoke-lambda.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-03-invoke-lambda.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-03-invoke-lambda.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-swift_on_lambda.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-swift_on_lambda.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-swift_on_lambda.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-swift_on_lambda.png diff --git a/Sources/AWSLambdaRuntime/Docs.docc/index.md b/Sources/AWSLambdaRuntime/Docs.docc/index.md deleted file mode 100644 index 4d7e1607..00000000 --- a/Sources/AWSLambdaRuntime/Docs.docc/index.md +++ /dev/null @@ -1,344 +0,0 @@ -# ``AWSLambdaRuntime`` - -An implementation of the AWS Lambda Runtime API in Swift. - -## Overview - -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. - -## Getting started - -If you have never used AWS Lambda or Docker before, check out this [getting started guide](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/1.0.0-alpha.3/tutorials/table-of-content) which helps you with every step from zero to a running Lambda. - -First, create a SwiftPM project and pull Swift AWS Lambda Runtime as dependency into your project - - ```swift - // swift-tools-version:5.6 - - import PackageDescription - - let package = Package( - name: "my-lambda", - products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), - ], - dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "0.1.0"), - ], - targets: [ - .executableTarget(name: "MyLambda", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - ]), - ] - ) - ``` - -Next, create a `main.swift` and implement your Lambda. - - ### Using Closures - - The simplest way to use `AWSLambdaRuntime` is to pass in a closure, for example: - - ```swift - // Import the module - import AWSLambdaRuntime - - // in this example we are receiving and responding with strings - Lambda.run { (context, name: String, callback: @escaping (Result) -> Void) in - callback(.success("Hello, \(name)")) - } - ``` - - More commonly, the event would be a JSON, which is modeled using `Codable`, for example: - - ```swift - // Import the module - import AWSLambdaRuntime - - // Request, uses Codable for transparent JSON encoding - private struct Request: Codable { - let name: String - } - - // Response, uses Codable for transparent JSON encoding - private struct Response: Codable { - let message: String - } - - // In this example we are receiving and responding with `Codable`. - Lambda.run { (context, request: Request, callback: @escaping (Result) -> Void) in - callback(.success(Response(message: "Hello, \(request.name)"))) - } - ``` - - Since most Lambda functions are triggered by events originating in the AWS platform like `SNS`, `SQS` or `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. For example, handling an `SQS` message: - -First, add a dependency on the event packages: - - ```swift - // swift-tools-version:5.6 - - import PackageDescription - - let package = Package( - name: "my-lambda", - products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), - ], - dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "0.1.0"), - ], - targets: [ - .executableTarget(name: "MyLambda", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime"), - ]), - ] - ) - ``` - - - ```swift - // Import the modules - import AWSLambdaRuntime - import AWSLambdaEvents - - // In this example we are receiving an SQS Event, with no response (Void). - Lambda.run { (context, message: SQS.Event, callback: @escaping (Result) -> Void) in - ... - callback(.success(Void())) - } - ``` - - Modeling Lambda functions as Closures is both simple and safe. Swift AWS Lambda Runtime will ensure that the user-provided code is offloaded from the network processing thread such that even if the code becomes slow to respond or gets stuck, the underlying process can continue to function. This safety comes at a small performance penalty from context switching between threads. In many cases, the simplicity and safety of using the Closure based API is often preferred over the complexity of the performance-oriented API. - - -## Deploying to AWS Lambda - -To deploy Lambda functions to AWS Lambda, you need to compile the code for Amazon Linux which is the OS used on AWS Lambda microVMs, package it as a Zip file, and upload to AWS. - -AWS offers several tools to interact and deploy Lambda functions to AWS Lambda including [SAM](https://aws.amazon.com/serverless/sam/) and the [AWS CLI](https://aws.amazon.com/cli/). - -To build the Lambda function for Amazon Linux, use the Docker image published by Swift.org on [Swift toolchains and Docker images for Amazon Linux 2](https://swift.org/download/). - -## Architecture - -The library defines three protocols for the implementation of a Lambda Handler. From low-level to more convenient: - -### ByteBufferLambdaHandler - -An `EventLoopFuture` based processing protocol for a Lambda that takes a `ByteBuffer` and returns a `ByteBuffer?` asynchronously. - -[`ByteBufferLambdaHandler`][bblh] is the lowest level protocol designed to power the higher level [`EventLoopLambdaHandler`][ellh] and [`LambdaHandler`][lh] based APIs. Users are not expected to use this protocol, though some performance sensitive applications that operate at the `ByteBuffer` level or have special serialization needs may choose to do so. - -```swift -public protocol ByteBufferLambdaHandler { - /// The Lambda handling method - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - context: Runtime `Context`. - /// - event: The event or request payload encoded as `ByteBuffer`. - /// - /// - Returns: An `EventLoopFuture` to report the result of the Lambda back to the runtime engine. - /// The `EventLoopFuture` should be completed with either a response encoded as `ByteBuffer` or an `Error` - func handle(context: Lambda.Context, event: ByteBuffer) -> EventLoopFuture -} -``` - -### EventLoopLambdaHandler - -[`EventLoopLambdaHandler`][ellh] is a strongly typed, `EventLoopFuture` based asynchronous processing protocol for a Lambda that takes a user defined `In` and returns a user defined `Out`. - -[`EventLoopLambdaHandler`][ellh] extends [`ByteBufferLambdaHandler`][bblh], providing `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer?` encoding for `Codable` and `String`. - -[`EventLoopLambdaHandler`][ellh] executes the user provided Lambda on the same `EventLoop` as the core runtime engine, making the processing fast but requires more care from the implementation to never block the `EventLoop`. It is designed for performance sensitive applications that use `Codable` or `String` based Lambda functions. - -```swift -public protocol EventLoopLambdaHandler: ByteBufferLambdaHandler { - associatedtype In - associatedtype Out - - /// The Lambda handling method - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - context: Runtime `Context`. - /// - event: Event of type `In` representing the event or request. - /// - /// - Returns: An `EventLoopFuture` to report the result of the Lambda back to the runtime engine. - /// The `EventLoopFuture` should be completed with either a response of type `Out` or an `Error` - func handle(context: Lambda.Context, event: In) -> EventLoopFuture - - /// Encode a response of type `Out` to `ByteBuffer` - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - parameters: - /// - allocator: A `ByteBufferAllocator` to help allocate the `ByteBuffer`. - /// - value: Response of type `Out`. - /// - /// - Returns: A `ByteBuffer` with the encoded version of the `value`. - func encode(allocator: ByteBufferAllocator, value: Out) throws -> ByteBuffer? - - /// Decode a`ByteBuffer` to a request or event of type `In` - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - /// - parameters: - /// - buffer: The `ByteBuffer` to decode. - /// - /// - Returns: A request or event of type `In`. - func decode(buffer: ByteBuffer) throws -> In -} -``` - -### LambdaHandler - -[`LambdaHandler`][lh] is a strongly typed, completion handler based asynchronous processing protocol for a Lambda that takes a user defined `In` and returns a user defined `Out`. - -[`LambdaHandler`][lh] extends [`ByteBufferLambdaHandler`][bblh], performing `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer` encoding for `Codable` and `String`. - -[`LambdaHandler`][lh] offloads the user provided Lambda execution to a `DispatchQueue` making processing safer but slower. - -```swift -public protocol LambdaHandler: EventLoopLambdaHandler { - /// Defines to which `DispatchQueue` the Lambda execution is offloaded to. - var offloadQueue: DispatchQueue { get } - - /// The Lambda handling method - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - context: Runtime `Context`. - /// - event: Event of type `In` representing the event or request. - /// - callback: Completion handler to report the result of the Lambda back to the runtime engine. - /// The completion handler expects a `Result` with either a response of type `Out` or an `Error` - func handle(context: Lambda.Context, event: In, callback: @escaping (Result) -> Void) -} -``` - -### Closures - -In addition to protocol-based Lambda, the library provides support for Closure-based ones, as demonstrated in the overview section above. Closure-based Lambdas are based on the [`LambdaHandler`][lh] protocol which mean they are safer. For most use cases, Closure-based Lambda is a great fit and users are encouraged to use them. - -The library includes implementations for `Codable` and `String` based Lambda. Since AWS Lambda is primarily JSON based, this covers the most common use cases. - -```swift -public typealias CodableClosure = (Lambda.Context, In, @escaping (Result) -> Void) -> Void -``` - -```swift -public typealias StringClosure = (Lambda.Context, String, @escaping (Result) -> Void) -> Void -``` - -This design allows for additional event types as well, and such Lambda implementation can extend one of the above protocols and provided their own `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer` encoding. - -### Context - -When calling the user provided Lambda function, the library provides a `Context` class that provides metadata about the execution context, as well as utilities for logging and allocating buffers. - -```swift -public final class Context { - /// The request ID, which identifies the request that triggered the function invocation. - public let requestID: String - - /// The AWS X-Ray tracing header. - public let traceID: String - - /// The ARN of the Lambda function, version, or alias that's specified in the invocation. - public let invokedFunctionARN: String - - /// The timestamp that the function times out - public let deadline: DispatchWallTime - - /// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider. - public let cognitoIdentity: String? - - /// For invocations from the AWS Mobile SDK, data about the client application and device. - public let clientContext: String? - - /// `Logger` to log with - /// - /// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable. - public let logger: Logger - - /// The `EventLoop` the Lambda is executed on. Use this to schedule work with. - /// This is useful when implementing the `EventLoopLambdaHandler` protocol. - /// - /// - note: The `EventLoop` is shared with the Lambda runtime engine and should be handled with extra care. - /// Most importantly the `EventLoop` must never be blocked. - public let eventLoop: EventLoop - - /// `ByteBufferAllocator` to allocate `ByteBuffer` - /// This is useful when implementing `EventLoopLambdaHandler` - public let allocator: ByteBufferAllocator -} -``` - -### Configuration - -The library’s behavior can be fine tuned using environment variables based configuration. The library supported the following environment variables: - -* `LOG_LEVEL`: Define the logging level as defined by [SwiftLog](https://github.com/apple/swift-log). Set to INFO by default. -* `MAX_REQUESTS`: Max cycles the library should handle before exiting. Set to none by default. -* `STOP_SIGNAL`: Signal to capture for termination. Set to `TERM` by default. -* `REQUEST_TIMEOUT`: Max time to wait for responses to come back from the AWS Runtime engine. Set to none by default. - - -### AWS Lambda Runtime Engine Integration - -The library is designed to integrate with AWS Lambda Runtime Engine via the [AWS Lambda Runtime API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) which was introduced as part of [AWS Lambda Custom Runtimes](https://aws.amazon.com/about-aws/whats-new/2018/11/aws-lambda-now-supports-custom-runtimes-and-layers/) in 2018. The latter is an HTTP server that exposes three main RESTful endpoint: - -* `/runtime/invocation/next` -* `/runtime/invocation/response` -* `/runtime/invocation/error` - -A single Lambda execution workflow is made of the following steps: - -1. The library calls AWS Lambda Runtime Engine `/next` endpoint to retrieve the next invocation request. -2. The library parses the response HTTP headers and populate the `Context` object. -3. The library reads the `/next` response body and attempt to decode it. Typically it decodes to user provided `In` type which extends `Decodable`, but users may choose to write Lambda functions that receive the input as `String` or `ByteBuffer` which require less, or no decoding. -4. The library hands off the `Context` and `In` event to the user provided handler. In the case of [`LambdaHandler`][lh] based handler this is done on a dedicated `DispatchQueue`, providing isolation between user's and the library's code. -5. User provided handler processes the request asynchronously, invoking a callback or returning a future upon completion, which returns a `Result` type with the `Out` or `Error` populated. -6. In case of error, the library posts to AWS Lambda Runtime Engine `/error` endpoint to provide the error details, which will show up on AWS Lambda logs. -7. In case of success, the library will attempt to encode the response. Typically it encodes from user provided `Out` type which extends `Encodable`, but users may choose to write Lambda functions that return a `String` or `ByteBuffer`, which require less, or no encoding. The library then posts the response to AWS Lambda Runtime Engine `/response` endpoint to provide the response to the callee. - -The library encapsulates the workflow via the internal `LambdaRuntimeClient` and `LambdaRunner` structs respectively. - -### Lifecycle Management - -AWS Lambda Runtime Engine controls the Application lifecycle and in the happy case never terminates the application, only suspends its execution when no work is available. - -As such, the library's main entry point is designed to run forever in a blocking fashion, performing the workflow described above in an endless loop. - -That loop is broken if/when an internal error occurs, such as a failure to communicate with AWS Lambda Runtime Engine API, or under other unexpected conditions. - -By default, the library also registers a Signal handler that traps `INT` and `TERM`, which are typical Signals used in modern deployment platforms to communicate shutdown request. - -### Integration with AWS Platform Events - -AWS Lambda functions can be invoked directly from the AWS Lambda console UI, AWS Lambda API, AWS SDKs, AWS CLI, and AWS toolkits. More commonly, they are invoked as a reaction to an event coming from the AWS platform. To make it easier to integrate with AWS platform events, [Swift AWS Lambda Runtime Events](http://github.com/swift-server/swift-aws-lambda-events) library is available, designed to work together with this runtime library. [Swift AWS Lambda Runtime Events](http://github.com/swift-server/swift-aws-lambda-events) includes an `AWSLambdaEvents` target which provides abstractions for many commonly used events. - -## Performance - -Lambda functions performance is usually measured across two axes: - -- **Cold start times**: The time it takes for a Lambda function to startup, ask for an invocation and process the first invocation. - -- **Warm invocation times**: The time it takes for a Lambda function to process an invocation after the Lambda has been invoked at least once. - -Larger packages size (Zip file uploaded to AWS Lambda) negatively impact the cold start time, since AWS needs to download and unpack the package before starting the process. - -Swift provides great Unicode support via [ICU](http://site.icu-project.org/home). Therefore, Swift-based Lambda functions include the ICU libraries which tend to be large. This impacts the download time mentioned above and an area for further optimization. Some of the alternatives worth exploring are using the system ICU that comes with Amazon Linux (albeit older than the one Swift ships with) or working to remove the ICU dependency altogether. We welcome ideas and contributions to this end. - - - -[lh]: ./AWSLambdaRuntimeCore/Protocols/LambdaHandler.html -[ellh]: ./AWSLambdaRuntimeCore/Protocols/EventLoopLambdaHandler.html -[bblh]: ./AWSLambdaRuntimeCore/Protocols/ByteBufferLambdaHandler.html diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/quick-setup.md b/Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/quick-setup.md rename to Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/01-overview.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/01-overview.tutorial similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/01-overview.tutorial rename to Sources/AWSLambdaRuntime/Docs.docc/tutorials/01-overview.tutorial diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/02-what-is-lambda.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/02-what-is-lambda.tutorial similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/02-what-is-lambda.tutorial rename to Sources/AWSLambdaRuntime/Docs.docc/tutorials/02-what-is-lambda.tutorial diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/03-prerequisites.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-prerequisites.tutorial similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/03-prerequisites.tutorial rename to Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-prerequisites.tutorial diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/03-write-function.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-write-function.tutorial similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/03-write-function.tutorial rename to Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-write-function.tutorial diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/04-deploy-function.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/04-deploy-function.tutorial similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/04-deploy-function.tutorial rename to Sources/AWSLambdaRuntime/Docs.docc/tutorials/04-deploy-function.tutorial diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/table-of-content.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/table-of-content.tutorial similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/table-of-content.tutorial rename to Sources/AWSLambdaRuntime/Docs.docc/tutorials/table-of-content.tutorial diff --git a/Sources/AWSLambdaRuntime/Context+Foundation.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift similarity index 92% rename from Sources/AWSLambdaRuntime/Context+Foundation.swift rename to Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift index 105b7765..72b7a65d 100644 --- a/Sources/AWSLambdaRuntime/Context+Foundation.swift +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift @@ -12,8 +12,7 @@ // //===----------------------------------------------------------------------===// -import AWSLambdaRuntimeCore - +#if FoundationJSONSupport #if canImport(FoundationEssentials) import FoundationEssentials #else @@ -26,3 +25,4 @@ extension LambdaContext { return Date(timeIntervalSince1970: secondsSinceEpoch) } } +#endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift new file mode 100644 index 00000000..9bd4d30f --- /dev/null +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift @@ -0,0 +1,138 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2022 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 +// +//===----------------------------------------------------------------------===// + +#if FoundationJSONSupport +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import struct Foundation.Data +import class Foundation.JSONDecoder +import class Foundation.JSONEncoder +#endif + +public struct LambdaJSONEventDecoder: LambdaEventDecoder { + @usableFromInline let jsonDecoder: JSONDecoder + + @inlinable + public init(_ jsonDecoder: JSONDecoder) { + self.jsonDecoder = jsonDecoder + } + + @inlinable + public func decode(_ type: Event.Type, from buffer: NIOCore.ByteBuffer) throws -> Event + where Event: Decodable { + try buffer.getJSONDecodable( + Event.self, + decoder: self.jsonDecoder, + at: buffer.readerIndex, + length: buffer.readableBytes + )! // must work, enough readable bytes + } +} + +public struct LambdaJSONOutputEncoder: LambdaOutputEncoder { + @usableFromInline let jsonEncoder: JSONEncoder + + @inlinable + public init(_ jsonEncoder: JSONEncoder) { + self.jsonEncoder = jsonEncoder + } + + @inlinable + public func encode(_ value: Output, into buffer: inout ByteBuffer) throws { + try buffer.writeJSONEncodable(value, encoder: self.jsonEncoder) + } +} + +extension LambdaCodableAdapter { + /// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output. + /// - Parameters: + /// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`. By default, a JSONEncoder is used. + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. By default, a JSONDecoder is used. + /// - handler: The handler object. + public init( + encoder: JSONEncoder = JSONEncoder(), + decoder: JSONDecoder = JSONDecoder(), + handler: sending Handler + ) + where + Output: Encodable, + Output == Handler.Output, + Encoder == LambdaJSONOutputEncoder, + Decoder == LambdaJSONEventDecoder + { + self.init( + encoder: LambdaJSONOutputEncoder(encoder), + decoder: LambdaJSONEventDecoder(decoder), + handler: handler + ) + } +} + +extension LambdaRuntime { + /// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a non-`Void` return type**. + /// - Parameters: + /// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. `JSONDecoder()` used as default. + /// - encoder: The encoder object that will be used to encode the generic `Output` into a `ByteBuffer`. `JSONEncoder()` used as default. + /// - body: The handler in the form of a closure. + public convenience init( + decoder: JSONDecoder = JSONDecoder(), + encoder: JSONEncoder = JSONEncoder(), + body: sending @escaping (Event, LambdaContext) async throws -> Output + ) + where + Handler == LambdaCodableAdapter< + LambdaHandlerAdapter>, + Event, + Output, + LambdaJSONEventDecoder, + LambdaJSONOutputEncoder + > + { + let handler = LambdaCodableAdapter( + encoder: encoder, + decoder: decoder, + handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) + ) + + self.init(handler: handler) + } + + /// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a `Void` return type**. + /// - Parameter body: The handler in the form of a closure. + /// - Parameter decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. `JSONDecoder()` used as default. + public convenience init( + decoder: JSONDecoder = JSONDecoder(), + body: sending @escaping (Event, LambdaContext) async throws -> Void + ) + where + Handler == LambdaCodableAdapter< + LambdaHandlerAdapter>, + Event, + Void, + LambdaJSONEventDecoder, + VoidEncoder + > + { + let handler = LambdaCodableAdapter( + decoder: LambdaJSONEventDecoder(decoder), + handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) + ) + + self.init(handler: handler) + } +} +#endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/Vendored/ByteBuffer-foundation.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/ByteBuffer-foundation.swift similarity index 98% rename from Sources/AWSLambdaRuntime/Vendored/ByteBuffer-foundation.swift rename to Sources/AWSLambdaRuntime/FoundationSupport/Vendored/ByteBuffer-foundation.swift index 8dbd7326..482e020f 100644 --- a/Sources/AWSLambdaRuntime/Vendored/ByteBuffer-foundation.swift +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/ByteBuffer-foundation.swift @@ -26,6 +26,7 @@ // //===----------------------------------------------------------------------===// +#if FoundationJSONSupport import NIOCore #if canImport(FoundationEssentials) @@ -104,3 +105,4 @@ extension ByteBuffer { } } } +#endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/Vendored/JSON+ByteBuffer.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/JSON+ByteBuffer.swift similarity index 98% rename from Sources/AWSLambdaRuntime/Vendored/JSON+ByteBuffer.swift rename to Sources/AWSLambdaRuntime/FoundationSupport/Vendored/JSON+ByteBuffer.swift index 092b2368..89ce9b87 100644 --- a/Sources/AWSLambdaRuntime/Vendored/JSON+ByteBuffer.swift +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/JSON+ByteBuffer.swift @@ -26,6 +26,7 @@ // //===----------------------------------------------------------------------===// +#if FoundationJSONSupport import NIOCore #if canImport(FoundationEssentials) @@ -147,3 +148,4 @@ extension JSONEncoder { return buffer } } +#endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/Lambda+Codable.swift b/Sources/AWSLambdaRuntime/Lambda+Codable.swift index fb6d2ca7..a77c0542 100644 --- a/Sources/AWSLambdaRuntime/Lambda+Codable.swift +++ b/Sources/AWSLambdaRuntime/Lambda+Codable.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2017-2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,126 +12,151 @@ // //===----------------------------------------------------------------------===// -@_exported import AWSLambdaRuntimeCore import NIOCore -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import struct Foundation.Data -import class Foundation.JSONDecoder -import class Foundation.JSONEncoder -#endif - -public struct LambdaJSONEventDecoder: LambdaEventDecoder { - @usableFromInline let jsonDecoder: JSONDecoder - - @inlinable - public init(_ jsonDecoder: JSONDecoder) { - self.jsonDecoder = jsonDecoder - } - - @inlinable - public func decode(_ type: Event.Type, from buffer: NIOCore.ByteBuffer) throws -> Event - where Event: Decodable { - try buffer.getJSONDecodable( - Event.self, - decoder: self.jsonDecoder, - at: buffer.readerIndex, - length: buffer.readableBytes - )! // must work, enough readable bytes - } -} - -public struct LambdaJSONOutputEncoder: LambdaOutputEncoder { - @usableFromInline let jsonEncoder: JSONEncoder - - @inlinable - public init(_ jsonEncoder: JSONEncoder) { - self.jsonEncoder = jsonEncoder - } - - @inlinable - public func encode(_ value: Output, into buffer: inout ByteBuffer) throws { - try buffer.writeJSONEncodable(value, encoder: self.jsonEncoder) - } -} - -extension LambdaCodableAdapter { - /// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output. - /// - Parameters: - /// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`. By default, a JSONEncoder is used. - /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. By default, a JSONDecoder is used. - /// - handler: The handler object. - public init( - encoder: JSONEncoder = JSONEncoder(), - decoder: JSONDecoder = JSONDecoder(), - handler: Handler - ) - where - Output: Encodable, - Output == Handler.Output, - Encoder == LambdaJSONOutputEncoder, - Decoder == LambdaJSONEventDecoder - { - self.init( - encoder: LambdaJSONOutputEncoder(encoder), - decoder: LambdaJSONEventDecoder(decoder), - handler: handler - ) - } -} - -extension LambdaRuntime { - /// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a non-`Void` return type**. +/// The protocol a decoder must conform to so that it can be used with ``LambdaCodableAdapter`` to decode incoming +/// `ByteBuffer` events. +public protocol LambdaEventDecoder { + /// Decode the `ByteBuffer` representing the received event into the generic `Event` type + /// the handler will receive. /// - Parameters: - /// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. `JSONDecoder()` used as default. - /// - encoder: The encoder object that will be used to encode the generic `Output` into a `ByteBuffer`. `JSONEncoder()` used as default. - /// - body: The handler in the form of a closure. - public convenience init( - decoder: JSONDecoder = JSONDecoder(), - encoder: JSONEncoder = JSONEncoder(), - body: sending @escaping (Event, LambdaContext) async throws -> Output - ) - where - Handler == LambdaCodableAdapter< - LambdaHandlerAdapter>, - Event, - Output, - LambdaJSONEventDecoder, - LambdaJSONOutputEncoder - > - { - let handler = LambdaCodableAdapter( - encoder: encoder, - decoder: decoder, - handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) - ) + /// - type: The type of the object to decode the buffer into. + /// - buffer: The buffer to be decoded. + /// - Returns: An object containing the decoded data. + func decode(_ type: Event.Type, from buffer: ByteBuffer) throws -> Event +} - self.init(handler: handler) +/// The protocol an encoder must conform to so that it can be used with ``LambdaCodableAdapter`` to encode the generic +/// ``LambdaOutputEncoder/Output`` object into a `ByteBuffer`. +public protocol LambdaOutputEncoder { + associatedtype Output + + /// Encode the generic type `Output` the handler has returned into a `ByteBuffer`. + /// - Parameters: + /// - value: The object to encode into a `ByteBuffer`. + /// - buffer: The `ByteBuffer` where the encoded value will be written to. + func encode(_ value: Output, into buffer: inout ByteBuffer) throws +} + +public struct VoidEncoder: LambdaOutputEncoder { + public typealias Output = Void + + public init() {} + + @inlinable + public func encode(_ value: Void, into buffer: inout NIOCore.ByteBuffer) throws {} +} + +/// Adapts a ``LambdaHandler`` conforming handler to conform to ``LambdaWithBackgroundProcessingHandler``. +public struct LambdaHandlerAdapter< + Event: Decodable, + Output, + Handler: LambdaHandler +>: LambdaWithBackgroundProcessingHandler where Handler.Event == Event, Handler.Output == Output { + @usableFromInline let handler: Handler + + /// Initializes an instance given a concrete handler. + /// - Parameter handler: The ``LambdaHandler`` conforming handler that is to be adapted to ``LambdaWithBackgroundProcessingHandler``. + @inlinable + public init(handler: sending Handler) { + self.handler = handler } - /// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a `Void` return type**. - /// - Parameter body: The handler in the form of a closure. - /// - Parameter decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. `JSONDecoder()` used as default. - public convenience init( - decoder: JSONDecoder = JSONDecoder(), - body: sending @escaping (Event, LambdaContext) async throws -> Void - ) - where - Handler == LambdaCodableAdapter< - LambdaHandlerAdapter>, - Event, - Void, - LambdaJSONEventDecoder, - VoidEncoder - > - { - let handler = LambdaCodableAdapter( - decoder: LambdaJSONEventDecoder(decoder), - handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) + /// Passes the generic `Event` object to the ``LambdaHandler/handle(_:context:)`` function, and + /// the resulting output is then written to ``LambdaWithBackgroundProcessingHandler``'s `outputWriter`. + /// - Parameters: + /// - event: The received event. + /// - outputWriter: The writer to write the computed response to. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + @inlinable + public func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + let output = try await self.handler.handle(event, context: context) + try await outputWriter.write(output) + } +} + +/// Adapts a ``LambdaWithBackgroundProcessingHandler`` conforming handler to conform to ``StreamingLambdaHandler``. +public struct LambdaCodableAdapter< + Handler: LambdaWithBackgroundProcessingHandler, + Event: Decodable, + Output, + Decoder: LambdaEventDecoder, + Encoder: LambdaOutputEncoder +>: StreamingLambdaHandler where Handler.Event == Event, Handler.Output == Output, Encoder.Output == Output { + @usableFromInline let handler: Handler + @usableFromInline let encoder: Encoder + @usableFromInline let decoder: Decoder + @usableFromInline var byteBuffer: ByteBuffer = .init() + + /// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output. + /// - Parameters: + /// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`. + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. + /// - handler: The handler object. + @inlinable + public init(encoder: sending Encoder, decoder: sending Decoder, handler: sending Handler) where Output: Encodable { + self.encoder = encoder + self.decoder = decoder + self.handler = handler + } + + /// Initializes an instance given a decoder, and a handler with a `Void` output. + /// - Parameters: + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. + /// - handler: The handler object. + @inlinable + public init(decoder: sending Decoder, handler: Handler) where Output == Void, Encoder == VoidEncoder { + self.encoder = VoidEncoder() + self.decoder = decoder + self.handler = handler + } + + /// A ``StreamingLambdaHandler/handle(_:responseWriter:context:)`` wrapper. + /// - Parameters: + /// - event: The received event. + /// - outputWriter: The writer to write the computed response to. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + @inlinable + public mutating func handle( + _ request: ByteBuffer, + responseWriter: Writer, + context: LambdaContext + ) async throws { + let event = try self.decoder.decode(Event.self, from: request) + + let writer = LambdaCodableResponseWriter( + encoder: self.encoder, + streamWriter: responseWriter ) + try await self.handler.handle(event, outputWriter: writer, context: context) + } +} + +/// A ``LambdaResponseStreamWriter`` wrapper that conforms to ``LambdaResponseWriter``. +public struct LambdaCodableResponseWriter: + LambdaResponseWriter +where Output == Encoder.Output { + @usableFromInline let underlyingStreamWriter: Base + @usableFromInline let encoder: Encoder + + /// Initializes an instance given an encoder and an underlying ``LambdaResponseStreamWriter``. + /// - Parameters: + /// - encoder: The encoder object that will be used to encode the generic `Output` into a `ByteBuffer`, which will then be passed to `streamWriter`. + /// - streamWriter: The underlying ``LambdaResponseStreamWriter`` that will be wrapped. + @inlinable + public init(encoder: Encoder, streamWriter: Base) { + self.encoder = encoder + self.underlyingStreamWriter = streamWriter + } - self.init(handler: handler) + @inlinable + public func write(_ output: Output) async throws { + var outputBuffer = ByteBuffer() + try self.encoder.encode(output, into: &outputBuffer) + try await self.underlyingStreamWriter.writeAndFinish(outputBuffer) } } diff --git a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift new file mode 100644 index 00000000..4d85f7b2 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift @@ -0,0 +1,538 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2020 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 +// +//===----------------------------------------------------------------------===// + +#if LocalServerSupport +import DequeModule +import Dispatch +import Logging +import NIOCore +import NIOHTTP1 +import NIOPosix +import Synchronization + +// This functionality is designed for local testing hence being a #if DEBUG flag. + +// For example: +// try Lambda.withLocalServer { +// 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 +// ) +// } +// } +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: sending @escaping () async throws -> Void + ) async throws { + var logger = Logger(label: "LocalServer") + logger.logLevel = Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info + + try await LambdaHTTPServer.withLocalServer( + invocationEndpoint: invocationEndpoint, + logger: logger + ) { + try await body() + } + } +} + +// MARK: - Local HTTP Server + +/// An HTTP server that behaves like the AWS Lambda service for local testing. +/// This server is used to simulate the AWS Lambda service for local testing but also to accept invocation requests from the lambda client. +/// +/// It accepts three types of requests from the Lambda function (through the LambdaRuntimeClient): +/// 1. GET /next - the lambda function polls this endpoint to get the next invocation request +/// 2. POST /:requestID/response - the lambda function posts the response to the invocation request +/// 3. POST /:requestID/error - the lambda function posts an error response to the invocation request +/// +/// It also accepts one type of request from the client invoking the lambda function: +/// 1. POST /invoke - the client posts the event to the lambda function +/// +/// This server passes the data received from /invoke POST request to the lambda function (GET /next) and then forwards the response back to the client. +private struct LambdaHTTPServer { + private let invocationEndpoint: String + + private let invocationPool = Pool() + private let responsePool = Pool() + + private init( + invocationEndpoint: String? + ) { + self.invocationEndpoint = invocationEndpoint ?? "/invoke" + } + + private enum TaskResult: Sendable { + case closureResult(Swift.Result) + case serverReturned(Swift.Result) + } + + struct UnsafeTransferBox: @unchecked Sendable { + let value: Value + + init(value: sending Value) { + self.value = value + } + } + + static func withLocalServer( + invocationEndpoint: String?, + host: String = "127.0.0.1", + port: Int = 7000, + eventLoopGroup: MultiThreadedEventLoopGroup = .singleton, + logger: Logger, + _ closure: sending @escaping () async throws -> Result + ) async throws -> Result { + let channel = try await ServerBootstrap(group: eventLoopGroup) + .serverChannelOption(.backlog, value: 256) + .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) + .childChannelOption(.maxMessagesPerRead, value: 1) + .bind( + host: host, + port: port + ) { channel in + channel.eventLoop.makeCompletedFuture { + + try channel.pipeline.syncOperations.configureHTTPServerPipeline( + withErrorHandling: true + ) + + return try NIOAsyncChannel( + wrappingChannelSynchronously: channel, + configuration: NIOAsyncChannel.Configuration( + inboundType: HTTPServerRequestPart.self, + outboundType: HTTPServerResponsePart.self + ) + ) + } + } + + logger.info( + "Server started and listening", + metadata: [ + "host": "\(channel.channel.localAddress?.ipAddress?.debugDescription ?? "")", + "port": "\(channel.channel.localAddress?.port ?? 0)", + ] + ) + + let server = LambdaHTTPServer(invocationEndpoint: invocationEndpoint) + + // Sadly the Swift compiler does not understand that the passed in closure will only be + // invoked once. Because of this we need an unsafe transfer box here. Buuuh! + let closureBox = UnsafeTransferBox(value: closure) + let result = await withTaskGroup(of: TaskResult.self, returning: Swift.Result.self) { + group in + group.addTask { + let c = closureBox.value + do { + let result = try await c() + return .closureResult(.success(result)) + } catch { + return .closureResult(.failure(error)) + } + } + + group.addTask { + do { + // We are handling each incoming connection in a separate child task. It is important + // to use a discarding task group here which automatically discards finished child tasks. + // A normal task group retains all child tasks and their outputs in memory until they are + // consumed by iterating the group or by exiting the group. Since, we are never consuming + // the results of the group we need the group to automatically discard them; otherwise, this + // would result in a memory leak over time. + try await withThrowingDiscardingTaskGroup { taskGroup in + try await channel.executeThenClose { inbound in + for try await connectionChannel in inbound { + + taskGroup.addTask { + logger.trace("Handling a new connection") + await server.handleConnection(channel: connectionChannel, logger: logger) + logger.trace("Done handling the connection") + } + } + } + } + return .serverReturned(.success(())) + } catch { + return .serverReturned(.failure(error)) + } + } + + // Now that the local HTTP server and LambdaHandler tasks are started, wait for the + // first of the two that will terminate. + // When the first task terminates, cancel the group and collect the result of the + // second task. + + // collect and return the result of the LambdaHandler + let serverOrHandlerResult1 = await group.next()! + group.cancelAll() + + switch serverOrHandlerResult1 { + case .closureResult(let result): + return result + + case .serverReturned(let result): + logger.error( + "Server shutdown before closure completed", + metadata: [ + "error": "\(result.maybeError != nil ? "\(result.maybeError!)" : "none")" + ] + ) + switch await group.next()! { + case .closureResult(let result): + return result + + case .serverReturned: + fatalError("Only one task is a server, and only one can return `serverReturned`") + } + } + } + + logger.info("Server shutting down") + return try result.get() + } + + /// This method handles individual TCP connections + private func handleConnection( + channel: NIOAsyncChannel, + logger: Logger + ) async { + + var requestHead: HTTPRequestHead! + var requestBody: ByteBuffer? + + // Note that this method is non-throwing and we are catching any error. + // We do this since we don't want to tear down the whole server when a single connection + // encounters an error. + do { + try await channel.executeThenClose { inbound, outbound in + for try await inboundData in inbound { + switch inboundData { + case .head(let head): + requestHead = head + + case .body(let body): + requestBody = body + + case .end: + precondition(requestHead != nil, "Received .end without .head") + // process the request + let response = try await self.processRequest( + head: requestHead, + body: requestBody, + logger: logger + ) + // send the responses + try await self.sendResponse( + response: response, + outbound: outbound, + logger: logger + ) + + requestHead = nil + requestBody = nil + } + } + } + } catch { + logger.error("Hit error: \(error)") + } + } + + /// This function process the URI request sent by the client and by the Lambda function + /// + /// It enqueues the client invocation and iterate over the invocation queue when the Lambda function sends /next request + /// It answers the /:requestID/response and /:requestID/error requests sent by the Lambda function but do not process the body + /// + /// - Parameters: + /// - head: the HTTP request head + /// - body: the HTTP request body + /// - Throws: + /// - Returns: the response to send back to the client or the Lambda function + private func processRequest( + head: HTTPRequestHead, + body: ByteBuffer?, + logger: Logger + ) async throws -> LocalServerResponse { + + if let body { + logger.trace( + "Processing request", + metadata: ["URI": "\(head.method) \(head.uri)", "Body": "\(String(buffer: body))"] + ) + } else { + logger.trace("Processing request", metadata: ["URI": "\(head.method) \(head.uri)"]) + } + + switch (head.method, head.uri) { + + // + // client invocations + // + // client POST /invoke + case (.POST, let url) where url.hasSuffix(self.invocationEndpoint): + guard let body else { + return .init(status: .badRequest, headers: [], body: nil) + } + // we always accept the /invoke request and push them to the pool + let requestId = "\(DispatchTime.now().uptimeNanoseconds)" + var logger = logger + logger[metadataKey: "requestID"] = "\(requestId)" + logger.trace("/invoke received invocation") + await self.invocationPool.push(LocalServerInvocation(requestId: requestId, request: body)) + + // wait for the lambda function to process the request + for try await response in self.responsePool { + logger.trace( + "Received response to return to client", + metadata: ["requestId": "\(response.requestId ?? "")"] + ) + if response.requestId == requestId { + return response + } else { + logger.error( + "Received response for a different request id", + metadata: ["response requestId": "\(response.requestId ?? "")", "requestId": "\(requestId)"] + ) + // should we return an error here ? Or crash as this is probably a programming error? + } + } + // What todo when there is no more responses to process? + // This should not happen as the async iterator blocks until there is a response to process + fatalError("No more responses to process - the async for loop should not return") + + // client uses incorrect HTTP method + case (_, let url) where url.hasSuffix(self.invocationEndpoint): + return .init(status: .methodNotAllowed) + + // + // lambda invocations + // + + // /next endpoint is called by the lambda polling for work + // this call only returns when there is a task to give to the lambda function + case (.GET, let url) where url.hasSuffix(Consts.getNextInvocationURLSuffix): + + // pop the tasks from the queue + logger.trace("/next waiting for /invoke") + for try await invocation in self.invocationPool { + logger.trace("/next retrieved invocation", metadata: ["requestId": "\(invocation.requestId)"]) + // this call also stores the invocation requestId into the response + return invocation.makeResponse(status: .accepted) + } + // What todo when there is no more tasks to process? + // This should not happen as the async iterator blocks until there is a task to process + fatalError("No more invocations to process - the async for loop should not return") + + // :requestID/response endpoint is called by the lambda posting the response + case (.POST, let url) where url.hasSuffix(Consts.postResponseURLSuffix): + let parts = 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 .init(status: .badRequest) + } + // enqueue the lambda function response to be served as response to the client /invoke + logger.trace("/:requestID/response received response", metadata: ["requestId": "\(requestID)"]) + await self.responsePool.push( + LocalServerResponse( + id: requestID, + status: .ok, + headers: [("Content-Type", "application/json")], + body: body + ) + ) + + // tell the Lambda function we accepted the response + return .init(id: requestID, status: .accepted) + + // :requestID/error endpoint is called by the lambda posting an error response + // we accept all requestID and we do not handle the body, we just acknowledge the request + case (.POST, let url) where url.hasSuffix(Consts.postErrorURLSuffix): + let parts = 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 .init(status: .badRequest) + } + // enqueue the lambda function response to be served as response to the client /invoke + logger.trace("/:requestID/response received response", metadata: ["requestId": "\(requestID)"]) + await self.responsePool.push( + LocalServerResponse( + id: requestID, + status: .internalServerError, + headers: [("Content-Type", "application/json")], + body: body + ) + ) + + return .init(status: .accepted) + + // unknown call + default: + return .init(status: .notFound) + } + } + + private func sendResponse( + response: LocalServerResponse, + outbound: NIOAsyncChannelOutboundWriter, + logger: Logger + ) async throws { + var headers = HTTPHeaders(response.headers ?? []) + headers.add(name: "Content-Length", value: "\(response.body?.readableBytes ?? 0)") + + logger.trace("Writing response", metadata: ["requestId": "\(response.requestId ?? "")"]) + try await outbound.write( + HTTPServerResponsePart.head( + HTTPResponseHead( + version: .init(major: 1, minor: 1), + status: response.status, + headers: headers + ) + ) + ) + if let body = response.body { + try await outbound.write(HTTPServerResponsePart.body(.byteBuffer(body))) + } + + try await outbound.write(HTTPServerResponsePart.end(nil)) + } + + /// A shared data structure to store the current invocation or response requests and the continuation objects. + /// This data structure is shared between instances of the HTTPHandler + /// (one instance to serve requests from the Lambda function and one instance to serve requests from the client invoking the lambda function). + private final class Pool: AsyncSequence, AsyncIteratorProtocol, Sendable where T: Sendable { + typealias Element = T + + enum State: ~Copyable { + case buffer(Deque) + case continuation(CheckedContinuation?) + } + + private let lock = Mutex(.buffer([])) + + /// enqueue an element, or give it back immediately to the iterator if it is waiting for an element + public func push(_ invocation: T) async { + // if the iterator is waiting for an element, give it to it + // otherwise, enqueue the element + let maybeContinuation = self.lock.withLock { state -> CheckedContinuation? in + switch consume state { + case .continuation(let continuation): + state = .buffer([]) + return continuation + + case .buffer(var buffer): + buffer.append(invocation) + state = .buffer(buffer) + return nil + } + } + + maybeContinuation?.resume(returning: invocation) + } + + func next() async throws -> T? { + // exit the async for loop if the task is cancelled + guard !Task.isCancelled else { + return nil + } + + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let nextAction = self.lock.withLock { state -> T? in + switch consume state { + case .buffer(var buffer): + if let first = buffer.popFirst() { + state = .buffer(buffer) + return first + } else { + state = .continuation(continuation) + return nil + } + + case .continuation: + fatalError("Concurrent invocations to next(). This is illegal.") + } + } + + guard let nextAction else { return } + + continuation.resume(returning: nextAction) + } + } + + func makeAsyncIterator() -> Pool { + self + } + } + + private struct LocalServerResponse: Sendable { + let requestId: String? + let status: HTTPResponseStatus + let headers: [(String, String)]? + let body: ByteBuffer? + init(id: String? = nil, status: HTTPResponseStatus, headers: [(String, String)]? = nil, body: ByteBuffer? = nil) + { + self.requestId = id + self.status = status + self.headers = headers + self.body = body + } + } + + private struct LocalServerInvocation: Sendable { + let requestId: String + let request: ByteBuffer + + func makeResponse(status: HTTPResponseStatus) -> LocalServerResponse { + + // required headers + let 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 LocalServerResponse(id: self.requestId, status: status, headers: headers, body: self.request) + } + } +} + +extension Result { + var maybeError: Failure? { + switch self { + case .success: + return nil + case .failure(let error): + return error + } + } +} +#endif diff --git a/Sources/AWSLambdaRuntimeCore/Lambda.swift b/Sources/AWSLambdaRuntime/Lambda.swift similarity index 57% rename from Sources/AWSLambdaRuntimeCore/Lambda.swift rename to Sources/AWSLambdaRuntime/Lambda.swift index 3ba90e9c..24f10343 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda.swift +++ b/Sources/AWSLambdaRuntime/Lambda.swift @@ -37,25 +37,33 @@ public enum Lambda { ) async throws where Handler: StreamingLambdaHandler { var handler = handler - while !Task.isCancelled { - let (invocation, writer) = try await runtimeClient.nextInvocation() + var logger = logger + do { + while !Task.isCancelled { + let (invocation, writer) = try await runtimeClient.nextInvocation() + logger[metadataKey: "aws-request-id"] = "\(invocation.metadata.requestID)" - do { - try await handler.handle( - invocation.event, - responseWriter: writer, - context: LambdaContext( - requestID: invocation.metadata.requestID, - traceID: invocation.metadata.traceID, - invokedFunctionARN: invocation.metadata.invokedFunctionARN, - deadline: DispatchWallTime(millisSinceEpoch: invocation.metadata.deadlineInMillisSinceEpoch), - logger: logger + do { + try await handler.handle( + invocation.event, + responseWriter: writer, + context: LambdaContext( + requestID: invocation.metadata.requestID, + traceID: invocation.metadata.traceID, + invokedFunctionARN: invocation.metadata.invokedFunctionARN, + deadline: DispatchWallTime( + millisSinceEpoch: invocation.metadata.deadlineInMillisSinceEpoch + ), + logger: logger + ) ) - ) - } catch { - try await writer.reportError(error) - continue + } catch { + try await writer.reportError(error) + continue + } } + } catch is CancellationError { + // don't allow cancellation error to propagate further } } diff --git a/Sources/AWSLambdaRuntimeCore/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/LambdaContext.swift rename to Sources/AWSLambdaRuntime/LambdaContext.swift diff --git a/Sources/AWSLambdaRuntimeCore/LambdaHandlers.swift b/Sources/AWSLambdaRuntime/LambdaHandlers.swift similarity index 96% rename from Sources/AWSLambdaRuntimeCore/LambdaHandlers.swift rename to Sources/AWSLambdaRuntime/LambdaHandlers.swift index b76b453d..82a6eef3 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaHandlers.swift +++ b/Sources/AWSLambdaRuntime/LambdaHandlers.swift @@ -154,7 +154,7 @@ public struct ClosureHandler: LambdaHandler { /// Initialize with a closure handler over generic `Input` and `Output` types. /// - Parameter body: The handler function written as a closure. - public init(body: @escaping (Event, LambdaContext) async throws -> Output) where Output: Encodable { + public init(body: sending @escaping (Event, LambdaContext) async throws -> Output) where Output: Encodable { self.body = body } @@ -192,8 +192,8 @@ extension LambdaRuntime { Encoder: LambdaOutputEncoder, Decoder: LambdaEventDecoder >( - encoder: Encoder, - decoder: Decoder, + encoder: sending Encoder, + decoder: sending Decoder, body: sending @escaping (Event, LambdaContext) async throws -> Output ) where @@ -205,13 +205,15 @@ extension LambdaRuntime { Encoder > { - let handler = LambdaCodableAdapter( + let closureHandler = ClosureHandler(body: body) + let streamingAdapter = LambdaHandlerAdapter(handler: closureHandler) + let codableWrapper = LambdaCodableAdapter( encoder: encoder, decoder: decoder, - handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) + handler: streamingAdapter ) - self.init(handler: handler) + self.init(handler: codableWrapper) } /// Initialize an instance with a ``LambdaHandler`` defined in the form of a closure **with a `Void` return type**, an encoder, and a decoder. @@ -219,7 +221,7 @@ extension LambdaRuntime { /// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. /// - body: The handler in the form of a closure. public convenience init( - decoder: Decoder, + decoder: sending Decoder, body: sending @escaping (Event, LambdaContext) async throws -> Void ) where diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRequestID.swift b/Sources/AWSLambdaRuntime/LambdaRequestID.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/LambdaRequestID.swift rename to Sources/AWSLambdaRuntime/LambdaRequestID.swift diff --git a/Sources/AWSLambdaTesting/LambdaTestRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime+ServiceLifecycle.swift similarity index 74% rename from Sources/AWSLambdaTesting/LambdaTestRuntime.swift rename to Sources/AWSLambdaRuntime/LambdaRuntime+ServiceLifecycle.swift index 6fba9656..54ecb537 100644 --- a/Sources/AWSLambdaTesting/LambdaTestRuntime.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime+ServiceLifecycle.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright (c) 2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// -import Logging -import NIOCore -import NIOPosix +#if ServiceLifecycleSupport +import ServiceLifecycle + +extension LambdaRuntime: Service {} +#endif diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift similarity index 99% rename from Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift rename to Sources/AWSLambdaRuntime/LambdaRuntime.swift index 317ee7ea..6bc2403c 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -82,7 +82,7 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St } else { - #if DEBUG + #if LocalServerSupport // we're not running on Lambda and we're compiled in DEBUG mode, // let's start a local server for testing try await Lambda.withLocalServer(invocationEndpoint: Lambda.env("LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT")) diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift similarity index 94% rename from Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift rename to Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift index bbd16efa..196cbeb1 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift @@ -49,9 +49,13 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol { } } + private typealias ConnectionContinuation = CheckedContinuation< + NIOLoopBound>, any Error + > + private enum ConnectionState { case disconnected - case connecting([CheckedContinuation, any Error>]) + case connecting([ConnectionContinuation]) case connected(Channel, LambdaChannelHandler) } @@ -141,24 +145,29 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol { } func nextInvocation() async throws -> (Invocation, Writer) { - switch self.lambdaState { - case .idle: - self.lambdaState = .waitingForNextInvocation - let handler = try await self.makeOrGetConnection() - let invocation = try await handler.nextInvocation() - guard case .waitingForNextInvocation = self.lambdaState else { + try await withTaskCancellationHandler { + switch self.lambdaState { + case .idle: + self.lambdaState = .waitingForNextInvocation + let handler = try await self.makeOrGetConnection() + let invocation = try await handler.nextInvocation() + guard case .waitingForNextInvocation = self.lambdaState else { + fatalError("Invalid state: \(self.lambdaState)") + } + self.lambdaState = .waitingForResponse(requestID: invocation.metadata.requestID) + return (invocation, Writer(runtimeClient: self)) + + case .waitingForNextInvocation, + .waitingForResponse, + .sendingResponse, + .sentResponse: fatalError("Invalid state: \(self.lambdaState)") } - self.lambdaState = .waitingForResponse(requestID: invocation.metadata.requestID) - return (invocation, Writer(runtimeClient: self)) - - case .waitingForNextInvocation, - .waitingForResponse, - .sendingResponse, - .sentResponse: - fatalError("Invalid state: \(self.lambdaState)") + } onCancel: { + Task { + await self.close() + } } - } private func write(_ buffer: NIOCore.ByteBuffer) async throws { @@ -284,11 +293,11 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol { case .connecting(var array): // Since we do get sequential invocations this case normally should never be hit. // We'll support it anyway. - return try await withCheckedThrowingContinuation { - (continuation: CheckedContinuation, any Error>) in + let loopBound = try await withCheckedThrowingContinuation { (continuation: ConnectionContinuation) in array.append(continuation) self.connectionState = .connecting(array) } + return loopBound.value case .connected(_, let handler): return handler } @@ -339,8 +348,9 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol { case .connecting(let array): self.connectionState = .connected(channel, handler) defer { + let loopBound = NIOLoopBound(handler, eventLoop: self.eventLoop) for continuation in array { - continuation.resume(returning: handler) + continuation.resume(returning: loopBound) } } return handler @@ -448,16 +458,16 @@ private final class LambdaChannelHandler self.configuration = configuration self.defaultHeaders = [ "host": "\(self.configuration.ip):\(self.configuration.port)", - "user-agent": "Swift-Lambda/Unknown", + "user-agent": .userAgent, ] self.errorHeaders = [ "host": "\(self.configuration.ip):\(self.configuration.port)", - "user-agent": "Swift-Lambda/Unknown", + "user-agent": .userAgent, "lambda-runtime-function-error-type": "Unhandled", ] self.streamingHeaders = [ "host": "\(self.configuration.ip):\(self.configuration.port)", - "user-agent": "Swift-Lambda/Unknown", + "user-agent": .userAgent, "transfer-encoding": "chunked", ] } @@ -628,7 +638,7 @@ private final class LambdaChannelHandler if byteBuffer?.readableBytes ?? 0 < 6_000_000 { [ "host": "\(self.configuration.ip):\(self.configuration.port)", - "user-agent": "Swift-Lambda/Unknown", + "user-agent": .userAgent, "content-length": "\(byteBuffer?.readableBytes ?? 0)", ] } else { @@ -815,6 +825,12 @@ extension LambdaChannelHandler: ChannelInboundHandler { func channelInactive(context: ChannelHandlerContext) { // fail any pending responses with last error or assume peer disconnected + switch self.state { + case .connected(_, .waitingForNextInvocation(let continuation)): + continuation.resume(throwing: self.lastError ?? ChannelError.ioOnClosedChannel) + default: + break + } // we don't need to forward channelInactive to the delegate, as the delegate observes the // closeFuture diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClientProtocol.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeClientProtocol.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/LambdaRuntimeClientProtocol.swift rename to Sources/AWSLambdaRuntime/LambdaRuntimeClientProtocol.swift diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift rename to Sources/AWSLambdaRuntime/LambdaRuntimeError.swift diff --git a/Sources/AWSLambdaRuntimeCore/Utils.swift b/Sources/AWSLambdaRuntime/Utils.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Utils.swift rename to Sources/AWSLambdaRuntime/Utils.swift diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+Codable.swift b/Sources/AWSLambdaRuntimeCore/Lambda+Codable.swift deleted file mode 100644 index 7a0a9a22..00000000 --- a/Sources/AWSLambdaRuntimeCore/Lambda+Codable.swift +++ /dev/null @@ -1,162 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 NIOCore - -/// The protocol a decoder must conform to so that it can be used with ``LambdaCodableAdapter`` to decode incoming -/// `ByteBuffer` events. -public protocol LambdaEventDecoder { - /// Decode the `ByteBuffer` representing the received event into the generic `Event` type - /// the handler will receive. - /// - Parameters: - /// - type: The type of the object to decode the buffer into. - /// - buffer: The buffer to be decoded. - /// - Returns: An object containing the decoded data. - func decode(_ type: Event.Type, from buffer: ByteBuffer) throws -> Event -} - -/// The protocol an encoder must conform to so that it can be used with ``LambdaCodableAdapter`` to encode the generic -/// ``LambdaOutputEncoder/Output`` object into a `ByteBuffer`. -public protocol LambdaOutputEncoder { - associatedtype Output - - /// Encode the generic type `Output` the handler has returned into a `ByteBuffer`. - /// - Parameters: - /// - value: The object to encode into a `ByteBuffer`. - /// - buffer: The `ByteBuffer` where the encoded value will be written to. - func encode(_ value: Output, into buffer: inout ByteBuffer) throws -} - -public struct VoidEncoder: LambdaOutputEncoder { - public typealias Output = Void - - public init() {} - - @inlinable - public func encode(_ value: Void, into buffer: inout NIOCore.ByteBuffer) throws {} -} - -/// Adapts a ``LambdaHandler`` conforming handler to conform to ``LambdaWithBackgroundProcessingHandler``. -public struct LambdaHandlerAdapter< - Event: Decodable, - Output, - Handler: LambdaHandler ->: LambdaWithBackgroundProcessingHandler where Handler.Event == Event, Handler.Output == Output { - @usableFromInline let handler: Handler - - /// Initializes an instance given a concrete handler. - /// - Parameter handler: The ``LambdaHandler`` conforming handler that is to be adapted to ``LambdaWithBackgroundProcessingHandler``. - @inlinable - public init(handler: Handler) { - self.handler = handler - } - - /// Passes the generic `Event` object to the ``LambdaHandler/handle(_:context:)`` function, and - /// the resulting output is then written to ``LambdaWithBackgroundProcessingHandler``'s `outputWriter`. - /// - Parameters: - /// - event: The received event. - /// - outputWriter: The writer to write the computed response to. - /// - context: The ``LambdaContext`` containing the invocation's metadata. - @inlinable - public func handle( - _ event: Event, - outputWriter: some LambdaResponseWriter, - context: LambdaContext - ) async throws { - let output = try await self.handler.handle(event, context: context) - try await outputWriter.write(output) - } -} - -/// Adapts a ``LambdaWithBackgroundProcessingHandler`` conforming handler to conform to ``StreamingLambdaHandler``. -public struct LambdaCodableAdapter< - Handler: LambdaWithBackgroundProcessingHandler, - Event: Decodable, - Output, - Decoder: LambdaEventDecoder, - Encoder: LambdaOutputEncoder ->: StreamingLambdaHandler where Handler.Event == Event, Handler.Output == Output, Encoder.Output == Output { - @usableFromInline let handler: Handler - @usableFromInline let encoder: Encoder - @usableFromInline let decoder: Decoder - @usableFromInline var byteBuffer: ByteBuffer = .init() - - /// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output. - /// - Parameters: - /// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`. - /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. - /// - handler: The handler object. - @inlinable - public init(encoder: Encoder, decoder: Decoder, handler: Handler) where Output: Encodable { - self.encoder = encoder - self.decoder = decoder - self.handler = handler - } - - /// Initializes an instance given a decoder, and a handler with a `Void` output. - /// - Parameters: - /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. - /// - handler: The handler object. - @inlinable - public init(decoder: Decoder, handler: Handler) where Output == Void, Encoder == VoidEncoder { - self.encoder = VoidEncoder() - self.decoder = decoder - self.handler = handler - } - - /// A ``StreamingLambdaHandler/handle(_:responseWriter:context:)`` wrapper. - /// - Parameters: - /// - event: The received event. - /// - outputWriter: The writer to write the computed response to. - /// - context: The ``LambdaContext`` containing the invocation's metadata. - @inlinable - public mutating func handle( - _ request: ByteBuffer, - responseWriter: Writer, - context: LambdaContext - ) async throws { - let event = try self.decoder.decode(Event.self, from: request) - - let writer = LambdaCodableResponseWriter( - encoder: self.encoder, - streamWriter: responseWriter - ) - try await self.handler.handle(event, outputWriter: writer, context: context) - } -} - -/// A ``LambdaResponseStreamWriter`` wrapper that conforms to ``LambdaResponseWriter``. -public struct LambdaCodableResponseWriter: - LambdaResponseWriter -where Output == Encoder.Output { - @usableFromInline let underlyingStreamWriter: Base - @usableFromInline let encoder: Encoder - - /// Initializes an instance given an encoder and an underlying ``LambdaResponseStreamWriter``. - /// - Parameters: - /// - encoder: The encoder object that will be used to encode the generic `Output` into a `ByteBuffer`, which will then be passed to `streamWriter`. - /// - streamWriter: The underlying ``LambdaResponseStreamWriter`` that will be wrapped. - @inlinable - public init(encoder: Encoder, streamWriter: Base) { - self.encoder = encoder - self.underlyingStreamWriter = streamWriter - } - - @inlinable - public func write(_ output: Output) async throws { - var outputBuffer = ByteBuffer() - try self.encoder.encode(output, into: &outputBuffer) - try await self.underlyingStreamWriter.writeAndFinish(outputBuffer) - } -} diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift deleted file mode 100644 index a23ef1cf..00000000 --- a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift +++ /dev/null @@ -1,325 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2020 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 -// -//===----------------------------------------------------------------------===// - -#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)!")) -// } -// } -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?)) { - - let eventLoop = context.eventLoop - let loopBoundContext = NIOLoopBound(context, eventLoop: eventLoop) - - 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 - let context = loopBoundContext.value - 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 - let context = loopBoundContext.value - 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/MockServer/MockHTTPServer.swift b/Sources/MockServer/MockHTTPServer.swift new file mode 100644 index 00000000..0849e325 --- /dev/null +++ b/Sources/MockServer/MockHTTPServer.swift @@ -0,0 +1,298 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2025 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 Logging +import NIOCore +import NIOHTTP1 +import NIOPosix +import Synchronization + +// for UUID and Date +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@main +struct HttpServer { + /// The server's host. (default: 127.0.0.1) + private let host: String + /// The server's port. (default: 7000) + private let port: Int + /// The server's event loop group. (default: MultiThreadedEventLoopGroup.singleton) + private let eventLoopGroup: MultiThreadedEventLoopGroup + /// the mode. Are we mocking a server for a Lambda function that expects a String or a JSON document? (default: string) + private let mode: Mode + /// the number of connections this server must accept before shutting down (default: 1) + private let maxInvocations: Int + /// the logger (control verbosity with LOG_LEVEL environment variable) + private let logger: Logger + + static func main() async throws { + var log = Logger(label: "MockServer") + log.logLevel = env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info + + let server = HttpServer( + host: env("HOST") ?? "127.0.0.1", + port: env("PORT").flatMap(Int.init) ?? 7000, + eventLoopGroup: .singleton, + mode: env("MODE").flatMap(Mode.init) ?? .string, + maxInvocations: env("MAX_INVOCATIONS").flatMap(Int.init) ?? 1, + logger: log + ) + try await server.run() + } + + /// This method starts the server and handles one unique incoming connections + /// The Lambda function will send two HTTP requests over this connection: one for the next invocation and one for the response. + private func run() async throws { + let channel = try await ServerBootstrap(group: self.eventLoopGroup) + .serverChannelOption(.backlog, value: 256) + .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) + .childChannelOption(.maxMessagesPerRead, value: 1) + .bind( + host: self.host, + port: self.port + ) { channel in + channel.eventLoop.makeCompletedFuture { + + try channel.pipeline.syncOperations.configureHTTPServerPipeline( + withErrorHandling: true + ) + + return try NIOAsyncChannel( + wrappingChannelSynchronously: channel, + configuration: NIOAsyncChannel.Configuration( + inboundType: HTTPServerRequestPart.self, + outboundType: HTTPServerResponsePart.self + ) + ) + } + } + + logger.info( + "Server started and listening", + metadata: [ + "host": "\(channel.channel.localAddress?.ipAddress?.debugDescription ?? "")", + "port": "\(channel.channel.localAddress?.port ?? 0)", + "maxInvocations": "\(self.maxInvocations)", + ] + ) + + // This counter is used to track the number of incoming connections. + // This mock servers accepts n TCP connection then shutdowns + let connectionCounter = SharedCounter(maxValue: self.maxInvocations) + + // We are handling each incoming connection in a separate child task. It is important + // to use a discarding task group here which automatically discards finished child tasks. + // A normal task group retains all child tasks and their outputs in memory until they are + // consumed by iterating the group or by exiting the group. Since, we are never consuming + // the results of the group we need the group to automatically discard them; otherwise, this + // would result in a memory leak over time. + try await withThrowingDiscardingTaskGroup { group in + try await channel.executeThenClose { inbound in + for try await connectionChannel in inbound { + + let counter = connectionCounter.current() + logger.trace("Handling new connection", metadata: ["connectionNumber": "\(counter)"]) + + group.addTask { + await self.handleConnection(channel: connectionChannel) + logger.trace("Done handling connection", metadata: ["connectionNumber": "\(counter)"]) + } + + if connectionCounter.increment() { + logger.info( + "Maximum number of connections reached, shutting down after current connection", + metadata: ["maxConnections": "\(self.maxInvocations)"] + ) + break // this causes the server to shutdown after handling the connection + } + } + } + } + logger.info("Server shutting down") + } + + /// This method handles a single connection by responsing hard coded value to a Lambda function request. + /// It handles two requests: one for the next invocation and one for the response. + /// when the maximum number of requests is reached, it closes the connection. + private func handleConnection( + channel: NIOAsyncChannel + ) async { + + var requestHead: HTTPRequestHead! + var requestBody: ByteBuffer? + + // each Lambda invocation results in TWO HTTP requests (next and response) + let requestCount = SharedCounter(maxValue: 2) + + // Note that this method is non-throwing and we are catching any error. + // We do this since we don't want to tear down the whole server when a single connection + // encounters an error. + do { + try await channel.executeThenClose { inbound, outbound in + for try await inboundData in inbound { + let requestNumber = requestCount.current() + logger.trace("Handling request", metadata: ["requestNumber": "\(requestNumber)"]) + + if case .head(let head) = inboundData { + logger.trace("Received request head", metadata: ["head": "\(head)"]) + requestHead = head + } + if case .body(let body) = inboundData { + logger.trace("Received request body", metadata: ["body": "\(body)"]) + requestBody = body + } + if case .end(let end) = inboundData { + logger.trace("Received request end", metadata: ["end": "\(String(describing: end))"]) + + precondition(requestHead != nil, "Received .end without .head") + let (responseStatus, responseHeaders, responseBody) = self.processRequest( + requestHead: requestHead, + requestBody: requestBody + ) + + try await self.sendResponse( + responseStatus: responseStatus, + responseHeaders: responseHeaders, + responseBody: responseBody, + outbound: outbound + ) + + requestHead = nil + + if requestCount.increment() { + logger.info( + "Maximum number of requests reached, closing this connection", + metadata: ["maxRequest": "2"] + ) + break // this finishes handiling request on this connection + } + } + } + } + } catch { + logger.error("Hit error: \(error)") + } + } + /// This function process the requests and return an hard-coded response (string or JSON depending on the mode). + /// We ignore the requestBody. + private func processRequest( + requestHead: HTTPRequestHead, + requestBody: ByteBuffer? + ) -> (HTTPResponseStatus, [(String, String)], String) { + var responseStatus: HTTPResponseStatus = .ok + var responseBody: String = "" + var responseHeaders: [(String, String)] = [] + + logger.trace( + "Processing request", + metadata: ["VERB": "\(requestHead.method)", "URI": "\(requestHead.uri)"] + ) + + if requestHead.uri.hasSuffix("/next") { + responseStatus = .accepted + + let requestId = UUID().uuidString + switch self.mode { + case .string: + responseBody = "\"Seb\"" // must be a valid JSON document + case .json: + responseBody = "{ \"name\": \"Seb\", \"age\" : 52 }" + } + let deadline = Int64(Date(timeIntervalSinceNow: 60).timeIntervalSince1970 * 1000) + responseHeaders = [ + (AmazonHeaders.requestID, requestId), + (AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime"), + (AmazonHeaders.traceID, "Root=1-5bef4de7-ad49b0e87f6ef6c87fc2e700;Parent=9a9197af755a6419;Sampled=1"), + (AmazonHeaders.deadline, String(deadline)), + ] + } else if requestHead.uri.hasSuffix("/response") { + responseStatus = .accepted + } else if requestHead.uri.hasSuffix("/error") { + responseStatus = .ok + } else { + responseStatus = .notFound + } + logger.trace("Returning response: \(responseStatus), \(responseHeaders), \(responseBody)") + return (responseStatus, responseHeaders, responseBody) + } + + private func sendResponse( + responseStatus: HTTPResponseStatus, + responseHeaders: [(String, String)], + responseBody: String, + outbound: NIOAsyncChannelOutboundWriter + ) async throws { + var headers = HTTPHeaders(responseHeaders) + headers.add(name: "Content-Length", value: "\(responseBody.utf8.count)") + headers.add(name: "KeepAlive", value: "timeout=1, max=2") + + logger.trace("Writing response head") + try await outbound.write( + HTTPServerResponsePart.head( + HTTPResponseHead( + version: .init(major: 1, minor: 1), // use HTTP 1.1 it keeps connection alive between requests + status: responseStatus, + headers: headers + ) + ) + ) + logger.trace("Writing response body") + try await outbound.write(HTTPServerResponsePart.body(.byteBuffer(ByteBuffer(string: responseBody)))) + logger.trace("Writing response end") + try await outbound.write(HTTPServerResponsePart.end(nil)) + } + + private enum Mode: String { + case string + case json + } + + private static func env(_ name: String) -> String? { + guard let value = getenv(name) else { + return nil + } + return String(cString: value) + } + + private enum AmazonHeaders { + static let requestID = "Lambda-Runtime-Aws-Request-Id" + static let traceID = "Lambda-Runtime-Trace-Id" + static let clientContext = "X-Amz-Client-Context" + static let cognitoIdentity = "X-Amz-Cognito-Identity" + static let deadline = "Lambda-Runtime-Deadline-Ms" + static let invokedFunctionARN = "Lambda-Runtime-Invoked-Function-Arn" + } + + private final class SharedCounter: Sendable { + private let counterMutex = Mutex(0) + private let maxValue: Int + + init(maxValue: Int) { + self.maxValue = maxValue + } + func current() -> Int { + counterMutex.withLock { $0 } + } + func increment() -> Bool { + counterMutex.withLock { + $0 += 1 + return $0 >= maxValue + } + } + } +} diff --git a/Sources/MockServer/main.swift b/Sources/MockServer/main.swift deleted file mode 100644 index 1b8466f9..00000000 --- a/Sources/MockServer/main.swift +++ /dev/null @@ -1,177 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2018 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 Dispatch -import NIOCore -import NIOHTTP1 -import NIOPosix - -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import Foundation -#endif - -struct MockServer { - private let group: EventLoopGroup - private let host: String - private let port: Int - private let mode: Mode - - public init() { - self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - self.host = env("HOST") ?? "127.0.0.1" - self.port = env("PORT").flatMap(Int.init) ?? 7000 - self.mode = env("MODE").flatMap(Mode.init) ?? .string - } - - func start() throws { - 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(mode: self.mode)) - } - } - try bootstrap.bind(host: self.host, port: self.port).flatMap { channel -> EventLoopFuture in - guard let localAddress = channel.localAddress else { - return channel.eventLoop.makeFailedFuture(ServerError.cantBind) - } - print("\(self) started and listening on \(localAddress)") - return channel.eventLoop.makeSucceededFuture(()) - }.wait() - } -} - -final class HTTPHandler: ChannelInboundHandler { - public typealias InboundIn = HTTPServerRequestPart - public typealias OutboundOut = HTTPServerResponsePart - - private let mode: Mode - - private var pending = CircularBuffer<(head: HTTPRequestHead, body: ByteBuffer?)>() - - public init(mode: Mode) { - self.mode = mode - } - - 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?)) { - var responseStatus: HTTPResponseStatus - var responseBody: String? - var responseHeaders: [(String, String)]? - - if request.head.uri.hasSuffix("/next") { - let requestId = UUID().uuidString - responseStatus = .ok - switch self.mode { - case .string: - responseBody = requestId - case .json: - responseBody = "{ \"body\": \"\(requestId)\" }" - } - let deadline = Int64(Date(timeIntervalSinceNow: 60).timeIntervalSince1970 * 1000) - responseHeaders = [ - (AmazonHeaders.requestID, requestId), - (AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime"), - (AmazonHeaders.traceID, "Root=1-5bef4de7-ad49b0e87f6ef6c87fc2e700;Parent=9a9197af755a6419;Sampled=1"), - (AmazonHeaders.deadline, String(deadline)), - ] - } else if request.head.uri.hasSuffix("/response") { - responseStatus = .accepted - } else { - responseStatus = .notFound - } - self.writeResponse(context: context, status: responseStatus, headers: responseHeaders, body: responseBody) - } - - func writeResponse( - context: ChannelHandlerContext, - status: HTTPResponseStatus, - headers: [(String, String)]? = nil, - body: String? = nil - ) { - var headers = HTTPHeaders(headers ?? []) - headers.add(name: "content-length", value: "\(body?.utf8.count ?? 0)") - let head = HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: status, headers: headers) - - context.write(wrapOutboundOut(.head(head))).whenFailure { error in - print("\(self) write error \(error)") - } - - if let b = body { - var buffer = context.channel.allocator.buffer(capacity: b.utf8.count) - buffer.writeString(b) - context.write(wrapOutboundOut(.body(.byteBuffer(buffer)))).whenFailure { error in - print("\(self) write error \(error)") - } - } - - context.writeAndFlush(wrapOutboundOut(.end(nil))).whenComplete { result in - if case .failure(let error) = result { - print("\(self) write error \(error)") - } - } - } -} - -enum ServerError: Error { - case notReady - case cantBind -} - -enum AmazonHeaders { - static let requestID = "Lambda-Runtime-Aws-Request-Id" - static let traceID = "Lambda-Runtime-Trace-Id" - static let clientContext = "X-Amz-Client-Context" - static let cognitoIdentity = "X-Amz-Cognito-Identity" - static let deadline = "Lambda-Runtime-Deadline-Ms" - static let invokedFunctionARN = "Lambda-Runtime-Invoked-Function-Arn" -} - -enum Mode: String { - case string - case json -} - -func env(_ name: String) -> String? { - guard let value = getenv(name) else { - return nil - } - return String(cString: value) -} - -// main -let server = MockServer() -try! server.start() -dispatchMain() diff --git a/Tests/AWSLambdaRuntimeTests/CollectEverythingLogHandler.swift b/Tests/AWSLambdaRuntimeTests/CollectEverythingLogHandler.swift new file mode 100644 index 00000000..537847d8 --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/CollectEverythingLogHandler.swift @@ -0,0 +1,138 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2025 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 Logging +import Synchronization +import Testing + +struct CollectEverythingLogHandler: LogHandler { + var metadata: Logger.Metadata = [:] + var logLevel: Logger.Level = .info + let logStore: LogStore + + final class LogStore: Sendable { + struct Entry: Sendable { + var level: Logger.Level + var message: String + var metadata: [String: String] + } + + let logs: Mutex<[Entry]> = .init([]) + + func append(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?) { + self.logs.withLock { entries in + entries.append( + Entry( + level: level, + message: message.description, + metadata: metadata?.mapValues { $0.description } ?? [:] + ) + ) + } + } + + func clear() { + self.logs.withLock { + $0.removeAll() + } + } + + enum LogFieldExpectedValue: ExpressibleByStringLiteral, ExpressibleByStringInterpolation { + case exactMatch(String) + case beginsWith(String) + case wildcard + case predicate((String) -> Bool) + + init(stringLiteral value: String) { + self = .exactMatch(value) + } + } + + @discardableResult + func assertContainsLog( + _ message: String, + _ metadata: (String, LogFieldExpectedValue)..., + sourceLocation: SourceLocation = #_sourceLocation + ) -> [Entry] { + var candidates = self.getAllLogsWithMessage(message) + if candidates.isEmpty { + Issue.record("Logs do not contain entry with message: \(message)", sourceLocation: sourceLocation) + return [] + } + for (key, value) in metadata { + var errorMsg: String + switch value { + case .wildcard: + candidates = candidates.filter { $0.metadata.contains { $0.key == key } } + errorMsg = "Logs do not contain entry with message: \(message) and metadata: \(key) *" + case .predicate(let predicate): + candidates = candidates.filter { $0.metadata[key].map(predicate) ?? false } + errorMsg = + "Logs do not contain entry with message: \(message) and metadata: \(key) matching predicate" + case .beginsWith(let prefix): + candidates = candidates.filter { $0.metadata[key]?.hasPrefix(prefix) ?? false } + errorMsg = "Logs do not contain entry with message: \(message) and metadata: \(key), \(value)" + case .exactMatch(let value): + candidates = candidates.filter { $0.metadata[key] == value } + errorMsg = "Logs do not contain entry with message: \(message) and metadata: \(key), \(value)" + } + if candidates.isEmpty { + Issue.record("Error: \(errorMsg)", sourceLocation: sourceLocation) + return [] + } + } + return candidates + } + + func assertDoesNotContainMessage(_ message: String, sourceLocation: SourceLocation = #_sourceLocation) { + let candidates = self.getAllLogsWithMessage(message) + if candidates.count > 0 { + Issue.record("Logs contain entry with message: \(message)", sourceLocation: sourceLocation) + } + } + + func getAllLogs() -> [Entry] { + self.logs.withLock { $0 } + } + + func getAllLogsWithMessage(_ message: String) -> [Entry] { + self.getAllLogs().filter { $0.message == message } + } + } + + init(logStore: LogStore) { + self.logStore = logStore + } + + func log( + level: Logger.Level, + message: Logger.Message, + metadata: Logger.Metadata?, + source: String, + file: String, + function: String, + line: UInt + ) { + self.logStore.append(level: level, message: message, metadata: self.metadata.merging(metadata ?? [:]) { $1 }) + } + + subscript(metadataKey key: String) -> Logger.Metadata.Value? { + get { + self.metadata[key] + } + set { + self.metadata[key] = newValue + } + } +} diff --git a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift b/Tests/AWSLambdaRuntimeTests/ControlPlaneRequestEncoderTests.swift similarity index 94% rename from Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift rename to Tests/AWSLambdaRuntimeTests/ControlPlaneRequestEncoderTests.swift index df1a4044..840a17c6 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift +++ b/Tests/AWSLambdaRuntimeTests/ControlPlaneRequestEncoderTests.swift @@ -17,7 +17,7 @@ import NIOEmbedded import NIOHTTP1 import XCTest -@testable import AWSLambdaRuntimeCore +@testable import AWSLambdaRuntime final class ControlPlaneRequestEncoderTests: XCTestCase { let host = "192.168.0.1" @@ -49,7 +49,7 @@ final class ControlPlaneRequestEncoderTests: XCTestCase { XCTAssertEqual(request?.head.uri, "/2018-06-01/runtime/invocation/next") XCTAssertEqual(request?.head.version, .http1_1) XCTAssertEqual(request?.head.headers["host"], [self.host]) - XCTAssertEqual(request?.head.headers["user-agent"], ["Swift-Lambda/Unknown"]) + XCTAssertEqual(request?.head.headers["user-agent"], [.userAgent]) XCTAssertNil(try self.server.readInbound(as: NIOHTTPServerRequestFull.self)) } @@ -64,7 +64,7 @@ final class ControlPlaneRequestEncoderTests: XCTestCase { XCTAssertEqual(request?.head.uri, "/2018-06-01/runtime/invocation/\(requestID)/response") XCTAssertEqual(request?.head.version, .http1_1) XCTAssertEqual(request?.head.headers["host"], [self.host]) - XCTAssertEqual(request?.head.headers["user-agent"], ["Swift-Lambda/Unknown"]) + XCTAssertEqual(request?.head.headers["user-agent"], [.userAgent]) XCTAssertEqual(request?.head.headers["content-length"], ["0"]) XCTAssertNil(try self.server.readInbound(as: NIOHTTPServerRequestFull.self)) @@ -82,7 +82,7 @@ final class ControlPlaneRequestEncoderTests: XCTestCase { XCTAssertEqual(request?.head.uri, "/2018-06-01/runtime/invocation/\(requestID)/response") XCTAssertEqual(request?.head.version, .http1_1) XCTAssertEqual(request?.head.headers["host"], [self.host]) - XCTAssertEqual(request?.head.headers["user-agent"], ["Swift-Lambda/Unknown"]) + XCTAssertEqual(request?.head.headers["user-agent"], [.userAgent]) XCTAssertEqual(request?.head.headers["content-length"], ["\(payload.readableBytes)"]) XCTAssertEqual(request?.body, payload) @@ -100,7 +100,7 @@ final class ControlPlaneRequestEncoderTests: XCTestCase { XCTAssertEqual(request?.head.uri, "/2018-06-01/runtime/invocation/\(requestID)/error") XCTAssertEqual(request?.head.version, .http1_1) XCTAssertEqual(request?.head.headers["host"], [self.host]) - XCTAssertEqual(request?.head.headers["user-agent"], ["Swift-Lambda/Unknown"]) + XCTAssertEqual(request?.head.headers["user-agent"], [.userAgent]) XCTAssertEqual(request?.head.headers["lambda-runtime-function-error-type"], ["Unhandled"]) let expectedBody = #"{"errorType":"SomeError","errorMessage":"An error happened"}"# @@ -123,7 +123,7 @@ final class ControlPlaneRequestEncoderTests: XCTestCase { XCTAssertEqual(request?.head.uri, "/2018-06-01/runtime/init/error") XCTAssertEqual(request?.head.version, .http1_1) XCTAssertEqual(request?.head.headers["host"], [self.host]) - XCTAssertEqual(request?.head.headers["user-agent"], ["Swift-Lambda/Unknown"]) + XCTAssertEqual(request?.head.headers["user-agent"], [.userAgent]) XCTAssertEqual(request?.head.headers["lambda-runtime-function-error-type"], ["Unhandled"]) let expectedBody = #"{"errorType":"StartupError","errorMessage":"Urgh! Startup failed. 😨"}"# XCTAssertEqual(request?.head.headers["content-length"], ["\(expectedBody.utf8.count)"]) diff --git a/Tests/AWSLambdaRuntimeCoreTests/InvocationTests.swift b/Tests/AWSLambdaRuntimeTests/InvocationTests.swift similarity index 97% rename from Tests/AWSLambdaRuntimeCoreTests/InvocationTests.swift rename to Tests/AWSLambdaRuntimeTests/InvocationTests.swift index fca58391..ea4eef1f 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/InvocationTests.swift +++ b/Tests/AWSLambdaRuntimeTests/InvocationTests.swift @@ -15,7 +15,7 @@ import NIOHTTP1 import Testing -@testable import AWSLambdaRuntimeCore +@testable import AWSLambdaRuntime #if canImport(FoundationEssentials) import FoundationEssentials diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaMockClient.swift b/Tests/AWSLambdaRuntimeTests/LambdaMockClient.swift similarity index 97% rename from Tests/AWSLambdaRuntimeCoreTests/LambdaMockClient.swift rename to Tests/AWSLambdaRuntimeTests/LambdaMockClient.swift index 613276ed..7714c84a 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaMockClient.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaMockClient.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -import AWSLambdaRuntimeCore +import AWSLambdaRuntime import Logging import NIOCore @@ -214,12 +214,12 @@ final actor LambdaMockClient: LambdaRuntimeClientProtocol { let eventProcessedHandler: CheckedContinuation } - func invoke(event: ByteBuffer) async throws -> ByteBuffer { + func invoke(event: ByteBuffer, requestID: String = UUID().uuidString) async throws -> ByteBuffer { try await withCheckedThrowingContinuation { eventProcessedHandler in do { let metadata = try InvocationMetadata( headers: .init([ - ("Lambda-Runtime-Aws-Request-Id", "100"), // arbitrary values + ("Lambda-Runtime-Aws-Request-Id", "\(requestID)"), // arbitrary values ("Lambda-Runtime-Deadline-Ms", "100"), ("Lambda-Runtime-Invoked-Function-Arn", "100"), ]) diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRequestIDTests.swift similarity index 99% rename from Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift rename to Tests/AWSLambdaRuntimeTests/LambdaRequestIDTests.swift index 45886c64..a144e20c 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRequestIDTests.swift @@ -15,7 +15,7 @@ import NIOCore import Testing -@testable import AWSLambdaRuntimeCore +@testable import AWSLambdaRuntime #if canImport(FoundationEssentials) import FoundationEssentials diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRunLoopTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRunLoopTests.swift similarity index 72% rename from Tests/AWSLambdaRuntimeCoreTests/LambdaRunLoopTests.swift rename to Tests/AWSLambdaRuntimeTests/LambdaRunLoopTests.swift index f57f051b..3253238e 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaRunLoopTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRunLoopTests.swift @@ -16,7 +16,7 @@ import Logging import NIOCore import Testing -@testable import AWSLambdaRuntimeCore +@testable import AWSLambdaRuntime #if canImport(FoundationEssentials) import FoundationEssentials @@ -32,6 +32,7 @@ struct LambdaRunLoopTests { responseWriter: some LambdaResponseStreamWriter, context: LambdaContext ) async throws { + context.logger.info("Test") try await responseWriter.writeAndFinish(event) } } @@ -42,6 +43,7 @@ struct LambdaRunLoopTests { responseWriter: some LambdaResponseStreamWriter, context: LambdaContext ) async throws { + context.logger.info("Test") throw LambdaError.handlerError } } @@ -54,16 +56,22 @@ struct LambdaRunLoopTests { let inputEvent = ByteBuffer(string: "Test Invocation Event") try await withThrowingTaskGroup(of: Void.self) { group in + let logStore = CollectEverythingLogHandler.LogStore() group.addTask { try await Lambda.runLoop( runtimeClient: self.mockClient, handler: self.mockEchoHandler, - logger: Logger(label: "RunLoopTest") + logger: Logger( + label: "RunLoopTest", + factory: { _ in CollectEverythingLogHandler(logStore: logStore) } + ) ) } - let response = try await self.mockClient.invoke(event: inputEvent) + let requestID = UUID().uuidString + let response = try await self.mockClient.invoke(event: inputEvent, requestID: requestID) #expect(response == inputEvent) + logStore.assertContainsLog("Test", ("aws-request-id", .exactMatch(requestID))) group.cancelAll() } @@ -73,20 +81,26 @@ struct LambdaRunLoopTests { let inputEvent = ByteBuffer(string: "Test Invocation Event") await withThrowingTaskGroup(of: Void.self) { group in + let logStore = CollectEverythingLogHandler.LogStore() group.addTask { try await Lambda.runLoop( runtimeClient: self.mockClient, handler: self.failingHandler, - logger: Logger(label: "RunLoopTest") + logger: Logger( + label: "RunLoopTest", + factory: { _ in CollectEverythingLogHandler(logStore: logStore) } + ) ) } + let requestID = UUID().uuidString await #expect( throws: LambdaError.handlerError, performing: { - try await self.mockClient.invoke(event: inputEvent) + try await self.mockClient.invoke(event: inputEvent, requestID: requestID) } ) + logStore.assertContainsLog("Test", ("aws-request-id", .exactMatch(requestID))) group.cancelAll() } diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift similarity index 59% rename from Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTests.swift rename to Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift index e779b931..53afbaf6 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift @@ -19,7 +19,7 @@ import Testing import struct Foundation.UUID -@testable import AWSLambdaRuntimeCore +@testable import AWSLambdaRuntime @Suite struct LambdaRuntimeClientTests { @@ -86,4 +86,56 @@ struct LambdaRuntimeClientTests { } } } + + @Test + func testCancellation() async throws { + struct HappyBehavior: LambdaServerBehavior { + let requestId = UUID().uuidString + let event = "hello" + + func getInvocation() -> GetInvocationResult { + .success((self.requestId, self.event)) + } + + func processResponse(requestId: String, response: String?) -> Result { + #expect(self.requestId == requestId) + #expect(self.event == response) + return .success(()) + } + + func processError(requestId: String, error: ErrorResponse) -> Result { + Issue.record("should not report error") + return .failure(.internalServerError) + } + + func processInitError(error: ErrorResponse) -> Result { + Issue.record("should not report init error") + return .failure(.internalServerError) + } + } + + try await withMockServer(behaviour: HappyBehavior()) { port in + try await LambdaRuntimeClient.withRuntimeClient( + configuration: .init(ip: "127.0.0.1", port: port), + eventLoop: NIOSingletons.posixEventLoopGroup.next(), + logger: self.logger + ) { runtimeClient in + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while true { + let (_, writer) = try await runtimeClient.nextInvocation() + // Wrap this is a task so cancellation isn't propagated to the write calls + try await Task { + try await writer.write(ByteBuffer(string: "hello")) + try await writer.finish() + }.value + } + } + // wait a small amount to ensure we are waiting for continuation + try await Task.sleep(for: .milliseconds(100)) + group.cancelAll() + } + } + } + } } diff --git a/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift b/Tests/AWSLambdaRuntimeTests/MockLambdaServer.swift similarity index 99% rename from Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift rename to Tests/AWSLambdaRuntimeTests/MockLambdaServer.swift index dc325742..11f43ba4 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift +++ b/Tests/AWSLambdaRuntimeTests/MockLambdaServer.swift @@ -17,7 +17,7 @@ import NIOCore import NIOHTTP1 import NIOPosix -@testable import AWSLambdaRuntimeCore +@testable import AWSLambdaRuntime #if canImport(FoundationEssentials) import FoundationEssentials diff --git a/Tests/AWSLambdaRuntimeCoreTests/Utils.swift b/Tests/AWSLambdaRuntimeTests/Utils.swift similarity index 100% rename from Tests/AWSLambdaRuntimeCoreTests/Utils.swift rename to Tests/AWSLambdaRuntimeTests/Utils.swift diff --git a/Tests/AWSLambdaRuntimeCoreTests/UtilsTest.swift b/Tests/AWSLambdaRuntimeTests/UtilsTest.swift similarity index 97% rename from Tests/AWSLambdaRuntimeCoreTests/UtilsTest.swift rename to Tests/AWSLambdaRuntimeTests/UtilsTest.swift index bdc0b0e5..0b3b5917 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/UtilsTest.swift +++ b/Tests/AWSLambdaRuntimeTests/UtilsTest.swift @@ -14,7 +14,7 @@ import XCTest -@testable import AWSLambdaRuntimeCore +@testable import AWSLambdaRuntime class UtilsTest: XCTestCase { func testGenerateXRayTraceID() { diff --git a/readme.md b/readme.md index 4d1ad7a4..37596ed2 100644 --- a/readme.md +++ b/readme.md @@ -1,10 +1,7 @@ > [!IMPORTANT] > The documentation included here refers to the Swift AWS Lambda Runtime v2 (code from the main branch). If you're developing for the runtime v1.x, check this [readme](https://github.com/swift-server/swift-aws-lambda-runtime/blob/v1/readme.md) instead. -> [!WARNING] -> The Swift AWS Runtime v2 is work in progress. We will add more documentation and code examples over time. - -This guide contains the follwoing sections: +This guide contains the following sections: - [The Swift AWS Lambda Runtime](#the-swift-aws-lambda-runtime) - [Pre-requisites](#pre-requisites) @@ -42,7 +39,7 @@ Swift AWS Lambda Runtime was designed to make building Lambda functions in Swift To get started, read [the Swift AWS Lambda runtime tutorial](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/main/tutorials/table-of-content). It provides developers with detailed step-by-step instructions to develop, build, and deploy a Lambda function. -We also wrote a comprehensive [deployment guide](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/main/documentation/awslambdaruntimecore/deployment). +We also wrote a comprehensive [deployment guide](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/main/documentation/awslambdaruntime/deployment). Or, if you're impatient to start with runtime v2, try these six steps: @@ -282,7 +279,7 @@ try await runtime.run() ### Integration with Swift Service LifeCycle -tbd + link to docc +Support for [Swift Service Lifecycle](https://github.com/swift-server/swift-service-lifecycle) is currently being implemented. You can follow https://github.com/swift-server/swift-aws-lambda-runtime/issues/374 for more details and teh current status. Your contributions are welcome. ### Use Lambda Background Tasks @@ -382,7 +379,7 @@ LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT=/2015-03-31/functions/function/invocatio ## Deploying your Swift Lambda functions -There is a full deployment guide available in [the documentation](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/main/documentation/awslambdaruntimecore/deployment). +There is a full deployment guide available in [the documentation](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/main/documentation/awslambdaruntime/deployment). There are multiple ways to deploy your Swift code to AWS Lambda. The very first time, you'll probably use the AWS Console to create a new Lambda function and upload your code as a zip file. However, as you iterate on your code, you'll want to automate the deployment process. @@ -424,9 +421,9 @@ Please refer to the full deployment guide available in [the documentation](https ## 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 [design document](Sources/AWSLambdaRuntime/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). +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/AWSLambdaRuntime/Documentation.docc/Proposals/0001-v2-api.md). ### Key Design Principles diff --git a/scripts/ubuntu-install-swift.sh b/scripts/ubuntu-install-swift.sh new file mode 100644 index 00000000..5ff58f46 --- /dev/null +++ b/scripts/ubuntu-install-swift.sh @@ -0,0 +1,70 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2025 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 +## +##===----------------------------------------------------------------------===## + +sudo apt update && sudo apt -y upgrade + +# Install Swift 6.0.3 +sudo apt-get -y install \ + binutils \ + git \ + gnupg2 \ + libc6-dev \ + libcurl4-openssl-dev \ + libedit2 \ + libgcc-13-dev \ + libncurses-dev \ + libpython3-dev \ + libsqlite3-0 \ + libstdc++-13-dev \ + libxml2-dev \ + libz3-dev \ + pkg-config \ + tzdata \ + unzip \ + zip \ + zlib1g-dev + +wget https://download.swift.org/swift-6.0.3-release/ubuntu2404-aarch64/swift-6.0.3-RELEASE/swift-6.0.3-RELEASE-ubuntu24.04-aarch64.tar.gz + +tar xfvz swift-6.0.3-RELEASE-ubuntu24.04-aarch64.tar.gz + +export PATH=/home/ubuntu/swift-6.0.3-RELEASE-ubuntu24.04-aarch64/usr/bin:"${PATH}" + +swift --version + +# Install Docker +sudo apt-get update +sudo apt-get install -y ca-certificates curl +sudo install -m 0755 -d /etc/apt/keyrings +sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc +sudo chmod a+r /etc/apt/keyrings/docker.asc + +# Add the repository to Apt sources: +# shellcheck source=/etc/os-release +# shellcheck disable=SC1091 +. /etc/os-release +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ + $VERSION_CODENAME stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +sudo apt-get update + +sudo apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + +# Add the current user to the docker group +sudo usermod -aG docker "$USER" + +# LOGOUT and LOGIN to apply the changes +exit 0 diff --git a/scripts/ubuntu-test-plugin.sh b/scripts/ubuntu-test-plugin.sh new file mode 100644 index 00000000..19d74609 --- /dev/null +++ b/scripts/ubuntu-test-plugin.sh @@ -0,0 +1,28 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2025 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 +## +##===----------------------------------------------------------------------===## + +# Connect with ssh + +export PATH=/home/ubuntu/swift-6.0.3-RELEASE-ubuntu24.04-aarch64/usr/bin:"${PATH}" + +# clone a project +git clone https://github.com/swift-server/swift-aws-lambda-runtime.git + +# be sure Swift is install. +# Youc an install swift with the following command: ./scripts/ubuntu-install-swift.sh + +# build the project +cd swift-aws-lambda-runtime/Examples/ResourcesPackaging/ || exit 1 +LAMBDA_USE_LOCAL_DEPS=../.. swift package archive --allow-network-connections docker