diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..068475e9 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,39 @@ +{ + "name": "Swift", + "image": "swift:6.0", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "false", + "username": "vscode", + "upgradePackages": "false" + }, + "ghcr.io/devcontainers/features/git:1": { + "version": "os-provided", + "ppa": "false" + } + }, + "runArgs": [ + "--cap-add=SYS_PTRACE", + "--security-opt", + "seccomp=unconfined" + ], + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "lldb.library": "/usr/lib/liblldb.so" + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "sswg.swift-lang" + ] + } + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/.github/ISSUE_TEMPLATE/issue-report.yml b/.github/ISSUE_TEMPLATE/issue-report.yml index e5f5d4e9..ae6e0ba3 100644 --- a/.github/ISSUE_TEMPLATE/issue-report.yml +++ b/.github/ISSUE_TEMPLATE/issue-report.yml @@ -17,7 +17,7 @@ body: attributes: label: Actual behavior description: What actually happened - placeholder: Describe + placeholder: Describe validations: required: true - type: textarea @@ -55,7 +55,7 @@ body: attributes: label: Swift version description: Swift environment version. - placeholder: | + placeholder: | Open a Terminal and execute the following command swift --version && uname -a diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 00000000..e29eb846 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,14 @@ +changelog: + categories: + - title: SemVer Major + labels: + - ⚠️ semver/major + - title: SemVer Minor + labels: + - 🆕 semver/minor + - title: SemVer Patch + labels: + - 🔨 semver/patch + - title: Other Changes + labels: + - semver/none diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml new file mode 100644 index 00000000..8abf473a --- /dev/null +++ b/.github/workflows/integration_tests.yml @@ -0,0 +1,166 @@ +name: IntegrationTests + +# As per Checkov CKV2_GHA_1 +permissions: read-all + +on: + workflow_call: + inputs: + name: + type: string + description: "The name of the workflow used for the concurrency group." + required: true + # 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: + type: string + description: "The list of examples to run. Pass a String with a valid JSON array such as \"[ 'HelloWorld', 'APIGateway' ]\"" + required: true + default: "" + examples_enabled: + 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." + default: true + check_foundation_enabled: + type: boolean + description: "Boolean to enable the check for Foundation dependency. Defaults to true." + default: true + matrix_linux_command: + type: string + description: "The command of the current Swift version linux matrix job to execute." + required: true + matrix_linux_swift_container_image: + type: string + # Note: we don't use Amazon Linux 2 here because zip is not installed by default. + description: "Container image for the matrix test jobs. Defaults to Swift 6.2 on Amazon Linux 2." + default: "swift:6.2-amazonlinux2" + +## We are cancelling previously triggered workflow runs +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.name }} + cancel-in-progress: true + +jobs: + test-examples: + name: Test Examples/${{ matrix.examples }} on ${{ matrix.swift.image }} + if: ${{ inputs.examples_enabled }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + examples: ${{ fromJson(inputs.examples) }} + + # We are using only one Swift version + swift: + - image: ${{ inputs.matrix_linux_swift_container_image }} + container: + image: ${{ matrix.swift.image }} + steps: + # GitHub checkout action has a dep on NodeJS 20 which is not running on Amazonlinux2 + # workaround is to manually checkout the repository + # https://github.com/actions/checkout/issues/1487 + - name: Manually Clone repository and checkout PR + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + # Clone the repository + git clone https://github.com/${{ github.repository }} + cd ${{ github.event.repository.name }} + + # Fetch the pull request + git fetch origin +refs/pull/$PR_NUMBER/merge: + + # Checkout the pull request + git checkout -qf FETCH_HEAD + + # - name: Checkout repository + # uses: actions/checkout@v4 + # with: + # persist-credentials: false + + - name: Mark the workspace as safe + working-directory: ${{ github.event.repository.name }} # until we can use action/checkout@v4 + # https://github.com/actions/checkout/issues/766 + run: git config --global --add safe.directory ${GITHUB_WORKSPACE} + + - name: Run matrix job + working-directory: ${{ github.event.repository.name }} # until we can use action/checkout@v4 + env: + COMMAND: ${{ inputs.matrix_linux_command }} + EXAMPLE: ${{ matrix.examples }} + run: | + .github/workflows/scripts/integration_tests.sh + + test-archive-plugin: + name: Test archive plugin + if: ${{ inputs.archive_plugin_enabled }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + examples: ${{ fromJson(inputs.archive_plugin_examples) }} + # These must run on Ubuntu and not in a container, because the plugin uses docker + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Mark the workspace as safe + # 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 + + check-foundation: + name: No dependencies on Foundation + if: ${{ inputs.check_foundation_enabled }} + runs-on: ubuntu-latest + container: + image: ${{ inputs.matrix_linux_swift_container_image }} + steps: + # GitHub checkout action has a dep on NodeJS 20 which is not running on Amazonlinux2 + # workaround is to manually checkout the repository + # https://github.com/actions/checkout/issues/1487 + - name: Manually Clone repository and checkout PR + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + # Clone the repository + git clone https://github.com/${{ github.repository }} + cd ${{ github.event.repository.name }} + + # Fetch the pull request + git fetch origin +refs/pull/$PR_NUMBER/merge: + + # Checkout the pull request + git checkout -qf FETCH_HEAD + + # - name: Checkout repository + # uses: actions/checkout@v4 + # with: + # persist-credentials: false + + - name: Mark the workspace as safe + working-directory: ${{ github.event.repository.name }} # until we can use action/checkout@v4 + # https://github.com/actions/checkout/issues/766 + run: git config --global --add safe.directory ${GITHUB_WORKSPACE} + + - name: Check for Foundation or ICU dependency + working-directory: ${{ github.event.repository.name }} # until we can use action/checkout@v4 + run: | + .github/workflows/scripts/check-link-foundation.sh diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 00000000..be5613de --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,74 @@ +name: PR + +on: + pull_request: + types: [opened, reopened, synchronize] + +# As per Checkov CKV2_GHA_1 +permissions: read-all + +jobs: + soundness: + name: Soundness + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main + with: + license_header_check_project_name: "SwiftAWSLambdaRuntime" + shell_check_enabled: true + python_lint_check_enabled: true + api_breakage_check_container_image: "swift:6.2-noble" + docs_check_container_image: "swift:6.2-noble" + format_check_container_image: "swift:6.2-noble" + yamllint_check_enabled: true + + unit-tests: + name: Unit tests + uses: apple/swift-nio/.github/workflows/unit_tests.yml@main + with: + linux_5_9_enabled: false + linux_5_10_enabled: false + linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error" + linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error" + + integration-tests: + name: Integration Tests + uses: ./.github/workflows/integration_tests.yml + with: + name: "Integration tests" + examples_enabled: true + matrix_linux_command: "LAMBDA_USE_LOCAL_DEPS=../.. swift build" + # 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: "[ 'APIGatewayV1', 'APIGatewayV2', 'APIGatewayV2+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HelloWorldNoTraits', 'HummingbirdLambda', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Streaming+Codable', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]" + archive_plugin_examples: "[ 'HelloWorld', 'ResourcesPackaging' ]" + archive_plugin_enabled: true + + swift-6-language-mode: + name: Swift 6 Language Mode + uses: apple/swift-nio/.github/workflows/swift_6_language_mode.yml@main + + semver-label-check: + name: Semantic Version label check + runs-on: ubuntu-latest + timeout-minutes: 1 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Check for Semantic Version label + uses: apple/swift-nio/.github/actions/pull_request_semver_label_checker@main + + # until there is a support for musl in swiftlang/github-workflows + # https://github.com/swiftlang/github-workflows/issues/34 + musl: + runs-on: ubuntu-latest + container: swift:6.0.2-noble + timeout-minutes: 30 + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Install SDK + run: swift sdk install https://download.swift.org/swift-6.0.2-release/static-sdk/swift-6.0.2-RELEASE/swift-6.0.2-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz --checksum aa5515476a403797223fc2aad4ca0c3bf83995d5427fb297cab1d93c68cee075 + - name: Build + run: swift build --swift-sdk x86_64-swift-linux-musl diff --git a/.github/workflows/scripts/check-archive-plugin.sh b/.github/workflows/scripts/check-archive-plugin.sh new file mode 100755 index 00000000..218ee79a --- /dev/null +++ b/.github/workflows/scripts/check-archive-plugin.sh @@ -0,0 +1,54 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2017-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 +## +##===----------------------------------------------------------------------===## + +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 + +# 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 + +# did the plugin generated a Linux binary? +[ -f "${OUTPUT_FILE}" ] +file "${OUTPUT_FILE}" | grep --silent ELF + +# did the plugin created a ZIP file? +[ -f "${ZIP_FILE}" ] + +# does the ZIP file contain the bootstrap? +unzip -l "${ZIP_FILE}" | grep --silent bootstrap + +# if EXAMPLE is ResourcesPackaging, check if the ZIP file contains hello.txt +if [ "$EXAMPLE" == "ResourcesPackaging" ]; then + echo "Checking if resource was added to the ZIP file" + unzip -l "${ZIP_FILE}" | grep --silent hello.txt + SUCCESS=$? + if [ "$SUCCESS" -eq 1 ]; then + log "❌ Resource not found." && exit 1 + else + log "✅ Resource found." + fi +fi + +echo "✅ The archive plugin is OK with example ${EXAMPLE}" +popd || exit 1 diff --git a/.github/workflows/scripts/check-link-foundation.sh b/.github/workflows/scripts/check-link-foundation.sh new file mode 100755 index 00000000..ccef6ee7 --- /dev/null +++ b/.github/workflows/scripts/check-link-foundation.sh @@ -0,0 +1,60 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2017-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 +## +##===----------------------------------------------------------------------===## + +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { error "$@"; exit 1; } + +EXAMPLE=APIGatewayV2 +OUTPUT_DIR=.build/release +OUTPUT_FILE=${OUTPUT_DIR}/APIGatewayLambda +LIBS_TO_CHECK="libFoundation.so libFoundationInternationalization.so lib_FoundationICU.so" + +pushd Examples/${EXAMPLE} || fatal "Failed to change directory to Examples/${EXAMPLE}." + +# recompile the example without the --static-swift-stdlib flag +LAMBDA_USE_LOCAL_DEPS=../.. swift build -c release -Xlinker -s || fatal "Failed to build the example." + +# check if the binary exists +if [ ! -f "${OUTPUT_FILE}" ]; then + error "❌ ${OUTPUT_FILE} does not exist." +fi + +# Checking for Foundation or ICU dependencies +echo "Checking for Foundation or ICU dependencies in ${OUTPUT_DIR}/${OUTPUT_FILE}." +LIBRARIES=$(ldd ${OUTPUT_FILE} | awk '{print $1}') +for LIB in ${LIBS_TO_CHECK}; do + echo -n "Checking for ${LIB}... " + + # check if the binary has a dependency on Foundation or ICU + echo "${LIBRARIES}" | grep "${LIB}" # return 1 if not found + + # 1 is success (grep failed to find the lib), 0 is failure (grep successly found the lib) + SUCCESS=$? + if [ "$SUCCESS" -eq 0 ]; then + log "❌ ${LIB} found." && break + else + log "✅ ${LIB} not found." + fi +done + +popd || fatal "Failed to change directory back to the root directory." + +# exit code is the opposite of the grep exit code +if [ "$SUCCESS" -eq 0 ]; then + fatal "❌ At least one foundation lib was found, reporting the error." +else + log "✅ No foundation lib found, congrats!" && exit 0 +fi \ No newline at end of file diff --git a/Examples/Deployment/scripts/serverless-deploy.sh b/.github/workflows/scripts/integration_tests.sh similarity index 56% rename from Examples/Deployment/scripts/serverless-deploy.sh rename to .github/workflows/scripts/integration_tests.sh index 241ee7bf..8d11b313 100755 --- a/Examples/Deployment/scripts/serverless-deploy.sh +++ b/.github/workflows/scripts/integration_tests.sh @@ -13,17 +13,20 @@ ## ##===----------------------------------------------------------------------===## -set -eu +set -euo pipefail -DIR="$(cd "$(dirname "$0")" && pwd)" -source $DIR/config.sh +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { error "$@"; exit 1; } -echo -e "\ndeploying $executable" +SWIFT_VERSION=$(swift --version) +test -n "${SWIFT_VERSION:-}" || fatal "SWIFT_VERSION unset" +test -n "${COMMAND:-}" || fatal "COMMAND unset" +test -n "${EXAMPLE:-}" || fatal "EXAMPLE unset" -$DIR/build-and-package.sh "$executable" +pushd Examples/"$EXAMPLE" > /dev/null -echo "-------------------------------------------------------------------------" -echo "deploying using Serverless" -echo "-------------------------------------------------------------------------" +log "Running command with Swift $SWIFT_VERSION" +eval "$COMMAND" -serverless deploy --config "./scripts/serverless/$executable-template.yml" --stage dev -v +popd diff --git a/.gitignore b/.gitignore index 000f5669..c9321c27 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store *.build +*.index-build /.xcodeproj *.pem .podspecs @@ -9,3 +10,9 @@ xcuserdata Package.resolved .serverless .vscode +Makefile +.devcontainer +.amazonq +.kiro +nodejs +.ash \ No newline at end of file diff --git a/.licenseignore b/.licenseignore new file mode 100644 index 00000000..acc480a8 --- /dev/null +++ b/.licenseignore @@ -0,0 +1,38 @@ +.gitignore +.licenseignore +.swiftformatignore +.spi.yml +.swift-format +.github/* +*.md +**/*.md +CONTRIBUTORS.txt +LICENSE.txt +NOTICE.txt +Package.swift +Package@swift-*.swift +Package.resolved +**/*.docc/* +**/.gitignore +**/Package.swift +**/Package.resolved +**/docker-compose*.yaml +**/docker/* +**/.dockerignore +**/Dockerfile +**/Makefile +**/*.html +**/*-template.yml +**/*.xcworkspace/* +**/*.xcodeproj/* +**/*.xcassets/* +**/*.appiconset/* +**/ResourcePackaging/hello.txt +.mailmap +.swiftformat +*.yaml +*.yml +**/.npmignore +**/*.json +**/*.txt +*.toml \ No newline at end of file diff --git a/.mailmap b/.mailmap index 91b18339..59f7e426 100644 --- a/.mailmap +++ b/.mailmap @@ -2,4 +2,5 @@ Tomer Doron Tomer Doron Tomer Doron Fabian Fett -Fabian Fett \ No newline at end of file +Fabian Fett +Sébastien Stormacq \ No newline at end of file diff --git a/.spi.yml b/.spi.yml index 713eb24a..9c13e3e4 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,4 +1,4 @@ version: 1 builder: configs: - - documentation_targets: [AWSLambdaRuntime, AWSLambdaRuntimeCore] + - documentation_targets: [AWSLambdaRuntime] diff --git a/.swift-format b/.swift-format new file mode 100644 index 00000000..7fa06fb3 --- /dev/null +++ b/.swift-format @@ -0,0 +1,62 @@ +{ + "version" : 1, + "indentation" : { + "spaces" : 4 + }, + "tabWidth" : 4, + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "private" + }, + "spacesAroundRangeFormationOperators" : false, + "indentConditionalCompilationBlocks" : false, + "indentSwitchCaseLabels" : false, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : true, + "lineBreakBeforeEachGenericRequirement" : true, + "lineLength" : 120, + "maximumBlankLines" : 1, + "respectsExistingLineBreaks" : true, + "prioritizeKeepingFunctionOutputTogether" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : false, + "AlwaysUseLiteralForEmptyCollectionInit" : false, + "AlwaysUseLowerCamelCase" : false, + "AmbiguousTrailingClosureOverload" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : true, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : false, + "NeverUseForceTry" : false, + "NeverUseImplicitlyUnwrappedOptionals" : false, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : true, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : true, + "NoLeadingUnderscores" : false, + "NoParensAroundConditions" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OmitExplicitReturns" : true, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : true, + "ReplaceForEachWithForLoop" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "UseEarlyExits" : false, + "UseExplicitNilCheckInConditions" : false, + "UseLetInEveryBoundCaseVariable" : false, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : false, + "UseSynthesizedInitializer" : false, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : false, + "ValidateDocumentationComments" : false + } +} diff --git a/.swiftformat b/.swiftformat deleted file mode 100644 index 2458d1bc..00000000 --- a/.swiftformat +++ /dev/null @@ -1,19 +0,0 @@ -# file options - ---swiftversion 5.4 ---exclude .build - -# format options - ---self insert ---patternlet inline ---stripunusedargs unnamed-only ---ifdef no-indent ---extensionacl on-declarations ---disable typeSugar ---disable andOperator ---disable wrapMultilineStatementBraces ---disable enumNamespaces ---disable redundantExtensionACL - -# rules diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index c2e31f97..77d546ad 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -8,26 +8,66 @@ needs to be listed here. ## COPYRIGHT HOLDERS - Apple Inc. (all contributors with '@apple.com') +- Amazon.com, Inc. or its affiliates (all contributors with '@amazon.com') ### Contributors - Adam Fowler +- Adolfo +- Alessio Buratti <9006089+Buratti@users.noreply.github.com> - Andrea Scuderi +- Bill <3207996+gestrich@users.noreply.github.com> - Brendan Kirchner - Bryan Bartow - Bryan Moffatt +- Camden Fullmer - Christoph Walcher - Colton Schlosser +- Cory Benfield +- Dmitry Platonov +- DwayneCoussement +- DwayneCoussement - Eneko Alonso -- Fabian Fett +- Fabian Fett +- Filipp Fediakov +- Florent Morin +- Franz Busch +- Franz Busch - George Barnett +- Jack Rosen +- Joannis Orlandos +- Joel Saltzman +- Johannes Bosecker - Johannes Weiss +- Josh <29730338+mr-j-tree@users.noreply.github.com> +- Juan A. Reyes <59104004+jareyesda@users.noreply.github.com> +- Konrad `ktoso` Malawski +- ML <44809298+mufumade@users.noreply.github.com> +- Marwane Koutar <100198937+MarwaneKoutar@users.noreply.github.com> +- Matt Massicotte <85322+mattmassicotte@users.noreply.github.com> - Max Desiatov +- Natan Rolnik - Norman Maurer +- Paul Toffoloni <69189821+ptoffy@users.noreply.github.com> +- Ralph Küpper - Ro-M +- Stefan Nienhuis +- Sven A. Schmidt +- Sébastien Stormacq +- Tim Condon <0xTim@users.noreply.github.com> +- Tobias - Tomer Doron +- YR Chen +- Yim Lee - Zhibin Cai +- aryan-25 +- dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +- jsonfry +- mattcorey - pmarrufo +- pokryfka <5090827+pokryfka@users.noreply.github.com> +- pokryfka +- sja26 - tachyonics **Updating this list** diff --git a/Examples/APIGatewayV1/Package.swift b/Examples/APIGatewayV1/Package.swift new file mode 100644 index 00000000..f52f9d74 --- /dev/null +++ b/Examples/APIGatewayV1/Package.swift @@ -0,0 +1,56 @@ +// swift-tools-version:6.2 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "APIGatewayLambda", targets: ["APIGatewayLambda"]) + ], + 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", from: "2.0.0"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.0.0"), + ], + targets: [ + .executableTarget( + name: "APIGatewayLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ], + path: "Sources" + ) + ] +) + +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/APIGatewayV1/README.md b/Examples/APIGatewayV1/README.md new file mode 100644 index 00000000..110efc2c --- /dev/null +++ b/Examples/APIGatewayV1/README.md @@ -0,0 +1,152 @@ +# REST API Gateway + +This is a simple example of an AWS Lambda function invoked through an Amazon API Gateway V1. + +> [!NOTE] +> This example uses the API Gateway V1 `Rest Api` endpoint type, whereas the [API Gateway V2](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/APIGateway) example uses the `HttpApi` endpoint type. For more information, see [Choose between REST APIs and HTTP APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html). + +## Code + +The Lambda function takes all HTTP headers it receives as input and returns them as output. + +The code creates a `LambdaRuntime` struct. In it's simplest form, the initializer takes a function as argument. The function is the handler that will be invoked when the API Gateway receives an HTTP request. + +The handler is `(event: APIGatewayRequest, context: LambdaContext) -> APIGatewayResponse`. The function takes two arguments: +- the event argument is a `APIGatewayRequest`. It is the parameter passed by the API Gateway. It contains all data passed in the HTTP request and some meta data. +- the context argument is a `Lambda Context`. It is a description of the runtime context. + +The function must return a `APIGatewayResponse`. + +`APIGatewayRequest` and `APIGatewayResponse` are defined in the [Swift AWS Lambda Events](https://github.com/swift-server/swift-aws-lambda-events) library. + +## Build & Package + +To build the package, type the following commands. + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip` + +## Deploy + +The deployment must include the Lambda function and the API Gateway. We use the [Serverless Application Model (SAM)](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) to deploy the infrastructure. + +**Prerequisites** : Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) + +The example directory contains a file named `template.yaml` that describes the deployment. + +To actually deploy your Lambda function and create the infrastructure, type the following `sam` command. + +```bash +sam deploy \ +--resolve-s3 \ +--template-file template.yaml \ +--stack-name APIGatewayLambda \ +--capabilities CAPABILITY_IAM +``` + +At the end of the deployment, the script lists the API Gateway endpoint. +The output is similar to this one. + +``` +----------------------------------------------------------------------------------------------------------------------------- +Outputs +----------------------------------------------------------------------------------------------------------------------------- +Key APIGatewayEndpoint +Description API Gateway endpoint URL" +Value https://a5q74es3k2.execute-api.us-east-1.amazonaws.com +----------------------------------------------------------------------------------------------------------------------------- +``` + +## Invoke your Lambda function + +To invoke the Lambda function, use this `curl` command line. + +```bash +curl https://a5q74es3k2.execute-api.us-east-1.amazonaws.com +``` + +Be sure to replace the URL with the API Gateway endpoint returned in the previous step. + +This should print a JSON similar to + +```bash +{"httpMethod":"GET","queryStringParameters":{},"isBase64Encoded":false,"resource":"\/","path":"\/","headers":{"X-Forwarded-Port":"3000","X-Forwarded-Proto":"http","User-Agent":"curl\/8.7.1","Host":"localhost:3000","Accept":"*\/*"},"requestContext":{"resourcePath":"\/","identity":{"sourceIp":"127.0.0.1","userAgent":"Custom User Agent String"},"httpMethod":"GET","resourceId":"123456","accountId":"123456789012","apiId":"1234567890","requestId":"a9d2db08-8364-4da4-8237-8912bf8148c8","domainName":"localhost:3000","stage":"Prod","path":"\/"},"multiValueQueryStringParameters":{},"pathParameters":{},"multiValueHeaders":{"Accept":["*\/*"],"Host":["localhost:3000"],"X-Forwarded-Port":["3000"],"User-Agent":["curl\/8.7.1"],"X-Forwarded-Proto":["http"]},"stageVariables":{}} +``` + +If you have `jq` installed, you can use it to pretty print the output. + +```bash +curl -s https://a5q74es3k2.execute-api.us-east-1.amazonaws.com | jq +{ + "stageVariables": {}, + "queryStringParameters": {}, + "multiValueHeaders": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/8.7.1" + ], + "X-Forwarded-Proto": [ + "http" + ], + "Host": [ + "localhost:3000" + ], + "X-Forwarded-Port": [ + "3000" + ] + }, + "pathParameters": {}, + "isBase64Encoded": false, + "path": "/", + "requestContext": { + "apiId": "1234567890", + "stage": "Prod", + "httpMethod": "GET", + "domainName": "localhost:3000", + "requestId": "a9d2db08-8364-4da4-8237-8912bf8148c8", + "identity": { + "userAgent": "Custom User Agent String", + "sourceIp": "127.0.0.1" + }, + "resourceId": "123456", + "path": "/", + "resourcePath": "/", + "accountId": "123456789012" + }, + "multiValueQueryStringParameters": {}, + "resource": "/", + "headers": { + "Accept": "*/*", + "X-Forwarded-Proto": "http", + "X-Forwarded-Port": "3000", + "Host": "localhost:3000", + "User-Agent": "curl/8.7.1" + }, + "httpMethod": "GET" +} +``` + +## Undeploy + +When done testing, you can delete the infrastructure with this command. + +```bash +sam delete +``` + +## ⚠️ Security and Reliability Notice + +These are example applications for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: + +- Enable access logging on API Gateway ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)) +- Ensure that AWS Lambda function is configured for function-level concurrent execution limit ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +- Check encryption settings for Lambda environment variables ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +- Ensure that AWS Lambda function is configured for a Dead Letter Queue (DLQ) ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +- Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html), [code example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres)) \ No newline at end of file diff --git a/Examples/Testing/Tests/LambdaTests.swift b/Examples/APIGatewayV1/Sources/main.swift similarity index 50% rename from Examples/Testing/Tests/LambdaTests.swift rename to Examples/APIGatewayV1/Sources/main.swift index f6676c10..2f65d84f 100644 --- a/Examples/Testing/Tests/LambdaTests.swift +++ b/Examples/APIGatewayV1/Sources/main.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2021 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,15 +12,19 @@ // //===----------------------------------------------------------------------===// +import AWSLambdaEvents import AWSLambdaRuntime -import AWSLambdaTesting -@testable import MyLambda -import XCTest -class LambdaTest: XCTestCase { - func testIt() async throws { - let input = UUID().uuidString - let result = try await Lambda.test(MyLambda.self, with: input) - XCTAssertEqual(result, String(input.reversed())) - } +let runtime = LambdaRuntime { + (event: APIGatewayRequest, context: LambdaContext) -> APIGatewayResponse in + + var header = HTTPHeaders() + context.logger.debug("Rest API Message received") + + header["content-type"] = "application/json" + + // echo the request in the response + return try APIGatewayResponse(statusCode: .ok, headers: header, encodableBody: event) } + +try await runtime.run() diff --git a/Examples/APIGatewayV1/template.yaml b/Examples/APIGatewayV1/template.yaml new file mode 100644 index 00000000..f32d8d81 --- /dev/null +++ b/Examples/APIGatewayV1/template.yaml @@ -0,0 +1,50 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for APIGateway Lambda Example + +# This is an example SAM template for the purpose of this project. +# When deploying such infrastructure in production environment, +# we strongly encourage you to follow these best practices for improved security and resiliency +# - Enable access loggin on API Gateway +# See: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html) +# - Ensure that AWS Lambda function is configured for function-level concurrent execution limit +# See: https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html +# https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html +# - Check encryption settings for Lambda environment variable +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html +# - Ensure that AWS Lambda function is configured for a Dead Letter Queue(DLQ) +# See: https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq +# - Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html +# Code Example: https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres + +Resources: + # Lambda function + APIGatewayLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip + Timeout: 60 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 128 + Architectures: + - arm64 + Environment: + Variables: + # by default, AWS Lambda runtime produces no log + # use `LOG_LEVEL: debug` for for lifecycle and event handling information + # use `LOG_LEVEL: trace` for detailed input event information + LOG_LEVEL: debug + Events: + RestApi: + Type: Api + Properties: + Path: / + Method: GET + +Outputs: + # print API Gateway endpoint + APIGatewayEndpoint: + Description: "API Gateway endpoint URL" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/" diff --git a/Examples/APIGatewayV2+LambdaAuthorizer/.gitignore b/Examples/APIGatewayV2+LambdaAuthorizer/.gitignore new file mode 100644 index 00000000..e4044f6f --- /dev/null +++ b/Examples/APIGatewayV2+LambdaAuthorizer/.gitignore @@ -0,0 +1,2 @@ +samconfig.toml +Makefile diff --git a/Examples/APIGatewayV2+LambdaAuthorizer/Package.swift b/Examples/APIGatewayV2+LambdaAuthorizer/Package.swift new file mode 100644 index 00000000..ebbc7133 --- /dev/null +++ b/Examples/APIGatewayV2+LambdaAuthorizer/Package.swift @@ -0,0 +1,63 @@ +// swift-tools-version:6.2 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "APIGatewayLambda", targets: ["APIGatewayLambda"]), + .executable(name: "AuthorizerLambda", targets: ["AuthorizerLambda"]), + ], + 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", from: "2.0.0"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.0.0"), + ], + targets: [ + .executableTarget( + name: "APIGatewayLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ] + ), + .executableTarget( + name: "AuthorizerLambda", + 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/APIGatewayV2+LambdaAuthorizer/README.md b/Examples/APIGatewayV2+LambdaAuthorizer/README.md new file mode 100644 index 00000000..35567333 --- /dev/null +++ b/Examples/APIGatewayV2+LambdaAuthorizer/README.md @@ -0,0 +1,122 @@ +# Lambda Authorizer with HTTPS API Gateway + +This is an example of a Lambda Authorizer function. There are two Lambda functions in this example. The first one is the authorizer function. The second one is the business function. The business function is exposed through a REST API using the HTTPS API Gateway. The API Gateway is configured to use the authorizer function to implement a custom logic to authorize the requests. + +>[!NOTE] +> If your application is protected by JWT tokens, it's recommended to use [the native JWT authorizer provided by the API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-jwt-authorizer.html). The Lambda authorizer is useful when you need to implement a custom authorization logic. See the [OAuth 2.0/JWT authorizer example for AWS SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-controlling-access-to-apis-oauth2-authorizer.html) to learn how to use the native JWT authorizer with SAM. + +## Code + +The authorizer function is a simple function that checks data received from the API Gateway. In this example, the API Gateway is configured to pass the content of the `Authorization` header to the authorizer Lambda function. + +There are two possible responses from a Lambda Authorizer function: policy and simple. The policy response returns an IAM policy document that describes the permissions of the caller. The simple response returns a boolean value that indicates if the caller is authorized or not. You can read more about the two types of responses in the [Lambda authorizer response format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-lambda-authorizer.html) section of the API Gateway documentation. + +This example uses an authorizer that returns the simple response. The authorizer function is defined in the `Sources/AuthorizerLambda` directory. The business function is defined in the `Sources/APIGatewayLambda` directory. + +## Build & Package + +To build the package, type the following commands. + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there is no error, there are two ZIP files ready to deploy, one for the authorizer function and one for the business function. +The ZIP file are located under `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager` + +## Deploy + +The deployment must include the Lambda functions and the API Gateway. We use the [Serverless Application Model (SAM)](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) to deploy the infrastructure. + +**Prerequisites** : Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) + +The example directory contains a file named `template.yaml` that describes the deployment. + +To actually deploy your Lambda function and create the infrastructure, type the following `sam` command. + +```bash +sam deploy \ +--resolve-s3 \ +--template-file template.yaml \ +--stack-name APIGatewayWithLambdaAuthorizer \ +--capabilities CAPABILITY_IAM +``` + +At the end of the deployment, the script lists the API Gateway endpoint. +The output is similar to this one. + +``` +----------------------------------------------------------------------------------------------------------------------------- +Outputs +----------------------------------------------------------------------------------------------------------------------------- +Key APIGatewayEndpoint +Description API Gateway endpoint URI +Value https://a5q74es3k2.execute-api.us-east-1.amazonaws.com/demo +----------------------------------------------------------------------------------------------------------------------------- +``` + +## Invoke your Lambda function + +To invoke the Lambda function, use this `curl` command line. Be sure to replace the URL with the API Gateway endpoint returned in the previous step. + +When invoking the Lambda function without `Authorization` header, the response is a `401 Unauthorized` error. + +```bash +curl -v https://a5q74es3k2.execute-api.us-east-1.amazonaws.com/demo +... +> GET /demo HTTP/2 +> Host: 6sm6270j21.execute-api.us-east-1.amazonaws.com +> User-Agent: curl/8.7.1 +> Accept: */* +> +* Request completely sent off +< HTTP/2 401 +< date: Sat, 04 Jan 2025 14:03:02 GMT +< content-type: application/json +< content-length: 26 +< apigw-requestid: D3bfpidOoAMESiQ= +< +* Connection #0 to host 6sm6270j21.execute-api.us-east-1.amazonaws.com left intact +{"message":"Unauthorized"} +``` + +When invoking the Lambda function with the `Authorization` header, the response is a `200 OK` status code. Note that the Lambda Authorizer function is configured to accept any value in the `Authorization` header. + +```bash +curl -v -H 'Authorization: 123' https://a5q74es3k2.execute-api.us-east-1.amazonaws.com/demo +... +> GET /demo HTTP/2 +> Host: 6sm6270j21.execute-api.us-east-1.amazonaws.com +> User-Agent: curl/8.7.1 +> Accept: */* +> Authorization: 123 +> +* Request completely sent off +< HTTP/2 200 +< date: Sat, 04 Jan 2025 14:04:43 GMT +< content-type: application/json +< content-length: 911 +< apigw-requestid: D3bvRjJcoAMEaig= +< +* Connection #0 to host 6sm6270j21.execute-api.us-east-1.amazonaws.com left intact +{"headers":{"x-forwarded-port":"443","x-forwarded-proto":"https","host":"6sm6270j21.execute-api.us-east-1.amazonaws.com","user-agent":"curl\/8.7.1","accept":"*\/*","content-length":"0","x-amzn-trace-id":"Root=1-67793ffa-05f1296f1a52f8a066180020","authorization":"123","x-forwarded-for":"81.49.207.77"},"routeKey":"ANY \/demo","version":"2.0","rawQueryString":"","isBase64Encoded":false,"queryStringParameters":{},"pathParameters":{},"rawPath":"\/demo","cookies":[],"requestContext":{"domainPrefix":"6sm6270j21","requestId":"D3bvRjJcoAMEaig=","domainName":"6sm6270j21.execute-api.us-east-1.amazonaws.com","stage":"$default","authorizer":{"lambda":{"abc1":"xyz1"}},"timeEpoch":1735999482988,"accountId":"401955065246","time":"04\/Jan\/2025:14:04:42 +0000","http":{"method":"GET","sourceIp":"81.49.207.77","path":"\/demo","userAgent":"curl\/8.7.1","protocol":"HTTP\/1.1"},"apiId":"6sm6270j21"},"stageVariables":{}} +``` + +## Undeploy + +When done testing, you can delete the infrastructure with this command. + +```bash +sam delete --stack-name APIGatewayWithLambdaAuthorizer +``` + +## ⚠️ Security and Reliability Notice + +These are example applications for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: + +- Enable access logging on API Gateway ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)) +- Ensure that AWS Lambda function is configured for function-level concurrent execution limit ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +- Check encryption settings for Lambda environment variables ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +- Ensure that AWS Lambda function is configured for a Dead Letter Queue (DLQ) ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +- Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html), [code example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres)) \ No newline at end of file diff --git a/Examples/APIGatewayV2+LambdaAuthorizer/Sources/APIGatewayLambda/main.swift b/Examples/APIGatewayV2+LambdaAuthorizer/Sources/APIGatewayLambda/main.swift new file mode 100644 index 00000000..f7662d1c --- /dev/null +++ b/Examples/APIGatewayV2+LambdaAuthorizer/Sources/APIGatewayLambda/main.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// 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 AWSLambdaEvents +import AWSLambdaRuntime + +let runtime = LambdaRuntime { + (event: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response in + + var header = HTTPHeaders() + context.logger.debug("HTTP API Message received") + + header["content-type"] = "application/json" + + // echo the request in the response + return try APIGatewayV2Response(statusCode: .ok, headers: header, encodableBody: event) +} + +try await runtime.run() diff --git a/Examples/APIGatewayV2+LambdaAuthorizer/Sources/AuthorizerLambda/main.swift b/Examples/APIGatewayV2+LambdaAuthorizer/Sources/AuthorizerLambda/main.swift new file mode 100644 index 00000000..60ea2b7b --- /dev/null +++ b/Examples/APIGatewayV2+LambdaAuthorizer/Sources/AuthorizerLambda/main.swift @@ -0,0 +1,79 @@ +//===----------------------------------------------------------------------===// +// +// 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 AWSLambdaEvents +import AWSLambdaRuntime + +// +// This is an example of a policy authorizer that always authorizes the request. +// The policy authorizer returns an IAM policy document that defines what the Lambda function caller can do and optional context key-value pairs +// +// This code is shown for the example only and is not used in this demo. +// This code doesn't perform any type of token validation. It should be used as a reference only. +let policyAuthorizerHandler: + (APIGatewayLambdaAuthorizerRequest, LambdaContext) async throws -> APIGatewayLambdaAuthorizerPolicyResponse = { + (request: APIGatewayLambdaAuthorizerRequest, context: LambdaContext) in + + context.logger.debug("+++ Policy Authorizer called +++") + + // typically, this function will check the validity of the incoming token received in the request + + // then it creates and returns a response + return APIGatewayLambdaAuthorizerPolicyResponse( + principalId: "John Appleseed", + + // this policy allows the caller to invoke any API Gateway endpoint + policyDocument: .init(statement: [ + .init( + action: "execute-api:Invoke", + effect: .allow, + resource: "*" + ) + + ]), + + // this is additional context we want to return to the caller + context: [ + "abc1": "xyz1", + "abc2": "xyz2", + ] + ) + } + +// +// This is an example of a simple authorizer that always authorizes the request. +// A simple authorizer returns a yes/no decision and optional context key-value pairs +// +// This code doesn't perform any type of token validation. It should be used as a reference only. +let simpleAuthorizerHandler: + (APIGatewayLambdaAuthorizerRequest, LambdaContext) async throws -> APIGatewayLambdaAuthorizerSimpleResponse = { + (_: APIGatewayLambdaAuthorizerRequest, context: LambdaContext) in + + context.logger.debug("+++ Simple Authorizer called +++") + + // typically, this function will check the validity of the incoming token received in the request + + return APIGatewayLambdaAuthorizerSimpleResponse( + // this is the authorization decision: yes or no + isAuthorized: true, + + // this is additional context we want to return to the caller + context: ["abc1": "xyz1"] + ) + } + +// create the runtime and start polling for new events. +// in this demo we use the simple authorizer handler +let runtime = LambdaRuntime(body: simpleAuthorizerHandler) +try await runtime.run() diff --git a/Examples/APIGatewayV2+LambdaAuthorizer/template.yaml b/Examples/APIGatewayV2+LambdaAuthorizer/template.yaml new file mode 100644 index 00000000..de70fccd --- /dev/null +++ b/Examples/APIGatewayV2+LambdaAuthorizer/template.yaml @@ -0,0 +1,93 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for APIGateway Lambda Example + +# This is an example SAM template for the purpose of this project. +# When deploying such infrastructure in production environment, +# we strongly encourage you to follow these best practices for improved security and resiliency +# - Enable access loggin on API Gateway +# See: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html) +# - Ensure that AWS Lambda function is configured for function-level concurrent execution limit +# See: https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html +# https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html +# - Check encryption settings for Lambda environment variable +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html +# - Ensure that AWS Lambda function is configured for a Dead Letter Queue(DLQ) +# See: https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq +# - Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html +# Code Example: https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres + +Resources: + # The API Gateway + MyProtectedApi: + Type: AWS::Serverless::HttpApi + Properties: + Auth: + DefaultAuthorizer: MyLambdaRequestAuthorizer + Authorizers: + MyLambdaRequestAuthorizer: + FunctionArn: !GetAtt AuthorizerLambda.Arn + Identity: + Headers: + - Authorization + AuthorizerPayloadFormatVersion: "2.0" + EnableSimpleResponses: true + + # Give the API Gateway permissions to invoke the Lambda authorizer + AuthorizerPermission: + Type: AWS::Lambda::Permission + Properties: + Action: lambda:InvokeFunction + FunctionName: !Ref AuthorizerLambda + Principal: apigateway.amazonaws.com + SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${MyProtectedApi}/* + + # Lambda business function + APIGatewayLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip + Timeout: 60 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 128 + Architectures: + - arm64 + Environment: + Variables: + # by default, AWS Lambda runtime produces no log + # use `LOG_LEVEL: debug` for for lifecycle and event handling information + # use `LOG_LEVEL: trace` for detailed input event information + LOG_LEVEL: debug + Events: + HttpApiEvent: + Type: HttpApi + Properties: + ApiId: !Ref MyProtectedApi + Path: /demo + Method: ANY + + # Lambda authorizer function + AuthorizerLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/AuthorizerLambda/AuthorizerLambda.zip + Timeout: 29 # max 29 seconds for Lambda authorizers + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 128 + Architectures: + - arm64 + Environment: + Variables: + # by default, AWS Lambda runtime produces no log + # use `LOG_LEVEL: debug` for for lifecycle and event handling information + # use `LOG_LEVEL: trace` for detailed input event information + LOG_LEVEL: debug + +Outputs: + # print API Gateway endpoint + APIGatewayEndpoint: + Description: API Gateway endpoint URI + Value: !Sub "https://${MyProtectedApi}.execute-api.${AWS::Region}.amazonaws.com/demo" diff --git a/Examples/APIGatewayV2/.gitignore b/Examples/APIGatewayV2/.gitignore new file mode 100644 index 00000000..e4044f6f --- /dev/null +++ b/Examples/APIGatewayV2/.gitignore @@ -0,0 +1,2 @@ +samconfig.toml +Makefile diff --git a/Examples/APIGatewayV2/Package.swift b/Examples/APIGatewayV2/Package.swift new file mode 100644 index 00000000..f52f9d74 --- /dev/null +++ b/Examples/APIGatewayV2/Package.swift @@ -0,0 +1,56 @@ +// swift-tools-version:6.2 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "APIGatewayLambda", targets: ["APIGatewayLambda"]) + ], + 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", from: "2.0.0"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.0.0"), + ], + targets: [ + .executableTarget( + name: "APIGatewayLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ], + path: "Sources" + ) + ] +) + +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/APIGatewayV2/README.md b/Examples/APIGatewayV2/README.md new file mode 100644 index 00000000..e7f41c4a --- /dev/null +++ b/Examples/APIGatewayV2/README.md @@ -0,0 +1,137 @@ +# HTTPS API Gateway + +This is a simple example of an AWS Lambda function invoked through an Amazon HTTPS API Gateway. + +> [!NOTE] +> This example uses the API Gateway V2 `Http Api` endpoint type, whereas the [API Gateway V1](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/APIGatewayV1) example uses the `Rest Api` endpoint type. For more information, see [Choose between REST APIs and HTTP APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html). + +## Code + +The Lambda function takes all HTTP headers it receives as input and returns them as output. + +The code creates a `LambdaRuntime` struct. In it's simplest form, the initializer takes a function as argument. The function is the handler that will be invoked when the API Gateway receives an HTTP request. + +The handler is `(event: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response`. The function takes two arguments: +- the event argument is a `APIGatewayV2Request`. It is the parameter passed by the API Gateway. It contains all data passed in the HTTP request and some meta data. +- the context argument is a `Lambda Context`. It is a description of the runtime context. + +The function must return a `APIGatewayV2Response`. + +`APIGatewayV2Request` and `APIGatewayV2Response` are defined in the [Swift AWS Lambda Events](https://github.com/swift-server/swift-aws-lambda-events) library. + +## Build & Package + +To build the package, type the following commands. + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip` + +## Deploy + +The deployment must include the Lambda function and the API Gateway. We use the [Serverless Application Model (SAM)](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) to deploy the infrastructure. + +**Prerequisites** : Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) + +The example directory contains a file named `template.yaml` that describes the deployment. + +To actually deploy your Lambda function and create the infrastructure, type the following `sam` command. + +```bash +sam deploy \ +--resolve-s3 \ +--template-file template.yaml \ +--stack-name APIGatewayLambda \ +--capabilities CAPABILITY_IAM +``` + +At the end of the deployment, the script lists the API Gateway endpoint. +The output is similar to this one. + +``` +----------------------------------------------------------------------------------------------------------------------------- +Outputs +----------------------------------------------------------------------------------------------------------------------------- +Key APIGatewayEndpoint +Description API Gateway endpoint URL" +Value https://a5q74es3k2.execute-api.us-east-1.amazonaws.com +----------------------------------------------------------------------------------------------------------------------------- +``` + +## Invoke your Lambda function + +To invoke the Lambda function, use this `curl` command line. + +```bash +curl https://a5q74es3k2.execute-api.us-east-1.amazonaws.com +``` + +Be sure to replace the URL with the API Gateway endpoint returned in the previous step. + +This should print a JSON similar to + +```bash +{"version":"2.0","rawPath":"\/","isBase64Encoded":false,"rawQueryString":"","headers":{"user-agent":"curl\/8.7.1","accept":"*\/*","host":"a5q74es3k2.execute-api.us-east-1.amazonaws.com","content-length":"0","x-amzn-trace-id":"Root=1-66fb0388-691f744d4bd3c99c7436a78d","x-forwarded-port":"443","x-forwarded-for":"81.0.0.43","x-forwarded-proto":"https"},"requestContext":{"requestId":"e719cgNpoAMEcwA=","http":{"sourceIp":"81.0.0.43","path":"\/","protocol":"HTTP\/1.1","userAgent":"curl\/8.7.1","method":"GET"},"stage":"$default","apiId":"a5q74es3k2","time":"30\/Sep\/2024:20:01:12 +0000","timeEpoch":1727726472922,"domainPrefix":"a5q74es3k2","domainName":"a5q74es3k2.execute-api.us-east-1.amazonaws.com","accountId":"012345678901"} +``` + +If you have `jq` installed, you can use it to pretty print the output. + +```bash +curl -s https://a5q74es3k2.execute-api.us-east-1.amazonaws.com | jq +{ + "version": "2.0", + "rawPath": "/", + "requestContext": { + "domainPrefix": "a5q74es3k2", + "stage": "$default", + "timeEpoch": 1727726558220, + "http": { + "protocol": "HTTP/1.1", + "method": "GET", + "userAgent": "curl/8.7.1", + "path": "/", + "sourceIp": "81.0.0.43" + }, + "apiId": "a5q74es3k2", + "accountId": "012345678901", + "requestId": "e72KxgsRoAMEMSA=", + "domainName": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", + "time": "30/Sep/2024:20:02:38 +0000" + }, + "rawQueryString": "", + "routeKey": "$default", + "headers": { + "x-forwarded-for": "81.0.0.43", + "user-agent": "curl/8.7.1", + "host": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", + "accept": "*/*", + "x-amzn-trace-id": "Root=1-66fb03de-07533930192eaf5f540db0cb", + "content-length": "0", + "x-forwarded-proto": "https", + "x-forwarded-port": "443" + }, + "isBase64Encoded": false +} +``` + +## Undeploy + +When done testing, you can delete the infrastructure with this command. + +```bash +sam delete +``` + +## ⚠️ Security and Reliability Notice + +These are example applications for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: + +- Enable access logging on API Gateway ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)) +- Ensure that AWS Lambda function is configured for function-level concurrent execution limit ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +- Check encryption settings for Lambda environment variables ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +- Ensure that AWS Lambda function is configured for a Dead Letter Queue (DLQ) ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +- Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html), [code example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres)) \ No newline at end of file diff --git a/Examples/APIGatewayV2/Sources/main.swift b/Examples/APIGatewayV2/Sources/main.swift new file mode 100644 index 00000000..f7662d1c --- /dev/null +++ b/Examples/APIGatewayV2/Sources/main.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// 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 AWSLambdaEvents +import AWSLambdaRuntime + +let runtime = LambdaRuntime { + (event: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response in + + var header = HTTPHeaders() + context.logger.debug("HTTP API Message received") + + header["content-type"] = "application/json" + + // echo the request in the response + return try APIGatewayV2Response(statusCode: .ok, headers: header, encodableBody: event) +} + +try await runtime.run() diff --git a/Examples/APIGatewayV2/template.yaml b/Examples/APIGatewayV2/template.yaml new file mode 100644 index 00000000..ce265ab0 --- /dev/null +++ b/Examples/APIGatewayV2/template.yaml @@ -0,0 +1,47 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for APIGateway Lambda Example + +# This is an example SAM template for the purpose of this project. +# When deploying such infrastructure in production environment, +# we strongly encourage you to follow these best practices for improved security and resiliency +# - Enable access loggin on API Gateway +# See: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html) +# - Ensure that AWS Lambda function is configured for function-level concurrent execution limit +# See: https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html +# https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html +# - Check encryption settings for Lambda environment variable +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html +# - Ensure that AWS Lambda function is configured for a Dead Letter Queue(DLQ) +# See: https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq +# - Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html +# Code Example: https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres + +Resources: + # Lambda function + APIGatewayLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip + Timeout: 60 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 128 + Architectures: + - arm64 + Environment: + Variables: + # by default, AWS Lambda runtime produces no log + # use `LOG_LEVEL: debug` for for lifecycle and event handling information + # use `LOG_LEVEL: trace` for detailed input event information + LOG_LEVEL: debug + Events: + HttpApiEvent: + Type: HttpApi + +Outputs: + # print API Gateway endpoint + APIGatewayEndpoint: + Description: API Gateway endpoint UR" + Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" diff --git a/Examples/BackgroundTasks/.gitignore b/Examples/BackgroundTasks/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/Examples/BackgroundTasks/.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/BackgroundTasks/Package.swift b/Examples/BackgroundTasks/Package.swift new file mode 100644 index 00000000..0ed15254 --- /dev/null +++ b/Examples/BackgroundTasks/Package.swift @@ -0,0 +1,54 @@ +// swift-tools-version:6.2 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "BackgroundTasks", targets: ["BackgroundTasks"]) + ], + 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", from: "2.0.0") + ], + targets: [ + .executableTarget( + name: "BackgroundTasks", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: "Sources" + ) + ] +) + +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/BackgroundTasks/README.md b/Examples/BackgroundTasks/README.md new file mode 100644 index 00000000..20449bcc --- /dev/null +++ b/Examples/BackgroundTasks/README.md @@ -0,0 +1,129 @@ +# Background Tasks + +This is an example for running background tasks in an AWS Lambda function. + +Background tasks allow code to execute asynchronously after the main response has been returned, enabling additional processing without affecting response latency. This approach is ideal for scenarios like logging, data updates, or notifications that can be deferred. The code leverages Lambda's "Response Streaming" feature, which is effective for balancing real-time user responsiveness with the ability to perform extended tasks post-response. + +For more information about Lambda background tasks, see [this AWS blog post](https://aws.amazon.com/blogs/compute/running-code-after-returning-a-response-from-an-aws-lambda-function/). + +## Code + +The sample code creates a `BackgroundProcessingHandler` struct that conforms to the `LambdaWithBackgroundProcessingHandler` protocol provided by the Swift AWS Lambda Runtime. + +The `BackgroundProcessingHandler` struct defines the input and output JSON received and returned by the Handler. + +The `handle(...)` method of this protocol receives incoming events as `Input` and returns the output as a `Greeting`. The `handle(...)` methods receives an `outputWriter` parameter to write the output before the function returns, giving some opportunities to run long-lasting tasks after the response has been returned to the client but before the function returns. + +The `handle(...)` method uses the `outputWriter` to return the response as soon as possible. It then waits for 10 seconds to simulate a long background work. When the 10 seconds elapsed, the function returns. The billing cycle ends when the function returns. + +The `handle(...)` method is marked as `mutating` to allow handlers to be implemented with a `struct`. + +Once the struct is created and the `handle(...)` method is defined, the sample code creates a `LambdaCodableAdapter` adapter to adapt the `LambdaWithBackgroundProcessingHandler` to a type accepted by the `LambdaRuntime` struct. Then, the sample code initializes the `LambdaRuntime` with the adapter just created. Finally, the code calls `run()` to start the interaction with the AWS Lambda control plane. + +## Build & Package + +To build & archive the package, type the following commands. + +```bash +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/BackgroundTasks/BackgroundTasks.zip` + +## Deploy with the AWS CLI + +Here is how to deploy using the `aws` command line. + +### Create the function + +```bash +AWS_ACCOUNT_ID=012345678901 +aws lambda create-function \ +--function-name BackgroundTasks \ +--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/BackgroundTasks/BackgroundTasks.zip \ +--runtime provided.al2 \ +--handler provided \ +--architectures arm64 \ +--role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution \ +--environment "Variables={LOG_LEVEL=debug}" \ +--timeout 15 +``` + +> [!IMPORTANT] +> The timeout value must be bigger than the time it takes for your function to complete its background tasks. Otherwise, the Lambda control plane will terminate the execution environment before your code has a chance to finish the tasks. Here, the sample function waits for 10 seconds and we set the timeout for 15 seconds. + +The `--environment` arguments sets the `LOG_LEVEL` environment variable to `debug`. This will ensure the debugging statements in the handler `context.logger.debug("...")` are printed in the Lambda function logs. + +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 set `AWS_ACCOUNT_ID` with your actual AWS account ID (for example: 012345678901). + +### Invoke your Lambda function + +To invoke the Lambda function, use `aws` command line. +```bash +aws lambda invoke \ + --function-name BackgroundTasks \ + --cli-binary-format raw-in-base64-out \ + --payload '{ "message" : "Hello Background Tasks" }' \ + response.json +``` + +This should immediately output the following result. + +``` +{ + "StatusCode": 200, + "ExecutedVersion": "$LATEST" +} +``` + +The response is visible in the `response.json` file. + +```bash +cat response.json +{"echoedMessage":"Hello Background Tasks"} +``` + +### View the function's logs + +You can observe additional messages being logged after the response is received. + +To tail the log, use the AWS CLI: +```bash +aws logs tail /aws/lambda/BackgroundTasks --follow +``` + +This produces an output like: +```text +INIT_START Runtime Version: provided:al2.v59 Runtime Version ARN: arn:aws:lambda:us-east-1::runtime:974c4a90f22278a2ef1c3f53c5c152167318aaf123fbb07c055a4885a4e97e52 +START RequestId: 4c8edd74-d776-4df9-9714-19086ab59bfd Version: $LATEST +debug LambdaRuntime : [BackgroundTasks] BackgroundProcessingHandler - message received +debug LambdaRuntime : [BackgroundTasks] BackgroundProcessingHandler - response sent. Performing background tasks. +debug LambdaRuntime : [BackgroundTasks] BackgroundProcessingHandler - Background tasks completed. Returning +END RequestId: 4c8edd74-d776-4df9-9714-19086ab59bfd +REPORT RequestId: 4c8edd74-d776-4df9-9714-19086ab59bfd Duration: 10160.89 ms Billed Duration: 10250 ms Memory Size: 128 MB Max Memory Used: 27 MB Init Duration: 88.20 ms +``` +> [!NOTE] +> The `debug` message are sent by the code inside the `handler()` function. Note that the `Duration` and `Billed Duration` on the last line are for 10.1 and 10.2 seconds respectively. + +Type CTRL-C to stop tailing the logs. + +## Cleanup + +When done testing, you can delete the Lambda function with this command. + +```bash +aws lambda delete-function --function-name BackgroundTasks +``` + +## ⚠️ Security and Reliability Notice + +These are example applications for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: + +- Enable access logging on API Gateway ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)) +- Ensure that AWS Lambda function is configured for function-level concurrent execution limit ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +- Check encryption settings for Lambda environment variables ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +- Ensure that AWS Lambda function is configured for a Dead Letter Queue (DLQ) ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +- Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html), [code example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres)) \ No newline at end of file diff --git a/Examples/BackgroundTasks/Sources/main.swift b/Examples/BackgroundTasks/Sources/main.swift new file mode 100644 index 00000000..1985fc34 --- /dev/null +++ b/Examples/BackgroundTasks/Sources/main.swift @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +struct BackgroundProcessingHandler: LambdaWithBackgroundProcessingHandler { + struct Input: Decodable { + let message: String + } + + struct Greeting: Encodable { + let echoedMessage: String + } + + typealias Event = Input + typealias Output = Greeting + + func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + // Return result to the Lambda control plane + context.logger.debug("BackgroundProcessingHandler - message received") + try await outputWriter.write(Greeting(echoedMessage: event.message)) + + // Perform some background work, e.g: + context.logger.debug("BackgroundProcessingHandler - response sent. Performing background tasks.") + try await Task.sleep(for: .seconds(10)) + + // Exit the function. All asynchronous work has been executed before exiting the scope of this function. + // Follows structured concurrency principles. + context.logger.debug("BackgroundProcessingHandler - Background tasks completed. Returning") + return + } +} + +let adapter = LambdaCodableAdapter(handler: BackgroundProcessingHandler()) +let runtime = LambdaRuntime.init(handler: adapter) +try await runtime.run() diff --git a/Examples/Benchmark/BenchmarkHandler.swift b/Examples/Benchmark/BenchmarkHandler.swift deleted file mode 100644 index d9cfce6e..00000000 --- a/Examples/Benchmark/BenchmarkHandler.swift +++ /dev/null @@ -1,32 +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 -// -//===----------------------------------------------------------------------===// - -import AWSLambdaRuntimeCore -import NIOCore - -// If you would like to benchmark Swift's Lambda Runtime, -// use this example which is more performant. -// `EventLoopLambdaHandler` does not offload the Lambda processing to a separate thread -// while the closure-based handlers do. - -@main -struct BenchmarkHandler: EventLoopLambdaHandler { - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture(BenchmarkHandler()) - } - - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture("hello, world!") - } -} diff --git a/Examples/Benchmark/Package.swift b/Examples/Benchmark/Package.swift deleted file mode 100644 index c6370ce6..00000000 --- a/Examples/Benchmark/Package.swift +++ /dev/null @@ -1,33 +0,0 @@ -// swift-tools-version:5.7 - -import class Foundation.ProcessInfo // needed for CI to test the local version of the library -import PackageDescription - -let package = Package( - name: "swift-aws-lambda-runtime-example", - platforms: [ - .macOS(.v12), - ], - products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), - ], - dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), - ], - targets: [ - .executableTarget( - name: "MyLambda", - dependencies: [ - .product(name: "AWSLambdaRuntimeCore", package: "swift-aws-lambda-runtime"), - ], - path: "." - ), - ] -) - -// for CI to test the local version of the library -if ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"] != nil { - package.dependencies = [ - .package(name: "swift-aws-lambda-runtime", path: "../.."), - ] -} diff --git a/Examples/CDK/Package.swift b/Examples/CDK/Package.swift new file mode 100644 index 00000000..f52f9d74 --- /dev/null +++ b/Examples/CDK/Package.swift @@ -0,0 +1,56 @@ +// swift-tools-version:6.2 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "APIGatewayLambda", targets: ["APIGatewayLambda"]) + ], + 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", from: "2.0.0"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.0.0"), + ], + targets: [ + .executableTarget( + name: "APIGatewayLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ], + path: "Sources" + ) + ] +) + +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/CDK/README.md b/Examples/CDK/README.md new file mode 100644 index 00000000..a1d08727 --- /dev/null +++ b/Examples/CDK/README.md @@ -0,0 +1,131 @@ +# API Gateway and Cloud Development Kit + +This is a simple example of an AWS Lambda function invoked through an Amazon API Gateway and deployed with the Cloud Development Kit (CDK). + +## Code + +The Lambda function takes all HTTP headers it receives as input and returns them as output. See the [API Gateway example](Examples/APIGateway/README.md) for a complete description of the code. + +## Build & Package + +To build the package, type the following commands. + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip` + +## Deploy + +>[NOTE] +>Before deploying the infrastructure, you need to have NodeJS and the AWS CDK installed and configured. +>For more information, see the [AWS CDK documentation](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html). + +To deploy the infrastructure, type the following commands. + +```sh +# Change to the infra directory +cd infra + +# Install the dependencies (only before the first deployment) +npm install + +cdk deploy + +✨ Synthesis time: 2.88s +... redacted for brevity ... +Do you wish to deploy these changes (y/n)? y +... redacted for brevity ... + ✅ LambdaApiStack + +✨ Deployment time: 42.96s + +Outputs: +LambdaApiStack.ApiUrl = https://tyqnjcawh0.execute-api.eu-central-1.amazonaws.com/ +Stack ARN: +arn:aws:cloudformation:eu-central-1:401955065246:stack/LambdaApiStack/e0054390-be05-11ef-9504-065628de4b89 + +✨ Total time: 45.84s +``` + +## Invoke your Lambda function + +To invoke the Lambda function, use this `curl` command line. + +```bash +curl https://tyqnjcawh0.execute-api.eu-central-1.amazonaws.com +``` + +Be sure to replace the URL with the API Gateway endpoint returned in the previous step. + +This should print a JSON similar to + +```bash +{"version":"2.0","rawPath":"\/","isBase64Encoded":false,"rawQueryString":"","headers":{"user-agent":"curl\/8.7.1","accept":"*\/*","host":"a5q74es3k2.execute-api.us-east-1.amazonaws.com","content-length":"0","x-amzn-trace-id":"Root=1-66fb0388-691f744d4bd3c99c7436a78d","x-forwarded-port":"443","x-forwarded-for":"81.0.0.43","x-forwarded-proto":"https"},"requestContext":{"requestId":"e719cgNpoAMEcwA=","http":{"sourceIp":"81.0.0.43","path":"\/","protocol":"HTTP\/1.1","userAgent":"curl\/8.7.1","method":"GET"},"stage":"$default","apiId":"a5q74es3k2","time":"30\/Sep\/2024:20:01:12 +0000","timeEpoch":1727726472922,"domainPrefix":"a5q74es3k2","domainName":"a5q74es3k2.execute-api.us-east-1.amazonaws.com","accountId":"012345678901"} +``` + +If you have `jq` installed, you can use it to pretty print the output. + +```bash +curl -s https://tyqnjcawh0.execute-api.eu-central-1.amazonaws.com | jq +{ + "version": "2.0", + "rawPath": "/", + "requestContext": { + "domainPrefix": "a5q74es3k2", + "stage": "$default", + "timeEpoch": 1727726558220, + "http": { + "protocol": "HTTP/1.1", + "method": "GET", + "userAgent": "curl/8.7.1", + "path": "/", + "sourceIp": "81.0.0.43" + }, + "apiId": "a5q74es3k2", + "accountId": "012345678901", + "requestId": "e72KxgsRoAMEMSA=", + "domainName": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", + "time": "30/Sep/2024:20:02:38 +0000" + }, + "rawQueryString": "", + "routeKey": "$default", + "headers": { + "x-forwarded-for": "81.0.0.43", + "user-agent": "curl/8.7.1", + "host": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", + "accept": "*/*", + "x-amzn-trace-id": "Root=1-66fb03de-07533930192eaf5f540db0cb", + "content-length": "0", + "x-forwarded-proto": "https", + "x-forwarded-port": "443" + }, + "isBase64Encoded": false +} +``` + +## Undeploy + +When done testing, you can delete the infrastructure with this command. + +```bash +cdk destroy + +Are you sure you want to delete: LambdaApiStack (y/n)? y +LambdaApiStack: destroying... [1/1] +... redacted for brevity ... + ✅ LambdaApiStack: destroyed +``` + +## ⚠️ Security and Reliability Notice + +These are example applications for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: + +- Enable access logging on API Gateway ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)) +- Ensure that AWS Lambda function is configured for function-level concurrent execution limit ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +- Check encryption settings for Lambda environment variables ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +- Ensure that AWS Lambda function is configured for a Dead Letter Queue (DLQ) ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +- Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html), [code example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres)) \ No newline at end of file diff --git a/Examples/CDK/Sources/main.swift b/Examples/CDK/Sources/main.swift new file mode 100644 index 00000000..2d5707d6 --- /dev/null +++ b/Examples/CDK/Sources/main.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// 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 AWSLambdaEvents +import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +let encoder = JSONEncoder() +let runtime = LambdaRuntime { + (event: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response in + + var header = HTTPHeaders() + context.logger.debug("HTTP API Message received") + + header["content-type"] = "application/json" + + // echo the request in the response + let data = try encoder.encode(event) + let response = String(decoding: data, as: Unicode.UTF8.self) + + return APIGatewayV2Response(statusCode: .ok, headers: header, body: response) +} + +try await runtime.run() diff --git a/Examples/CDK/infra/.gitignore b/Examples/CDK/infra/.gitignore new file mode 100644 index 00000000..a08f1af9 --- /dev/null +++ b/Examples/CDK/infra/.gitignore @@ -0,0 +1,9 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out + diff --git a/Examples/CDK/infra/.npmignore b/Examples/CDK/infra/.npmignore new file mode 100644 index 00000000..c1d6d45d --- /dev/null +++ b/Examples/CDK/infra/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/Examples/CDK/infra/README.md b/Examples/CDK/infra/README.md new file mode 100644 index 00000000..9315fe5b --- /dev/null +++ b/Examples/CDK/infra/README.md @@ -0,0 +1,14 @@ +# Welcome to your CDK TypeScript project + +This is a blank project for CDK development with TypeScript. + +The `cdk.json` file tells the CDK Toolkit how to execute your app. + +## Useful commands + +* `npm run build` compile typescript to js +* `npm run watch` watch for changes and compile +* `npm run test` perform the jest unit tests +* `npx cdk deploy` deploy this stack to your default AWS account/region +* `npx cdk diff` compare deployed stack with current state +* `npx cdk synth` emits the synthesized CloudFormation template diff --git a/Examples/LocalDebugging/MyApp/MyApp/MyApp.swift b/Examples/CDK/infra/bin/deploy.ts similarity index 66% rename from Examples/LocalDebugging/MyApp/MyApp/MyApp.swift rename to Examples/CDK/infra/bin/deploy.ts index 8b4ae638..a83096da 100644 --- a/Examples/LocalDebugging/MyApp/MyApp/MyApp.swift +++ b/Examples/CDK/infra/bin/deploy.ts @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2021 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,13 +12,8 @@ // //===----------------------------------------------------------------------===// -import SwiftUI +import * as cdk from 'aws-cdk-lib'; +import { LambdaApiStack } from '../lib/lambda-api-project-stack'; -@main -struct MyApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } - } -} +const app = new cdk.App(); +new LambdaApiStack(app, 'LambdaApiStack'); diff --git a/Examples/CDK/infra/cdk.json b/Examples/CDK/infra/cdk.json new file mode 100644 index 00000000..06b03d2f --- /dev/null +++ b/Examples/CDK/infra/cdk.json @@ -0,0 +1,81 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/deploy.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true + } +} diff --git a/Examples/CDK/infra/lib/lambda-api-project-stack.ts b/Examples/CDK/infra/lib/lambda-api-project-stack.ts new file mode 100644 index 00000000..b4aaa3d1 --- /dev/null +++ b/Examples/CDK/infra/lib/lambda-api-project-stack.ts @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// 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 * as cdk from 'aws-cdk-lib'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as apigateway from 'aws-cdk-lib/aws-apigatewayv2'; +import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations'; + +export class LambdaApiStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // Create the Lambda function + const lambdaFunction = new lambda.Function(this, 'SwiftLambdaFunction', { + runtime: lambda.Runtime.PROVIDED_AL2, + architecture: lambda.Architecture.ARM_64, + handler: 'bootstrap', + code: lambda.Code.fromAsset('../.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip'), + memorySize: 128, + timeout: cdk.Duration.seconds(30), + environment: { + LOG_LEVEL: 'debug', + }, + }); + + // Create the integration + const integration = new HttpLambdaIntegration( + 'LambdaIntegration', + lambdaFunction + ); + + // Create HTTP API with the integration + const httpApi = new apigateway.HttpApi(this, 'HttpApi', { + defaultIntegration: integration, + }); + + // Output the API URL + new cdk.CfnOutput(this, 'ApiUrl', { + value: httpApi.url ?? 'Something went wrong', + }); + } +} + diff --git a/Examples/CDK/infra/package-lock.json b/Examples/CDK/infra/package-lock.json new file mode 100644 index 00000000..1d01345f --- /dev/null +++ b/Examples/CDK/infra/package-lock.json @@ -0,0 +1,1182 @@ +{ + "name": "deploy", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "deploy", + "version": "0.1.0", + "dependencies": { + "aws-cdk-lib": "^2.189.1", + "constructs": "^10.4.2" + }, + "bin": { + "deploy": "bin/deploy.js" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "22.13.10", + "aws-cdk": "2.1003.0", + "ts-node": "^10.9.2", + "typescript": "~5.8.2" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.242", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.242.tgz", + "integrity": "sha512-4c1bAy2ISzcdKXYS1k4HYZsNrgiwbiDzj36ybwFVxEWZXVAP0dimQTCaB9fxu7sWzEjw3d+eaw6Fon+QTfTIpQ==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", + "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "45.2.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-45.2.0.tgz", + "integrity": "sha512-5TTUkGHQ+nfuUGwKA8/Yraxb+JdNUh4np24qk/VHXmrCMq+M6HfmGWfhcg/QlHA2S5P3YIamfYHdQAB4uSNLAg==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.2" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.2", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/aws-cdk": { + "version": "2.1003.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1003.0.tgz", + "integrity": "sha512-FORPDGW8oUg4tXFlhX+lv/j+152LO9wwi3/CwNr1WY3c3HwJUtc0fZGb2B3+Fzy6NhLWGHJclUsJPEhjEt8Nhg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 14.15.0" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.206.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.206.0.tgz", + "integrity": "sha512-WQGSSzSX+CvIG3j4GICxCAARGaB2dbB2ZiAn8dqqWdUkF6G9pedlSd3bjB0NHOqrxJMu3jYQCYf3gLYTaJuR8A==", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/asset-awscli-v1": "2.2.242", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^45.0.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.0", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^3.1.2", + "punycode": "^2.3.1", + "semver": "^7.7.2", + "table": "^6.9.0", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "constructs": "^10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.17.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.12", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.0.6", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.7.2", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/constructs": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", + "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==", + "license": "Apache-2.0" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/Examples/CDK/infra/package.json b/Examples/CDK/infra/package.json new file mode 100644 index 00000000..7104e018 --- /dev/null +++ b/Examples/CDK/infra/package.json @@ -0,0 +1,23 @@ +{ + "name": "deploy", + "version": "0.1.0", + "bin": { + "deploy": "bin/deploy.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "cdk": "cdk" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "22.13.10", + "aws-cdk": "2.1003.0", + "ts-node": "^10.9.2", + "typescript": "~5.8.2" + }, + "dependencies": { + "aws-cdk-lib": "^2.189.1", + "constructs": "^10.4.2" + } +} diff --git a/Examples/CDK/infra/tsconfig.json b/Examples/CDK/infra/tsconfig.json new file mode 100644 index 00000000..aaa7dc51 --- /dev/null +++ b/Examples/CDK/infra/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} diff --git a/Examples/Deployment/.dockerignore b/Examples/Deployment/.dockerignore deleted file mode 100644 index 24e5b0a1..00000000 --- a/Examples/Deployment/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -.build diff --git a/Examples/Deployment/Dockerfile b/Examples/Deployment/Dockerfile deleted file mode 100644 index 32962859..00000000 --- a/Examples/Deployment/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM swift:5.5-amazonlinux2 - -RUN yum -y install zip diff --git a/Examples/Deployment/Package.swift b/Examples/Deployment/Package.swift deleted file mode 100644 index 51181f66..00000000 --- a/Examples/Deployment/Package.swift +++ /dev/null @@ -1,36 +0,0 @@ -// swift-tools-version:5.7 - -import class Foundation.ProcessInfo // needed for CI to test the local version of the library -import PackageDescription - -let package = Package( - name: "swift-aws-lambda-runtime-samples", - platforms: [ - .macOS(.v12), - ], - products: [ - // introductory example - .executable(name: "HelloWorld", targets: ["HelloWorld"]), - // good for benchmarking - .executable(name: "Benchmark", targets: ["Benchmark"]), - // demonstrate different types of error handling - ], - dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), - ], - targets: [ - .executableTarget(name: "Benchmark", dependencies: [ - .product(name: "AWSLambdaRuntimeCore", package: "swift-aws-lambda-runtime"), - ]), - .executableTarget(name: "HelloWorld", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - ]), - ] -) - -// for CI to test the local version of the library -if ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"] != nil { - package.dependencies = [ - .package(name: "swift-aws-lambda-runtime", path: "../.."), - ] -} diff --git a/Examples/Deployment/README.md b/Examples/Deployment/README.md deleted file mode 100644 index aca3e391..00000000 --- a/Examples/Deployment/README.md +++ /dev/null @@ -1,184 +0,0 @@ -# Deployment Examples - -This sample project is a collection of Lambda functions that demonstrates -how to write a simple Lambda function in Swift, and how to package and deploy it -to the AWS Lambda platform. - -The scripts are prepared to work from the `Deployment` folder. - -``` -git clone https://github.com/swift-server/swift-aws-lambda-runtime.git -cd swift-aws-lambda-runtime/Examples/Deployment -``` - -Note: The example scripts assume you have [jq](https://stedolan.github.io/jq/download/) command line tool installed. - -## Mac M1 Considerations - -Lambdas will run on an x86 processor by default. Building a Lambda with an M1 will create an arm-based executable which will not run on an x86 processor. Here are a few options for building Swift Lambdas on an M1: - -1. Configure the Lambda to run on the [Graviton2](https://aws.amazon.com/blogs/aws/aws-lambda-functions-powered-by-aws-graviton2-processor-run-your-functions-on-arm-and-get-up-to-34-better-price-performance/) Arm-based processor. -2. Build with the x86 architecture by specifying `--platform linux/amd64` in all Docker 'build' and 'run' commands in `build-and-package.sh`. - -## Deployment instructions using AWS CLI - -Steps to deploy this sample to AWS Lambda using the AWS CLI: - -1. Login to AWS Console and create an AWS Lambda with the following settings: - * Runtime: Custom runtime - * Handler: Can be any string, does not matter in this case - -2. Build, package and deploy the Lambda - - ``` - ./scripts/deploy.sh - ``` - - Notes: - - This script assumes you have AWS CLI installed and credentials setup in `~/.aws/credentials`. - - The default lambda function name is `SwiftSample`. You can specify a different one updating `lambda_name` in `deploy.sh` - - Update `s3_bucket=swift-lambda-test` in `deploy.sh` before running (AWS S3 buckets require a unique global name) - - Both lambda function and S3 bucket must exist before deploying for the first time. - -### Deployment instructions using AWS SAM (Serverless Application Model) - -AWS [Serverless Application Model](https://aws.amazon.com/serverless/sam/) (SAM) is an open-source framework for building serverless applications. This framework allows you to easily deploy other AWS resources and more complex deployment mechanisms such a CI pipelines. - -***Note:*** Deploying using SAM will automatically create resources within your AWS account. Charges may apply for these resources. - -To use SAM to deploy this sample to AWS: - -1. Install the AWS CLI by following the [instructions](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html). - -2. Install SAM CLI by following the [instructions](https://aws.amazon.com/serverless/sam/). - -3. Build, package and deploy the Lambda - - ``` - ./scripts/sam-deploy.sh --guided - ``` - -The script will ask you which sample Lambda you wish to deploy. It will then guide you through the SAM setup process. - - ``` - Setting default arguments for 'sam deploy' - ========================================= - Stack Name [sam-app]: swift-aws-lambda-runtime-sample - AWS Region [us-east-1]: - #Shows you resources changes to be deployed and require a 'Y' to initiate deploy - Confirm changes before deploy [y/N]: Y - #SAM needs permission to be able to create roles to connect to the resources in your template - Allow SAM CLI IAM role creation [Y/n]: Y - Save arguments to samconfig.toml [Y/n]: Y - ``` - -If you said yes to confirm changes, SAM will ask you to accept changes to the infrastructure you are setting up. For more on this, see [Cloud Formation](https://aws.amazon.com/cloudformation/). - -The `sam-deploy` script passes through any parameters to the SAM deploy command. - -4. Subsequent deploys can just use the command minus the `guided` parameter: - - ``` - ./scripts/sam-deploy.sh - ``` - -The script will ask you which sample Lambda you wish to deploy. If you are deploying a different sample lambda, the deploy process will pull down the previous Lambda. - -SAM will still ask you to confirm changes if you said yes to that initially. - -5. Testing - -For the API Gateway sample: - -The SAM template will provide an output labelled `LambdaApiGatewayEndpoint` which you can use to test the Lambda. For example: - - ``` - curl <> - ``` - -***Warning:*** This SAM template is only intended as a sample and creates a publicly accessible HTTP endpoint. - -For all other samples use the AWS Lambda console. - -### Deployment instructions using Serverless Framework (serverless.com) - -[Serverless framework](https://www.serverless.com/open-source/) (Serverless) is a provider agnostic, open-source framework for building serverless applications. This framework allows you to easily deploy other AWS resources and more complex deployment mechanisms such a CI pipelines. Serverless Framework offers solutions for not only deploying but also testing, monitoring, alerting, and security and is widely adopted by the industry and offers along the open-source version a paid one. - -***Note:*** Deploying using Serverless will automatically create resources within your AWS account. Charges may apply for these resources. - -To use Serverless to deploy this sample to AWS: - -1. Install the AWS CLI by following the [instructions](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html). - -2. Install Serverless by following the [instructions](https://www.serverless.com/framework/docs/getting-started/). -If you already have installed be sure you have the latest version. -The examples have been tested with the version 1.72.0. - -``` -Serverless --version -Framework Core: 1.72.0 (standalone) -Plugin: 3.6.13 -SDK: 2.3.1 -Components: 2.30.12 -``` - -3. Build, package and deploy the Lambda - - ``` - ./scripts/serverless-deploy.sh - ``` - -The script will ask you which sample Lambda you wish to deploy. - -The `serverless-deploy.sh` script passes through any parameters to the Serverless deploy command. - -4. Testing - -For the APIGateway sample: - -The Serverless template will provide an endpoint which you can use to test the Lambda. - -Outuput example: - -``` -... -... -Serverless: Stack update finished... -Service Information -service: apigateway-swift-aws -stage: dev -region: us-east-1 -stack: apigateway-swift-aws-dev -resources: 12 -api keys: - None -endpoints: - GET - https://r39lvhfng3.execute-api.us-east-1.amazonaws.com/api -functions: - httpGet: apigateway-swift-aws-dev-httpGet -layers: - None - -Stack Outputs -HttpGetLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:XXXXXXXXX:function:apigateway-swift-aws-dev-httpGet:1 -ServerlessDeploymentBucketName: apigateway-swift-aws-dev-serverlessdeploymentbuck-ud51msgcrj1e -HttpApiUrl: https://r39lvhfng3.execute-api.us-east-1.amazonaws.com -``` - -For example: - - ``` - curl https://r39lvhfng3.execute-api.us-east-1.amazonaws.com/api - ``` - -***Warning:*** This Serverless template is only intended as a sample and creates a publicly accessible HTTP endpoint. - -For all other samples use the AWS Lambda console. - -4. Remove - - ``` - ./scripts/serverless-remove.sh - ``` - -The script will ask you which sample Lambda you wish to remove from the previous deployment. diff --git a/Examples/Deployment/Sources/Benchmark/BenchmarkHandler.swift b/Examples/Deployment/Sources/Benchmark/BenchmarkHandler.swift deleted file mode 100644 index d9cfce6e..00000000 --- a/Examples/Deployment/Sources/Benchmark/BenchmarkHandler.swift +++ /dev/null @@ -1,32 +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 -// -//===----------------------------------------------------------------------===// - -import AWSLambdaRuntimeCore -import NIOCore - -// If you would like to benchmark Swift's Lambda Runtime, -// use this example which is more performant. -// `EventLoopLambdaHandler` does not offload the Lambda processing to a separate thread -// while the closure-based handlers do. - -@main -struct BenchmarkHandler: EventLoopLambdaHandler { - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture(BenchmarkHandler()) - } - - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture("hello, world!") - } -} diff --git a/Examples/Deployment/scripts/SAM/Benchmark-template.yml b/Examples/Deployment/scripts/SAM/Benchmark-template.yml deleted file mode 100644 index 55100d12..00000000 --- a/Examples/Deployment/scripts/SAM/Benchmark-template.yml +++ /dev/null @@ -1,14 +0,0 @@ -AWSTemplateFormatVersion : '2010-09-09' -Transform: AWS::Serverless-2016-10-31 -Description: A sample SAM template for deploying Lambda functions. - -Resources: -# Benchmark Function - benchmarkFunction: - Type: AWS::Serverless::Function - Properties: - Handler: Provided - Runtime: provided - CodeUri: ../../.build/lambda/Benchmark/lambda.zip -# Instructs new versions to be published to an alias named "live". - AutoPublishAlias: live diff --git a/Examples/Deployment/scripts/SAM/HelloWorld-template.yml b/Examples/Deployment/scripts/SAM/HelloWorld-template.yml deleted file mode 100644 index 22b09df7..00000000 --- a/Examples/Deployment/scripts/SAM/HelloWorld-template.yml +++ /dev/null @@ -1,14 +0,0 @@ -AWSTemplateFormatVersion : '2010-09-09' -Transform: AWS::Serverless-2016-10-31 -Description: A sample SAM template for deploying Lambda functions. - -Resources: -# HelloWorld Function - helloWorldFunction: - Type: AWS::Serverless::Function - Properties: - Handler: Provided - Runtime: provided - CodeUri: ../../.build/lambda/HelloWorld/lambda.zip -# Instructs new versions to be published to an alias named "live". - AutoPublishAlias: live diff --git a/Examples/Deployment/scripts/build-and-package.sh b/Examples/Deployment/scripts/build-and-package.sh deleted file mode 100755 index f1e0a922..00000000 --- a/Examples/Deployment/scripts/build-and-package.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## 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 -## -##===----------------------------------------------------------------------===## - -set -eu - -executable=$1 -workspace="$(pwd)/../.." - -echo "-------------------------------------------------------------------------" -echo "preparing docker build image" -echo "-------------------------------------------------------------------------" -docker build . -t builder -echo "done" - -echo "-------------------------------------------------------------------------" -echo "building \"$executable\" lambda" -echo "-------------------------------------------------------------------------" -docker run --rm -v "$workspace":/workspace -w /workspace/Examples/Deployment builder \ - bash -cl "swift build --product $executable -c release" -echo "done" - -echo "-------------------------------------------------------------------------" -echo "packaging \"$executable\" lambda" -echo "-------------------------------------------------------------------------" -docker run --rm -v "$workspace":/workspace -w /workspace/Examples/Deployment builder \ - bash -cl "./scripts/package.sh $executable" -echo "done" diff --git a/Examples/Deployment/scripts/config.sh b/Examples/Deployment/scripts/config.sh deleted file mode 100755 index d4ab9f6f..00000000 --- a/Examples/Deployment/scripts/config.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## 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 -## -##===----------------------------------------------------------------------===## - -DIR="$(cd "$(dirname "$0")" && pwd)" -executables=( $(swift package dump-package | sed -e 's|: null|: ""|g' | jq '.products[] | (select(.type.executable)) | .name' | sed -e 's|"||g') ) - -if [[ ${#executables[@]} = 0 ]]; then - echo "no executables found" - exit 1 -elif [[ ${#executables[@]} = 1 ]]; then - executable=${executables[0]} -elif [[ ${#executables[@]} > 1 ]]; then - echo "multiple executables found:" - for executable in ${executables[@]}; do - echo " * $executable" - done - echo "" - read -p "select which executables to deploy: " executable -fi - -echo "-------------------------------------------------------------------------" -echo "configuration" -echo "-------------------------------------------------------------------------" -echo "current dir: $DIR" -echo "executable: $executable" -echo "-------------------------------------------------------------------------" diff --git a/Examples/Deployment/scripts/deploy.sh b/Examples/Deployment/scripts/deploy.sh deleted file mode 100755 index 3720b4d0..00000000 --- a/Examples/Deployment/scripts/deploy.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## 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 -## -##===----------------------------------------------------------------------===## - -set -eu - -DIR="$(cd "$(dirname "$0")" && pwd)" -source $DIR/config.sh - -workspace="$DIR/../.." - -echo -e "\ndeploying $executable" - -$DIR/build-and-package.sh "$executable" - -echo "-------------------------------------------------------------------------" -echo "uploading \"$executable\" lambda to AWS S3" -echo "-------------------------------------------------------------------------" - -read -p "S3 bucket name to upload zip file (must exist in AWS S3): " s3_bucket -s3_bucket=${s3_bucket:-swift-lambda-test} # default for easy testing - -aws s3 cp ".build/lambda/$executable/lambda.zip" "s3://$s3_bucket/" - -echo "-------------------------------------------------------------------------" -echo "updating AWS Lambda to use \"$executable\"" -echo "-------------------------------------------------------------------------" - -read -p "Lambda Function name (must exist in AWS Lambda): " lambda_name -lambda_name=${lambda_name:-SwiftSample} # default for easy testing - -aws lambda update-function-code --function "$lambda_name" --s3-bucket "$s3_bucket" --s3-key lambda.zip diff --git a/Examples/Deployment/scripts/package.sh b/Examples/Deployment/scripts/package.sh deleted file mode 100755 index 17d5853b..00000000 --- a/Examples/Deployment/scripts/package.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## 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 -## -##===----------------------------------------------------------------------===## - -set -eu - -executable=$1 - -target=".build/lambda/$executable" -rm -rf "$target" -mkdir -p "$target" -cp ".build/release/$executable" "$target/" -# add the target deps based on ldd -ldd ".build/release/$executable" | grep swift | awk '{print $3}' | xargs cp -Lv -t "$target" -cd "$target" -ln -s "$executable" "bootstrap" -zip --symlinks lambda.zip * diff --git a/Examples/Deployment/scripts/sam-deploy.sh b/Examples/Deployment/scripts/sam-deploy.sh deleted file mode 100755 index d87d966d..00000000 --- a/Examples/Deployment/scripts/sam-deploy.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## 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 -## -##===----------------------------------------------------------------------===## - -DIR="$(cd "$(dirname "$0")" && pwd)" -source $DIR/config.sh - -echo -e "\ndeploying $executable" - -$DIR/build-and-package.sh "$executable" - -echo "-------------------------------------------------------------------------" -echo "deploying using SAM" -echo "-------------------------------------------------------------------------" - -sam deploy --template "./scripts/SAM/$executable-template.yml" $@ diff --git a/Examples/Deployment/scripts/serverless-remove.sh b/Examples/Deployment/scripts/serverless-remove.sh deleted file mode 100755 index 262c07cb..00000000 --- a/Examples/Deployment/scripts/serverless-remove.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## 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 -## -##===----------------------------------------------------------------------===## - -set -eu - -DIR="$(cd "$(dirname "$0")" && pwd)" -source $DIR/config.sh - -echo -e "\nremoving $executable" - -echo "-------------------------------------------------------------------------" -echo "removing using Serverless" -echo "-------------------------------------------------------------------------" - -serverless remove --config "./scripts/serverless/$executable-template.yml" --stage dev -v diff --git a/Examples/Deployment/scripts/serverless/Benchmark-template.yml b/Examples/Deployment/scripts/serverless/Benchmark-template.yml deleted file mode 100644 index 1b2b1940..00000000 --- a/Examples/Deployment/scripts/serverless/Benchmark-template.yml +++ /dev/null @@ -1,20 +0,0 @@ -service: benchmark-swift-aws - -package: - artifact: .build/lambda/Benchmark/lambda.zip - -provider: - name: aws - runtime: provided - iamRoleStatements: - - Effect: Allow - Action: - - logs:CreateLogGroup - - logs:CreateLogStream - - logs:PutLogEvents - Resource: "*" - -functions: - benchmarkFunction: - handler: Benchmark - memorySize: 128 \ No newline at end of file diff --git a/Examples/Deployment/scripts/serverless/HelloWorld-template.yml b/Examples/Deployment/scripts/serverless/HelloWorld-template.yml deleted file mode 100644 index 8d12bb74..00000000 --- a/Examples/Deployment/scripts/serverless/HelloWorld-template.yml +++ /dev/null @@ -1,20 +0,0 @@ -service: helloworld-swift-aws - -package: - artifact: .build/lambda/HelloWorld/lambda.zip - -provider: - name: aws - runtime: provided - iamRoleStatements: - - Effect: Allow - Action: - - logs:CreateLogGroup - - logs:CreateLogStream - - logs:PutLogEvents - Resource: "*" - -functions: - hello: - handler: HelloWorld - memorySize: 128 \ No newline at end of file diff --git a/Examples/Echo/Package.swift b/Examples/Echo/Package.swift deleted file mode 100644 index a6d7ffef..00000000 --- a/Examples/Echo/Package.swift +++ /dev/null @@ -1,33 +0,0 @@ -// swift-tools-version:5.7 - -import class Foundation.ProcessInfo // needed for CI to test the local version of the library -import PackageDescription - -let package = Package( - name: "swift-aws-lambda-runtime-example", - platforms: [ - .macOS(.v12), - ], - products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), - ], - dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), - ], - targets: [ - .executableTarget( - name: "MyLambda", - dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - ], - path: "." - ), - ] -) - -// for CI to test the local version of the library -if ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"] != nil { - package.dependencies = [ - .package(name: "swift-aws-lambda-runtime", path: "../.."), - ] -} diff --git a/Examples/ErrorHandling/Lambda.swift b/Examples/ErrorHandling/Lambda.swift deleted file mode 100644 index d8e560aa..00000000 --- a/Examples/ErrorHandling/Lambda.swift +++ /dev/null @@ -1,104 +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 -// -//===----------------------------------------------------------------------===// - -import AWSLambdaRuntime - -// MARK: - Run Lambda - -@main -struct MyLambda: SimpleLambdaHandler { - func handle(_ request: Request, context: LambdaContext) async throws -> Response { - // switch over the error type "requested" by the request, and trigger such error accordingly - switch request.error { - // no error here! - case .none: - return Response(awsRequestID: context.requestID, requestID: request.requestID, status: .ok) - // trigger a "managed" error - domain specific business logic failure - case .managed: - return Response(awsRequestID: context.requestID, requestID: request.requestID, status: .error) - // trigger an "unmanaged" error - an unexpected Swift Error triggered while processing the request - case .unmanaged(let error): - throw UnmanagedError(description: error) - // trigger a "fatal" error - a panic type error which will crash the process - case .fatal: - fatalError("crash!") - } - } -} - -// MARK: - Request and Response - -struct Request: Codable { - let requestID: String - let error: Error - - public init(requestID: String, error: Error? = nil) { - self.requestID = requestID - self.error = error ?? .none - } - - public enum Error: Codable, RawRepresentable { - case none - case managed - case unmanaged(String) - case fatal - - public init?(rawValue: String) { - switch rawValue { - case "none": - self = .none - case "managed": - self = .managed - case "fatal": - self = .fatal - default: - self = .unmanaged(rawValue) - } - } - - public var rawValue: String { - switch self { - case .none: - return "none" - case .managed: - return "managed" - case .fatal: - return "fatal" - case .unmanaged(let error): - return error - } - } - } -} - -struct Response: Codable { - let awsRequestID: String - let requestID: String - let status: Status - - public init(awsRequestID: String, requestID: String, status: Status) { - self.awsRequestID = awsRequestID - self.requestID = requestID - self.status = status - } - - public enum Status: Int, Codable { - case ok - case error - } -} - -struct UnmanagedError: Error { - let description: String -} diff --git a/Examples/ErrorHandling/Package.swift b/Examples/ErrorHandling/Package.swift deleted file mode 100644 index a6d7ffef..00000000 --- a/Examples/ErrorHandling/Package.swift +++ /dev/null @@ -1,33 +0,0 @@ -// swift-tools-version:5.7 - -import class Foundation.ProcessInfo // needed for CI to test the local version of the library -import PackageDescription - -let package = Package( - name: "swift-aws-lambda-runtime-example", - platforms: [ - .macOS(.v12), - ], - products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), - ], - dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), - ], - targets: [ - .executableTarget( - name: "MyLambda", - dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - ], - path: "." - ), - ] -) - -// for CI to test the local version of the library -if ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"] != nil { - package.dependencies = [ - .package(name: "swift-aws-lambda-runtime", path: "../.."), - ] -} diff --git a/Examples/Foundation/Lambda.swift b/Examples/Foundation/Lambda.swift deleted file mode 100644 index 660574a1..00000000 --- a/Examples/Foundation/Lambda.swift +++ /dev/null @@ -1,266 +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 -// -//===----------------------------------------------------------------------===// - -import AWSLambdaRuntime -import Dispatch -import Foundation -#if canImport(FoundationNetworking) && canImport(FoundationXML) -import FoundationNetworking -import FoundationXML -#endif -import Logging - -// MARK: - Run Lambda - -@main -struct MyLambda: LambdaHandler { - let calculator: ExchangeRatesCalculator - - init(context: LambdaInitializationContext) async throws { - // the ExchangeRatesCalculator() can be reused over and over - self.calculator = ExchangeRatesCalculator() - } - - func handle(_ event: Request, context: LambdaContext) async throws -> [Exchange] { - try await withCheckedThrowingContinuation { continuation in - self.calculator.run(logger: context.logger) { result in - switch result { - case .success(let exchanges): - continuation.resume(returning: exchanges) - case .failure(let error): - continuation.resume(throwing: error) - } - } - } - } -} - -// MARK: - Business Logic - -// This is a contrived example performing currency exchange rate lookup and conversion using URLSession and XML parsing -struct ExchangeRatesCalculator { - static let currencies = ["EUR", "USD", "JPY"] - static let currenciesEmojies = [ - "EUR": "💶", - "JPY": "💴", - "USD": "💵", - ] - - let locale: Locale - let calendar: Calendar - - init() { - // This is data from HMRC, the UK tax authority. Therefore we want to use their locale when interpreting data from the server. - self.locale = Locale(identifier: "en_GB") - // Use the UK calendar, not the system one. - var calendar = self.locale.calendar - calendar.timeZone = TimeZone(identifier: "UTC")! - self.calendar = calendar - } - - func run(logger: Logger, callback: @escaping (Result<[Exchange], Swift.Error>) -> Void) { - let startDate = Date() - let months = (1 ... 12).map { - self.calendar.date(byAdding: DateComponents(month: -$0), to: startDate)! - } - - self.download(logger: logger, - months: months, - monthIndex: months.startIndex, - currencies: Self.currencies, - state: [:]) { result in - - switch result { - case .failure(let error): - return callback(.failure(error)) - case .success(let downloadedDataByMonth): - logger.debug("Downloads complete") - - var result = [Exchange]() - var previousData: [String: Decimal?] = [:] - for (_, exchangeRateData) in downloadedDataByMonth.filter({ $1.period != nil }).sorted(by: { $0.key < $1.key }) { - for (currencyCode, rate) in exchangeRateData.ratesByCurrencyCode.sorted(by: { $0.key < $1.key }) { - if let rate = rate, let currencyEmoji = Self.currenciesEmojies[currencyCode] { - let change: Exchange.Change - switch previousData[currencyCode] { - case .some(.some(let previousRate)) where rate > previousRate: - change = .up - case .some(.some(let previousRate)) where rate < previousRate: - change = .down - case .some(.some(let previousRate)) where rate == previousRate: - change = .none - default: - change = .unknown - } - result.append(Exchange(date: exchangeRateData.period!.start, - from: .init(symbol: "GBP", emoji: "💷"), - to: .init(symbol: currencyCode, emoji: currencyEmoji), - rate: rate, - change: change)) - } - } - previousData = exchangeRateData.ratesByCurrencyCode - } - callback(.success(result)) - } - } - } - - private func download(logger: Logger, - months: [Date], - monthIndex: Array.Index, - currencies: [String], - state: [Date: ExchangeRates], - callback: @escaping ((Result<[Date: ExchangeRates], Swift.Error>) -> Void)) - { - if monthIndex == months.count { - return callback(.success(state)) - } - - var newState = state - - let month = months[monthIndex] - let url = self.exchangeRatesURL(forMonthContaining: month) - logger.debug("requesting exchange rate from \(url)") - let dataTask = URLSession.shared.dataTask(with: url) { data, _, error in - do { - guard let data = data else { - throw error! - } - let exchangeRates = try self.parse(data: data, currencyCodes: Set(currencies)) - newState[month] = exchangeRates - logger.debug("Finished downloading month: \(month)") - if let period = exchangeRates.period { - logger.debug("Got data covering period: \(period)") - } - } catch { - return callback(.failure(error)) - } - self.download(logger: logger, - months: months, - monthIndex: monthIndex.advanced(by: 1), - currencies: currencies, - state: newState, - callback: callback) - } - dataTask.resume() - } - - private func parse(data: Data, currencyCodes: Set) throws -> ExchangeRates { - let document = try XMLDocument(data: data) - let dateFormatter = DateFormatter() - dateFormatter.timeZone = TimeZone(identifier: "Etc/UTC")! - dateFormatter.dateFormat = "dd/MMM/yy" - let interval: DateInterval? - if let period = try document.nodes(forXPath: "/exchangeRateMonthList/@Period").first?.stringValue, - period.count == 26 { - // "01/Sep/2018 to 30/Sep/2018" - let startString = period[period.startIndex ..< period.index(period.startIndex, offsetBy: 11)] - let to = period[startString.endIndex ..< period.index(startString.endIndex, offsetBy: 4)] - let endString = period[to.endIndex ..< period.index(to.endIndex, offsetBy: 11)] - if let startDate = dateFormatter.date(from: String(startString)), - let startDay = calendar.dateInterval(of: .day, for: startDate), - to == " to ", - let endDate = dateFormatter.date(from: String(endString)), - let endDay = calendar.dateInterval(of: .day, for: endDate) { - interval = DateInterval(start: startDay.start, end: endDay.end) - } else { - interval = nil - } - } else { - interval = nil - } - - let ratesByCurrencyCode: [String: Decimal?] = Dictionary(uniqueKeysWithValues: try currencyCodes.map { - let xpathCurrency = $0.replacingOccurrences(of: "'", with: "'") - if let rateString = try document.nodes(forXPath: "/exchangeRateMonthList/exchangeRate/currencyCode[text()='\(xpathCurrency)']/../rateNew/text()").first?.stringValue, - // We must parse the decimal data using the UK locale, not the system one. - let rate = Decimal(string: rateString, locale: self.locale) { - return ($0, rate) - } else { - return ($0, nil) - } - }) - - return (period: interval, ratesByCurrencyCode: ratesByCurrencyCode) - } - - private func makeUTCDateFormatter(dateFormat: String) -> DateFormatter { - let utcTimeZone = TimeZone(identifier: "UTC")! - let result = DateFormatter() - result.locale = Locale(identifier: "en_US_POSIX") - result.timeZone = utcTimeZone - result.dateFormat = dateFormat - return result - } - - private func exchangeRatesURL(forMonthContaining date: Date) -> URL { - let exchangeRatesBaseURL = URL(string: "https://www.hmrc.gov.uk/softwaredevelopers/rates")! - let dateFormatter = self.makeUTCDateFormatter(dateFormat: "MMyy") - return exchangeRatesBaseURL.appendingPathComponent("exrates-monthly-\(dateFormatter.string(from: date)).xml") - } - - private typealias ExchangeRates = (period: DateInterval?, ratesByCurrencyCode: [String: Decimal?]) - - private struct Error: Swift.Error, CustomStringConvertible { - let description: String - } -} - -// MARK: - Request and Response - -struct Request: Decodable {} - -struct Exchange: Encodable { - @DateCoding - var date: Date - let from: Currency - let to: Currency - let rate: Decimal - let change: Change - - struct Currency: Encodable { - let symbol: String - let emoji: String - } - - enum Change: String, Encodable { - case up - case down - case none - case unknown - } - - @propertyWrapper - public struct DateCoding: Encodable { - public let wrappedValue: Date - - public init(wrappedValue: Date) { - self.wrappedValue = wrappedValue - } - - func encode(to encoder: Encoder) throws { - let string = Self.dateFormatter.string(from: self.wrappedValue) - var container = encoder.singleValueContainer() - try container.encode(string) - } - - private static var dateFormatter: ISO8601DateFormatter { - let dateFormatter = ISO8601DateFormatter() - dateFormatter.timeZone = TimeZone(identifier: "UTC")! - dateFormatter.formatOptions = [.withYear, .withMonth, .withDashSeparatorInDate] - return dateFormatter - } - } -} diff --git a/Examples/Foundation/Package.swift b/Examples/Foundation/Package.swift deleted file mode 100644 index a6d7ffef..00000000 --- a/Examples/Foundation/Package.swift +++ /dev/null @@ -1,33 +0,0 @@ -// swift-tools-version:5.7 - -import class Foundation.ProcessInfo // needed for CI to test the local version of the library -import PackageDescription - -let package = Package( - name: "swift-aws-lambda-runtime-example", - platforms: [ - .macOS(.v12), - ], - products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), - ], - dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), - ], - targets: [ - .executableTarget( - name: "MyLambda", - dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - ], - path: "." - ), - ] -) - -// for CI to test the local version of the library -if ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"] != nil { - package.dependencies = [ - .package(name: "swift-aws-lambda-runtime", path: "../.."), - ] -} diff --git a/Examples/HelloJSON/.gitignore b/Examples/HelloJSON/.gitignore new file mode 100644 index 00000000..e41d0be5 --- /dev/null +++ b/Examples/HelloJSON/.gitignore @@ -0,0 +1,4 @@ +response.json +samconfig.toml +template.yaml +Makefile diff --git a/Examples/HelloJSON/Package.swift b/Examples/HelloJSON/Package.swift new file mode 100644 index 00000000..e81d12ac --- /dev/null +++ b/Examples/HelloJSON/Package.swift @@ -0,0 +1,56 @@ +// swift-tools-version:6.2 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "swift-aws-lambda-runtime-example", + 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", + from: "2.0.0" + ) + ], + targets: [ + .executableTarget( + name: "HelloJSON", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ] + ) + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} diff --git a/Examples/HelloJSON/README.md b/Examples/HelloJSON/README.md new file mode 100644 index 00000000..1f372191 --- /dev/null +++ b/Examples/HelloJSON/README.md @@ -0,0 +1,90 @@ +# Hello JSON + +This is a simple example of an AWS Lambda function that takes a JSON structure as an input parameter and returns a JSON structure as a response. + +The runtime takes care of decoding the input and encoding the output. + +## Code + +The code defines `HelloRequest` and `HelloResponse` data structures to represent the input and output payloads. These structures are typically shared with a client project, such as an iOS application. + +The code creates a `LambdaRuntime` struct. In it's simplest form, the initializer takes a function as an argument. The function is the handler that will be invoked when an event triggers the Lambda function. + +The handler is `(event: HelloRequest, context: LambdaContext)`. The function takes two arguments: +- the event argument is a `HelloRequest`. It is the parameter passed when invoking the function. +- the context argument is a `Lambda Context`. It is a description of the runtime context. + +The function return value will be encoded to a `HelloResponse` as your Lambda function response. + +## Build & Package + +To build & archive the package, type the following commands. + +```bash +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/HelloJSON/HelloJSON.zip` + +## Deploy + +Here is how to deploy using the `aws` command line. + +```bash +# Replace with your AWS Account ID +AWS_ACCOUNT_ID=012345678901 + +aws lambda create-function \ +--function-name HelloJSON \ +--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/HelloJSON/HelloJSON.zip \ +--runtime provided.al2 \ +--handler provided \ +--architectures arm64 \ +--role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution +``` + +The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. + +Be sure to define the `AWS_ACCOUNT_ID` environment variable with your actual AWS account ID (for example: 012345678901). + +## Invoke your Lambda function + +To invoke the Lambda function, use this `aws` command line. + +```bash +aws lambda invoke \ +--function-name HelloJSON \ +--payload $(echo '{ "name" : "Seb", "age" : 50 }' | base64) \ +out.txt && cat out.txt && rm out.txt +``` + +Note that the payload is expected to be a valid JSON string. + +This should output the following result. + +``` +{ + "StatusCode": 200, + "ExecutedVersion": "$LATEST" +} +{"greetings":"Hello Seb. You look younger than your age."} +``` + +## Undeploy + +When done testing, you can delete the Lambda function with this command. + +```bash +aws lambda delete-function --function-name HelloJSON +``` + +## ⚠️ Security and Reliability Notice + +These are example applications for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: + +- Enable access logging on API Gateway ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)) +- Ensure that AWS Lambda function is configured for function-level concurrent execution limit ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +- Check encryption settings for Lambda environment variables ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +- Ensure that AWS Lambda function is configured for a Dead Letter Queue (DLQ) ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +- Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html), [code example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres)) \ No newline at end of file diff --git a/Examples/HelloJSON/Sources/main.swift b/Examples/HelloJSON/Sources/main.swift new file mode 100644 index 00000000..7e48971b --- /dev/null +++ b/Examples/HelloJSON/Sources/main.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime + +// in this example we are receiving and responding with JSON structures + +// the data structure to represent the input parameter +struct HelloRequest: Decodable { + let name: String + let age: Int +} + +// the data structure to represent the output response +struct HelloResponse: Encodable { + let greetings: String +} + +// the Lambda runtime +let runtime = LambdaRuntime { + (event: HelloRequest, context: LambdaContext) in + + HelloResponse( + greetings: "Hello \(event.name). You look \(event.age > 30 ? "younger" : "older") than your age." + ) +} + +// start the loop +try await runtime.run() diff --git a/Examples/HelloWorld/.gitignore b/Examples/HelloWorld/.gitignore new file mode 100644 index 00000000..e41d0be5 --- /dev/null +++ b/Examples/HelloWorld/.gitignore @@ -0,0 +1,4 @@ +response.json +samconfig.toml +template.yaml +Makefile diff --git a/Examples/HelloWorld/Package.swift b/Examples/HelloWorld/Package.swift new file mode 100644 index 00000000..8665a07f --- /dev/null +++ b/Examples/HelloWorld/Package.swift @@ -0,0 +1,59 @@ +// swift-tools-version:6.1 +// This example has to be in Swift 6.1 because it is used in the test archive plugin CI job +// That job runs on GitHub's ubuntu-latest environment that only supports Swift 6.1 +// https://github.com/actions/runner-images?tab=readme-ov-file +// https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md +// We can update to Swift 6.2 when GitHUb hosts will have Swift 6.2 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "MyLambda", targets: ["MyLambda"]) + ], + 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", from: "2.0.0") + ], + targets: [ + .executableTarget( + name: "MyLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: "Sources" + ) + ] +) + +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/HelloWorld/README.md b/Examples/HelloWorld/README.md new file mode 100644 index 00000000..d4f7bbee --- /dev/null +++ b/Examples/HelloWorld/README.md @@ -0,0 +1,113 @@ +# Hello World + +This is a simple example of an AWS Lambda function that takes a `String` as input parameter and returns a `String` as response. + +## Code + +The code creates a `LambdaRuntime` struct. In it's simplest form, the initializer takes a function as argument. The function is the handler that will be invoked when an event triggers the Lambda function. + +The handler is `(event: String, context: LambdaContext)`. The function takes two arguments: +- the event argument is a `String`. It is the parameter passed when invoking the function. +- the context argument is a `Lambda Context`. It is a description of the runtime context. + +The function return value will be encoded 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. + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip` + +## Deploy + +Here is how to deploy using the `aws` command line. + +```bash +aws lambda create-function \ +--function-name MyLambda \ +--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.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 replace with your actual AWS account ID (for example: 012345678901). + +## Invoke your Lambda function + +To invoke the Lambda function, use this `aws` command line. + +```bash +aws lambda invoke \ +--function-name MyLambda \ +--payload $(echo \"Seb\" | base64) \ +out.txt && cat out.txt && rm out.txt +``` + +Note that the payload is expected to be a valid JSON string, hence the surroundings quotes (`"`). + +This should output the following result. + +``` +{ + "StatusCode": 200, + "ExecutedVersion": "$LATEST" +} +"Hello Seb" +``` + +## Undeploy + +When done testing, you can delete the Lambda function with this command. + +```bash +aws lambda delete-function --function-name MyLambda +``` + +## ⚠️ Security and Reliability Notice + +These are example applications for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: + +- Enable access logging on API Gateway ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)) +- Ensure that AWS Lambda function is configured for function-level concurrent execution limit ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +- Check encryption settings for Lambda environment variables ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +- Ensure that AWS Lambda function is configured for a Dead Letter Queue (DLQ) ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +- Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html), [code example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres)) \ No newline at end of file diff --git a/Examples/Echo/Lambda.swift b/Examples/HelloWorld/Sources/main.swift similarity index 65% rename from Examples/Echo/Lambda.swift rename to Examples/HelloWorld/Sources/main.swift index 00c0a5e5..5aab1a79 100644 --- a/Examples/Echo/Lambda.swift +++ b/Examples/HelloWorld/Sources/main.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2021 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 @@ -16,10 +16,9 @@ import AWSLambdaRuntime // in this example we are receiving and responding with strings -@main -struct MyLambda: SimpleLambdaHandler { - func handle(_ input: String, context: LambdaContext) async throws -> String { - // as an example, respond with the input's reversed - String(input.reversed()) - } +let runtime = LambdaRuntime { + (event: String, context: LambdaContext) in + "Hello \(event)" } + +try await runtime.run() diff --git a/Examples/HelloWorldNoTraits/.gitignore b/Examples/HelloWorldNoTraits/.gitignore new file mode 100644 index 00000000..e41d0be5 --- /dev/null +++ b/Examples/HelloWorldNoTraits/.gitignore @@ -0,0 +1,4 @@ +response.json +samconfig.toml +template.yaml +Makefile diff --git a/Examples/HelloWorldNoTraits/Package.swift b/Examples/HelloWorldNoTraits/Package.swift new file mode 100644 index 00000000..2c613221 --- /dev/null +++ b/Examples/HelloWorldNoTraits/Package.swift @@ -0,0 +1,54 @@ +// swift-tools-version:6.2 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "MyLambda", targets: ["MyLambda"]) + ], + 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", from: "2.0.0", traits: []) + ], + targets: [ + .executableTarget( + name: "MyLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: "Sources" + ) + ] +) + +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, traits: []) + ] +} diff --git a/Examples/HelloWorldNoTraits/README.md b/Examples/HelloWorldNoTraits/README.md new file mode 100644 index 00000000..653fc15d --- /dev/null +++ b/Examples/HelloWorldNoTraits/README.md @@ -0,0 +1,117 @@ +# Hello World, with no traits + +This is an example of a low-level AWS Lambda function that takes a `ByteBuffer` as input parameter and writes its response on the provided `LambdaResponseStreamWriter`. + +This function disables all the default traits: the support for JSON Encoder and Decoder from Foundation, the support for Swift Service Lifecycle, and for the local tetsing server. + +The main reasons of the existence of this example are + +1. to show how to write a low-level Lambda function that doesn't rely on JSON encodinga and decoding. +2. to show you how to disable traits when using the Lambda Runtime Library. +3. to add an integration test to our continous integration pipeline to make sure the library compiles with no traits enabled. + +## Disabling all traits + +Traits are functions of the AWS Lambda Runtime that you can disable at compile time to reduce the size of your binary, and therefore reduce the cold start time of your Lambda function. + +The library supports three traits: + +- "FoundationJSONSupport": adds the required API to encode and decode payloads with Foundation's `JSONEncoder` and `JSONDecoder`. + +- "ServiceLifecycleSupport": adds support for the Swift Service Lifecycle library. + +- "LocalServerSupport": adds support for testing your function locally with a built-in HTTP server. + +This example disables all the traits. To disable one or several traits, modify `Package.swift`: + +```swift + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "2.0.0-beta", traits: []) + ], +``` + +## Code + +The code creates a `LambdaRuntime` struct. In its simplest form, the initializer takes a function as argument. The function is the handler that will be invoked when an event triggers the Lambda function. + +The handler signature is `(event: ByteBuffer, response: LambdaResponseStreamWriter, context: LambdaContext)`. + +The function takes three arguments: +- the event argument is a `ByteBuffer`. It's the parameter passed when invoking the function. You are responsible of decoding this parameter, if necessary. +- the response writer provides you with functions to write the response stream back. +- the context argument is a `Lambda Context`. It is a description of the runtime context. + +The function return value will be encoded as your Lambda function response. + +## Test locally + +You cannot test this function locally, because the "LocalServer" trait is disabled. + +## Build & Package + +To build & archive the package, type the following commands. + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip` + +## Deploy + +Here is how to deploy using the `aws` command line. + +```bash +aws lambda create-function \ +--function-name MyLambda \ +--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.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 replace with your actual AWS account ID (for example: 012345678901). + +## Invoke your Lambda function + +To invoke the Lambda function, use this `aws` command line. + +```bash +aws lambda invoke \ +--function-name MyLambda \ +--payload $(echo "Seb" | base64) \ +out.txt && cat out.txt && rm out.txt +``` + +This should output the following result. + +``` +{ + "StatusCode": 200, + "ExecutedVersion": "$LATEST" +} +"Hello World!" +``` + +## Undeploy + +When done testing, you can delete the Lambda function with this command. + +```bash +aws lambda delete-function --function-name MyLambda +``` + +## ⚠️ Security and Reliability Notice + +These are example applications for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: + +- Enable access logging on API Gateway ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)) +- Ensure that AWS Lambda function is configured for function-level concurrent execution limit ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +- Check encryption settings for Lambda environment variables ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +- Ensure that AWS Lambda function is configured for a Dead Letter Queue (DLQ) ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +- Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html), [code example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres)) \ No newline at end of file diff --git a/Examples/Deployment/Sources/HelloWorld/HelloWorldHandler.swift b/Examples/HelloWorldNoTraits/Sources/main.swift similarity index 63% rename from Examples/Deployment/Sources/HelloWorld/HelloWorldHandler.swift rename to Examples/HelloWorldNoTraits/Sources/main.swift index 9f4a16f3..f0f167ab 100644 --- a/Examples/Deployment/Sources/HelloWorld/HelloWorldHandler.swift +++ b/Examples/HelloWorldNoTraits/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,11 +13,10 @@ //===----------------------------------------------------------------------===// import AWSLambdaRuntime +import NIOCore -// introductory example, the obligatory "hello, world!" -@main -struct HelloWorldHandler: SimpleLambdaHandler { - func handle(_ event: String, context: LambdaContext) async throws -> String { - "hello, world" - } +let runtime = LambdaRuntime { event, response, context in + try await response.writeAndFinish(ByteBuffer(string: "Hello World!")) } + +try await runtime.run() diff --git a/Examples/HummingbirdLambda/.gitignore b/Examples/HummingbirdLambda/.gitignore new file mode 100644 index 00000000..fe6e212a --- /dev/null +++ b/Examples/HummingbirdLambda/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +samconfig.toml \ No newline at end of file diff --git a/Examples/HummingbirdLambda/Package.swift b/Examples/HummingbirdLambda/Package.swift new file mode 100644 index 00000000..cadca817 --- /dev/null +++ b/Examples/HummingbirdLambda/Package.swift @@ -0,0 +1,59 @@ +// swift-tools-version: 6.2 +// 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: "HBLambda", + platforms: [.macOS(.v15)], + dependencies: [ + .package( + url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + from: "2.0.0" + ), + .package( + url: "https://github.com/hummingbird-project/hummingbird-lambda.git", + branch: "main" + ), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.1.0"), + ], + targets: [ + .executableTarget( + name: "HBLambda", + dependencies: [ + .product(name: "HummingbirdLambda", package: "hummingbird-lambda"), + .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/HummingbirdLambda/README.md b/Examples/HummingbirdLambda/README.md new file mode 100644 index 00000000..fe706662 --- /dev/null +++ b/Examples/HummingbirdLambda/README.md @@ -0,0 +1,90 @@ +# Hummingbird Lambda + +This is a simple example of an AWS Lambda function using the [Hummingbird](https://github.com/hummingbird-project/hummingbird) web framework, invoked through an Amazon API Gateway. + +## Code + +The Lambda function uses Hummingbird's router to handle HTTP requests. It defines a simple GET endpoint at `/hello` that returns "Hello". + +The code creates a `Router` with `AppRequestContext` (which is a type alias for `BasicLambdaRequestContext`). The router defines HTTP routes using Hummingbird's familiar syntax. + +The `APIGatewayV2LambdaFunction` wraps the Hummingbird router to make it compatible with AWS Lambda and API Gateway V2 events. + +`APIGatewayV2Request` is defined in the [Swift AWS Lambda Events](https://github.com/swift-server/swift-aws-lambda-events) library, and the Hummingbird Lambda integration is provided by the [Hummingbird Lambda](https://github.com/hummingbird-project/hummingbird-lambda) package. + +## Build & Package + +To build the package, type the following commands. + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/HBLambda/HBLambda.zip` + +## Deploy + +The deployment must include the Lambda function and the API Gateway. We use the [Serverless Application Model (SAM)](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) to deploy the infrastructure. + +**Prerequisites** : Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) + +The example directory contains a file named `template.yaml` that describes the deployment. + +To actually deploy your Lambda function and create the infrastructure, type the following `sam` command. + +```bash +sam deploy \ +--resolve-s3 \ +--template-file template.yaml \ +--stack-name HummingbirdLambda \ +--capabilities CAPABILITY_IAM +``` + +At the end of the deployment, the script lists the API Gateway endpoint. +The output is similar to this one. + +``` +----------------------------------------------------------------------------------------------------------------------------- +Outputs +----------------------------------------------------------------------------------------------------------------------------- +Key APIGatewayEndpoint +Description API Gateway endpoint URL" +Value https://a5q74es3k2.execute-api.us-east-1.amazonaws.com +----------------------------------------------------------------------------------------------------------------------------- +``` + +## Invoke your Lambda function + +To invoke the Lambda function, use this `curl` command line to call the `/hello` endpoint. + +```bash +curl https://a5q74es3k2.execute-api.us-east-1.amazonaws.com/hello +``` + +Be sure to replace the URL with the API Gateway endpoint returned in the previous step. + +This should print: + +```bash +Hello +``` + +## Undeploy + +When done testing, you can delete the infrastructure with this command. + +```bash +sam delete +``` + +## ⚠️ Security and Reliability Notice + +These are example applications for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: + +- Enable access logging on API Gateway ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)) +- Ensure that AWS Lambda function is configured for function-level concurrent execution limit ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +- Check encryption settings for Lambda environment variables ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +- Ensure that AWS Lambda function is configured for a Dead Letter Queue (DLQ) ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +- Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html), [code example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres)) \ No newline at end of file diff --git a/Examples/HummingbirdLambda/Sources/main.swift b/Examples/HummingbirdLambda/Sources/main.swift new file mode 100644 index 00000000..15d19099 --- /dev/null +++ b/Examples/HummingbirdLambda/Sources/main.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// 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 AWSLambdaEvents +import HummingbirdLambda +import Logging + +typealias AppRequestContext = BasicLambdaRequestContext +let router = Router(context: AppRequestContext.self) + +router.get("hello") { _, _ in + "Hello" +} + +let lambda = APIGatewayV2LambdaFunction(router: router) +try await lambda.runService() diff --git a/Examples/HummingbirdLambda/template.yaml b/Examples/HummingbirdLambda/template.yaml new file mode 100644 index 00000000..207548f8 --- /dev/null +++ b/Examples/HummingbirdLambda/template.yaml @@ -0,0 +1,47 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for Hummingbird Lambda Example + +# This is an example SAM template for the purpose of this project. +# When deploying such infrastructure in production environment, +# we strongly encourage you to follow these best practices for improved security and resiliency +# - Enable access loggin on API Gateway +# See: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html) +# - Ensure that AWS Lambda function is configured for function-level concurrent execution limit +# See: https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html +# https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html +# - Check encryption settings for Lambda environment variable +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html +# - Ensure that AWS Lambda function is configured for a Dead Letter Queue(DLQ) +# See: https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq +# - Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html +# Code Example: https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres + +Resources: + # Lambda function + HBLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/HBLambda/HBLambda.zip + Timeout: 60 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 128 + Architectures: + - arm64 + Environment: + Variables: + # by default, AWS Lambda runtime produces no log + # use `LOG_LEVEL: debug` for for lifecycle and event handling information + # use `LOG_LEVEL: trace` for detailed input event information + LOG_LEVEL: debug + Events: + HttpApiEvent: + Type: HttpApi + +Outputs: + # print API Gateway endpoint + APIGatewayEndpoint: + Description: API Gateway endpoint URI" + Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" diff --git a/Examples/JSON/Lambda.swift b/Examples/JSON/Lambda.swift deleted file mode 100644 index cad3b650..00000000 --- a/Examples/JSON/Lambda.swift +++ /dev/null @@ -1,34 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2021 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import AWSLambdaRuntime - -struct Request: Codable { - let body: String -} - -struct Response: Codable { - let body: String -} - -// in this example we are receiving and responding with codables. Request and Response above are examples of how to use -// codables to model your request and response objects - -@main -struct MyLambda: SimpleLambdaHandler { - func handle(_ event: Request, context: LambdaContext) async throws -> Response { - // as an example, respond with the input event's reversed body - Response(body: String(event.body.reversed())) - } -} diff --git a/Examples/JSON/Package.swift b/Examples/JSON/Package.swift deleted file mode 100644 index a6d7ffef..00000000 --- a/Examples/JSON/Package.swift +++ /dev/null @@ -1,33 +0,0 @@ -// swift-tools-version:5.7 - -import class Foundation.ProcessInfo // needed for CI to test the local version of the library -import PackageDescription - -let package = Package( - name: "swift-aws-lambda-runtime-example", - platforms: [ - .macOS(.v12), - ], - products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), - ], - dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), - ], - targets: [ - .executableTarget( - name: "MyLambda", - dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - ], - path: "." - ), - ] -) - -// for CI to test the local version of the library -if ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"] != nil { - package.dependencies = [ - .package(name: "swift-aws-lambda-runtime", path: "../.."), - ] -} diff --git a/Examples/LocalDebugging/Example.xcworkspace/contents.xcworkspacedata b/Examples/LocalDebugging/Example.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index e42d285c..00000000 --- a/Examples/LocalDebugging/Example.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - diff --git a/Examples/LocalDebugging/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Examples/LocalDebugging/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/Examples/LocalDebugging/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/Examples/LocalDebugging/Example.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Examples/LocalDebugging/Example.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5..00000000 --- a/Examples/LocalDebugging/Example.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/Examples/LocalDebugging/MyApp/MyApp.xcodeproj/project.pbxproj b/Examples/LocalDebugging/MyApp/MyApp.xcodeproj/project.pbxproj deleted file mode 100644 index 0ac2539a..00000000 --- a/Examples/LocalDebugging/MyApp/MyApp.xcodeproj/project.pbxproj +++ /dev/null @@ -1,366 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 55; - objects = { - -/* Begin PBXBuildFile section */ - 7CD1174B26FE468F007DD17A /* MyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CD1174A26FE468F007DD17A /* MyApp.swift */; }; - 7CD1174D26FE468F007DD17A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CD1174C26FE468F007DD17A /* ContentView.swift */; }; - 7CD1174F26FE4692007DD17A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7CD1174E26FE4692007DD17A /* Assets.xcassets */; }; - 7CD1175226FE4692007DD17A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7CD1175126FE4692007DD17A /* Preview Assets.xcassets */; }; - 7CD1175A26FE4F44007DD17A /* Shared in Frameworks */ = {isa = PBXBuildFile; productRef = 7CD1175926FE4F44007DD17A /* Shared */; }; -/* End PBXBuildFile section */ - -/* Begin PBXFileReference section */ - 7CD1174726FE468F007DD17A /* MyApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MyApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 7CD1174A26FE468F007DD17A /* MyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyApp.swift; sourceTree = ""; }; - 7CD1174C26FE468F007DD17A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - 7CD1174E26FE4692007DD17A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 7CD1175126FE4692007DD17A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 7CD1174426FE468F007DD17A /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 7CD1175A26FE4F44007DD17A /* Shared in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 7CD1173E26FE468F007DD17A = { - isa = PBXGroup; - children = ( - 7CD1174926FE468F007DD17A /* MyApp */, - 7CD1174826FE468F007DD17A /* Products */, - 7CD1175826FE4F44007DD17A /* Frameworks */, - ); - sourceTree = ""; - }; - 7CD1174826FE468F007DD17A /* Products */ = { - isa = PBXGroup; - children = ( - 7CD1174726FE468F007DD17A /* MyApp.app */, - ); - name = Products; - sourceTree = ""; - }; - 7CD1174926FE468F007DD17A /* MyApp */ = { - isa = PBXGroup; - children = ( - 7CD1174A26FE468F007DD17A /* MyApp.swift */, - 7CD1174C26FE468F007DD17A /* ContentView.swift */, - 7CD1174E26FE4692007DD17A /* Assets.xcassets */, - 7CD1175026FE4692007DD17A /* Preview Content */, - ); - path = MyApp; - sourceTree = ""; - }; - 7CD1175026FE4692007DD17A /* Preview Content */ = { - isa = PBXGroup; - children = ( - 7CD1175126FE4692007DD17A /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; - 7CD1175826FE4F44007DD17A /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 7CD1174626FE468F007DD17A /* MyApp */ = { - isa = PBXNativeTarget; - buildConfigurationList = 7CD1175526FE4692007DD17A /* Build configuration list for PBXNativeTarget "MyApp" */; - buildPhases = ( - 7CD1174326FE468F007DD17A /* Sources */, - 7CD1174426FE468F007DD17A /* Frameworks */, - 7CD1174526FE468F007DD17A /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = MyApp; - packageProductDependencies = ( - 7CD1175926FE4F44007DD17A /* Shared */, - ); - productName = MyApp; - productReference = 7CD1174726FE468F007DD17A /* MyApp.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 7CD1173F26FE468F007DD17A /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1300; - LastUpgradeCheck = 1300; - TargetAttributes = { - 7CD1174626FE468F007DD17A = { - CreatedOnToolsVersion = 13.0; - }; - }; - }; - buildConfigurationList = 7CD1174226FE468F007DD17A /* Build configuration list for PBXProject "MyApp" */; - compatibilityVersion = "Xcode 13.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 7CD1173E26FE468F007DD17A; - productRefGroup = 7CD1174826FE468F007DD17A /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 7CD1174626FE468F007DD17A /* MyApp */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 7CD1174526FE468F007DD17A /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 7CD1175226FE4692007DD17A /* Preview Assets.xcassets in Resources */, - 7CD1174F26FE4692007DD17A /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 7CD1174326FE468F007DD17A /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 7CD1174D26FE468F007DD17A /* ContentView.swift in Sources */, - 7CD1174B26FE468F007DD17A /* MyApp.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - 7CD1175326FE4692007DD17A /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 7CD1175426FE4692007DD17A /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 7CD1175626FE4692007DD17A /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"MyApp/Preview Content\""; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.apple.swift.MyApp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 7CD1175726FE4692007DD17A /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"MyApp/Preview Content\""; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.apple.swift.MyApp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 7CD1174226FE468F007DD17A /* Build configuration list for PBXProject "MyApp" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7CD1175326FE4692007DD17A /* Debug */, - 7CD1175426FE4692007DD17A /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 7CD1175526FE4692007DD17A /* Build configuration list for PBXNativeTarget "MyApp" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7CD1175626FE4692007DD17A /* Debug */, - 7CD1175726FE4692007DD17A /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - -/* Begin XCSwiftPackageProductDependency section */ - 7CD1175926FE4F44007DD17A /* Shared */ = { - isa = XCSwiftPackageProductDependency; - productName = Shared; - }; -/* End XCSwiftPackageProductDependency section */ - }; - rootObject = 7CD1173F26FE468F007DD17A /* Project object */; -} diff --git a/Examples/LocalDebugging/MyApp/MyApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/LocalDebugging/MyApp/MyApp/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 9221b9bb..00000000 --- a/Examples/LocalDebugging/MyApp/MyApp/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/LocalDebugging/MyApp/MyApp/Assets.xcassets/Contents.json b/Examples/LocalDebugging/MyApp/MyApp/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596..00000000 --- a/Examples/LocalDebugging/MyApp/MyApp/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/LocalDebugging/MyApp/MyApp/ContentView.swift b/Examples/LocalDebugging/MyApp/MyApp/ContentView.swift deleted file mode 100644 index 4c7d3158..00000000 --- a/Examples/LocalDebugging/MyApp/MyApp/ContentView.swift +++ /dev/null @@ -1,95 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2020-2021 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 Shared -import SwiftUI - -struct ContentView: View { - @State var name: String = "" - @State var password: String = "" - @State var response: String = "" - @State private var isLoading: Bool = false - - var body: some View { - VStack(alignment: .leading, spacing: 20) { - TextField("Username", text: $name) - SecureField("Password", text: $password) - let inputIncomplete = name.isEmpty || password.isEmpty - Button { - Task { - isLoading = true - do { - response = try await self.register() - } catch { - response = error.localizedDescription - } - isLoading = false - } - } label: { - Text("Register") - .padding() - .foregroundColor(.white) - .background(.black) - .border(.black, width: 2) - .opacity(isLoading ? 0 : 1) - .overlay { - if isLoading { - ProgressView() - } - } - } - .disabled(inputIncomplete || isLoading) - .opacity(inputIncomplete ? 0.5 : 1) - Text(response) - }.padding(100) - } - - func register() async throws -> String { - guard let url = URL(string: "http://127.0.0.1:7000/invoke") else { - fatalError("invalid url") - } - var request = URLRequest(url: url) - request.httpMethod = "POST" - - guard let jsonRequest = try? JSONEncoder().encode(Request(name: self.name, password: self.password)) else { - fatalError("encoding error") - } - request.httpBody = jsonRequest - - let (data, response) = try await URLSession.shared.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw CommunicationError(reason: "Invalid response, expected HTTPURLResponse.") - } - guard httpResponse.statusCode == 200 else { - throw CommunicationError(reason: "Invalid response code: \(httpResponse.statusCode)") - } - - let jsonResponse = try JSONDecoder().decode(Response.self, from: data) - return jsonResponse.message - } -} - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } -} - -struct CommunicationError: LocalizedError { - let reason: String - var errorDescription: String? { - self.reason - } -} diff --git a/Examples/LocalDebugging/MyApp/MyApp/Preview Content/Preview Assets.xcassets/Contents.json b/Examples/LocalDebugging/MyApp/MyApp/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596..00000000 --- a/Examples/LocalDebugging/MyApp/MyApp/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/LocalDebugging/MyLambda/.dockerignore b/Examples/LocalDebugging/MyLambda/.dockerignore deleted file mode 100644 index 24e5b0a1..00000000 --- a/Examples/LocalDebugging/MyLambda/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -.build diff --git a/Examples/LocalDebugging/MyLambda/Dockerfile b/Examples/LocalDebugging/MyLambda/Dockerfile deleted file mode 100644 index 32962859..00000000 --- a/Examples/LocalDebugging/MyLambda/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM swift:5.5-amazonlinux2 - -RUN yum -y install zip diff --git a/Examples/LocalDebugging/MyLambda/Lambda.swift b/Examples/LocalDebugging/MyLambda/Lambda.swift deleted file mode 100644 index 397ece30..00000000 --- a/Examples/LocalDebugging/MyLambda/Lambda.swift +++ /dev/null @@ -1,27 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2020-2021 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import AWSLambdaRuntime -import Shared - -// set LOCAL_LAMBDA_SERVER_ENABLED env variable to "true" to start -// a local server simulator which will allow local debugging - -@main -struct MyLambda: SimpleLambdaHandler { - func handle(_ request: Request, context: LambdaContext) async throws -> Response { - // TODO: something useful - Response(message: "Hello, \(request.name)!") - } -} diff --git a/Examples/LocalDebugging/MyLambda/Package.swift b/Examples/LocalDebugging/MyLambda/Package.swift deleted file mode 100644 index afc9fa21..00000000 --- a/Examples/LocalDebugging/MyLambda/Package.swift +++ /dev/null @@ -1,37 +0,0 @@ -// swift-tools-version:5.7 - -import class Foundation.ProcessInfo // needed for CI to test the local version of the library -import PackageDescription - -let package = Package( - name: "MyLambda", - platforms: [ - .macOS(.v12), - ], - products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), - ], - dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), - .package(name: "Shared", path: "../Shared"), - ], - targets: [ - .executableTarget( - name: "MyLambda", - dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - .product(name: "Shared", package: "Shared"), - ], - path: ".", - exclude: ["scripts/", "Dockerfile"] - ), - ] -) - -// for CI to test the local version of the library -if ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"] != nil { - package.dependencies = [ - .package(name: "swift-aws-lambda-runtime", path: "../../.."), - .package(name: "Shared", path: "../Shared"), - ] -} diff --git a/Examples/LocalDebugging/MyLambda/scripts/deploy.sh b/Examples/LocalDebugging/MyLambda/scripts/deploy.sh deleted file mode 100755 index 75be0ceb..00000000 --- a/Examples/LocalDebugging/MyLambda/scripts/deploy.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## 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 -## -##===----------------------------------------------------------------------===## - -set -eu - -executable=MyLambda -lambda_name=SwiftSample -s3_bucket=swift-lambda-test - -echo -e "\ndeploying $executable" - -echo "-------------------------------------------------------------------------" -echo "preparing docker build image" -echo "-------------------------------------------------------------------------" -docker build . -t builder -echo "done" - -echo "-------------------------------------------------------------------------" -echo "building \"$executable\" lambda" -echo "-------------------------------------------------------------------------" -docker run --rm -v `pwd`/../../..:/workspace -w /workspace/Examples/LocalDebugging/MyLambda builder \ - bash -cl "swift build --product $executable -c release" -echo "done" - -echo "-------------------------------------------------------------------------" -echo "packaging \"$executable\" lambda" -echo "-------------------------------------------------------------------------" -docker run --rm -v `pwd`:/workspace -w /workspace builder \ - bash -cl "./scripts/package.sh $executable" -echo "done" - -echo "-------------------------------------------------------------------------" -echo "uploading \"$executable\" lambda to s3" -echo "-------------------------------------------------------------------------" - -aws s3 cp .build/lambda/$executable/lambda.zip s3://$s3_bucket/ - -echo "-------------------------------------------------------------------------" -echo "updating \"$lambda_name\" to latest \"$executable\"" -echo "-------------------------------------------------------------------------" -aws lambda update-function-code --function $lambda_name --s3-bucket $s3_bucket --s3-key lambda.zip diff --git a/Examples/LocalDebugging/MyLambda/scripts/package.sh b/Examples/LocalDebugging/MyLambda/scripts/package.sh deleted file mode 100755 index 17d5853b..00000000 --- a/Examples/LocalDebugging/MyLambda/scripts/package.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## 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 -## -##===----------------------------------------------------------------------===## - -set -eu - -executable=$1 - -target=".build/lambda/$executable" -rm -rf "$target" -mkdir -p "$target" -cp ".build/release/$executable" "$target/" -# add the target deps based on ldd -ldd ".build/release/$executable" | grep swift | awk '{print $3}' | xargs cp -Lv -t "$target" -cd "$target" -ln -s "$executable" "bootstrap" -zip --symlinks lambda.zip * diff --git a/Examples/LocalDebugging/README.md b/Examples/LocalDebugging/README.md deleted file mode 100644 index 25ee92ba..00000000 --- a/Examples/LocalDebugging/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# Local Debugging Example - -This sample project demonstrates how to write a simple Lambda function in Swift, -and how to use local debugging techniques that simulate how the Lambda function -would be invoked by the AWS Lambda Runtime engine. - -The example includes an Xcode workspace with three modules: - -1. [MyApp](MyApp) is a SwiftUI iOS application that calls the Lambda function. -2. [MyLambda](MyLambda) is a SwiftPM executable package for the Lambda function. -3. [Shared](Shared) is a SwiftPM library package used for shared code between the iOS application and the Lambda function, -such as the Request and Response model objects. - -The local debugging experience is achieved by running the Lambda function in the context of the -debug-only local lambda engine simulator which starts a local HTTP server enabling the communication -between the iOS application and the Lambda function over HTTP. - -To try out this example, open the workspace in Xcode and "run" the two targets, -using the relevant `MyLambda` and `MyApp` Xcode schemes. - -Start with running the `MyLambda` target. -* Switch to the `MyLambda` scheme and select the "My Mac" destination -* Set the `LOCAL_LAMBDA_SERVER_ENABLED` environment variable to `true` by editing the `MyLambda` scheme Run/Arguments options. -* Hit `Run` -* Once it is up you should see a log message in the Xcode console saying -`LocalLambdaServer started and listening on 127.0.0.1:7000, receiving events on /invoke` -which means the local emulator is up and receiving traffic on port `7000` and expecting events on the `/invoke` endpoint. - -Continue to run the `MyApp` target -* Switch to the `MyApp` scheme and select a simulator destination. -* Hit `Run` -* Once up, the application's UI should appear in the simulator allowing you -to interact with it. - -Once both targets are running, set up breakpoints in the iOS application or Lambda function to observe the system behavior. diff --git a/Examples/LocalDebugging/Shared/Package.swift b/Examples/LocalDebugging/Shared/Package.swift deleted file mode 100644 index 96eb2003..00000000 --- a/Examples/LocalDebugging/Shared/Package.swift +++ /dev/null @@ -1,16 +0,0 @@ -// swift-tools-version:5.5 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "Shared", - products: [ - .library(name: "Shared", targets: ["Shared"]), - ], - dependencies: [ - ], - targets: [ - .target(name: "Shared", dependencies: []), - ] -) diff --git a/Examples/LocalDebugging/Shared/Sources/Shared/Shared.swift b/Examples/LocalDebugging/Shared/Sources/Shared/Shared.swift deleted file mode 100644 index 8189eac3..00000000 --- a/Examples/LocalDebugging/Shared/Sources/Shared/Shared.swift +++ /dev/null @@ -1,35 +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 -// -//===----------------------------------------------------------------------===// - -public struct Request: Codable, CustomStringConvertible { - public let name: String - public let password: String - - public init(name: String, password: String) { - self.name = name - self.password = password - } - - public var description: String { - "name: \(self.name), password: ***" - } -} - -public struct Response: Codable { - public let message: String - - public init(message: String) { - self.message = message - } -} diff --git a/Examples/README.md b/Examples/README.md new file mode 100644 index 00000000..c5f6d38c --- /dev/null +++ b/Examples/README.md @@ -0,0 +1,85 @@ +This directory contains example code for Lambda functions. + +## Pre-requisites + +- Ensure you have the Swift 6.x toolchain installed. You can [install Swift toolchains](https://www.swift.org/install/macos/) from Swift.org + +- When developing on macOS, be sure you use macOS 15 (Sequoia) or a more recent macOS version. + +- To build and archive your Lambda functions, you need to [install docker](https://docs.docker.com/desktop/install/mac-install/). + +- To deploy your Lambda functions and invoke them, you must have [an AWS account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-creating.html) and [install and configure the `aws` command line](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). + +- Some examples are using [AWS SAM](https://aws.amazon.com/serverless/sam/). Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) before deploying these examples. + +- Some examples are using the [AWS CDK](https://aws.amazon.com/cdk/). Install the [CDK CLI](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) before deploying these examples. + +## Examples + +- **[API Gateway V1](APIGatewayV1/README.md)**: an REST API with [Amazon API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) and a Lambda function as backend (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). + +- **[API Gateway V2](APIGatewayV2/README.md)**: an HTTPS API with [Amazon API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) and a Lambda function as backend (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). + +- **[API Gateway V2 with Lambda Authorizer](APIGatewayV2+LambdaAuthorizer/README.md)**: an HTTPS API with [Amazon API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) protected by a Lambda authorizer (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). + +- **[BackgroundTasks](BackgroundTasks/README.md)**: a Lambda function that continues to run background tasks after having sent the response (requires [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)). + +- **[CDK](CDK/README.md)**: a simple example of an AWS Lambda function invoked through an Amazon API Gateway and deployed with the Cloud Development Kit (CDK). + +- **[HelloJSON](HelloJSON/README.md)**: a Lambda function that accepts JSON as an input parameter and responds with a JSON output (requires [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)). + +- **[HelloWorld](HelloWorld/README.md)**: a simple Lambda function (requires [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)). + +- **[Hummingbird](Hummingbird/README.md)**: a Lambda function using the Hummingbird web framework with API Gateway integration (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). + +- **[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/)). + +- **[Streaming](Streaming/README.md)**: create a Lambda function exposed as an URL. The Lambda function streams its response over time. (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). + +- **[Streaming+Codable](Streaming+Codable/README.md)**: a Lambda function that combines JSON input decoding with response streaming capabilities, demonstrating a streaming codable interface (requires [AWS SAM](https://aws.amazon.com/serverless/sam/) or the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)). + +- **[Testing](Testing/README.md)**: a test suite for Lambda functions. + +## AWS Credentials and Signature + +This section is a short tutorial on the AWS Signature protocol and the AWS credentials. + +**What is AWS SigV4?** + +AWS SigV4, short for "Signature Version 4," is a protocol AWS uses to authenticate and secure requests. When you, as a developer, send a request to an AWS service, AWS SigV4 makes sure the request is verified and hasn’t been tampered with. This is done through a digital signature, which is created by combining your request details with your secret AWS credentials. This signature tells AWS that the request is genuine and is coming from a user who has the right permissions. + +**How to Obtain AWS Access Keys and Session Tokens** + +To start making authenticated requests with AWS SigV4, you’ll need three main pieces of information: + +1. **Access Key ID**: This is a unique identifier for your AWS account, IAM (Identity and Access Management) user, or federated user. + +2. **Secret Access Key**: This is a secret code that only you and AWS know. It works together with your access key ID to sign requests. + +3. **Session Token (Optional)**: If you're using temporary security credentials, AWS will also provide a session token. This is usually required if you're using temporary access (e.g., through AWS STS, which provides short-lived, temporary credentials, or for federated users). + +To obtain these keys, you need an AWS account: + +1. **Sign up or Log in to AWS Console**: Go to the [AWS Management Console](https://aws.amazon.com/console/), log in, or create an AWS account if you don’t have one. + +2. **Create IAM User**: In the console, go to IAM (Identity and Access Management) and create a new user. Ensure you set permissions that match what the user will need for your application (e.g., permissions to access specific AWS services, such as AWS Lambda). + +3. **Generate Access Key and Secret Access Key**: In the IAM user credentials section, find the option to generate an "Access Key" and "Secret Access Key." Save these securely! You’ll need them to authenticate your requests. + +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. + +## ⚠️ Security and Reliability Notice + +These are example applications for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: + +- Enable access logging on API Gateway ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)) +- Ensure that AWS Lambda function is configured for function-level concurrent execution limit ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +- Check encryption settings for Lambda environment variables ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +- Ensure that AWS Lambda function is configured for a Dead Letter Queue (DLQ) ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +- Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html), [code example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres)) \ No newline at end of file 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..24c16180 --- /dev/null +++ b/Examples/ResourcesPackaging/Package.swift @@ -0,0 +1,61 @@ +// swift-tools-version:6.1 +// This example has to be in Swift 6.1 because it is used in the test archive plugin CI job +// That job runs on GitHub's ubuntu-latest environment that only supports Swift 6.1 +// https://github.com/actions/runner-images?tab=readme-ov-file +// https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md +// We can update to Swift 6.2 when GitHUb hosts will have Swift 6.2 + +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", from: "2.0.0") + ], + 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/Examples/Testing/Sources/Lambda.swift b/Examples/ResourcesPackaging/Sources/main.swift similarity index 57% rename from Examples/Testing/Sources/Lambda.swift rename to Examples/ResourcesPackaging/Sources/main.swift index 568710e4..dccbd863 100644 --- a/Examples/Testing/Sources/Lambda.swift +++ b/Examples/ResourcesPackaging/Sources/main.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2021 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,13 +13,14 @@ //===----------------------------------------------------------------------===// import AWSLambdaRuntime +import Foundation -// in this example we are receiving and responding with strings - -@main -struct MyLambda: SimpleLambdaHandler { - func handle(_ event: String, context: LambdaContext) async throws -> String { - // as an example, respond with the event's reversed body - String(event.reversed()) +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..54fe7972 --- /dev/null +++ b/Examples/S3EventNotifier/Package.swift @@ -0,0 +1,50 @@ +// swift-tools-version: 6.2 +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", from: "2.0.0"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events", from: "1.0.0"), + ], + 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..ae45b540 --- /dev/null +++ b/Examples/S3EventNotifier/README.md @@ -0,0 +1,104 @@ +# 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. + +## ⚠️ Security and Reliability Notice + +These are example applications for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: + +- Enable access logging on API Gateway ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)) +- Ensure that AWS Lambda function is configured for function-level concurrent execution limit ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +- Check encryption settings for Lambda environment variables ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +- Ensure that AWS Lambda function is configured for a Dead Letter Queue (DLQ) ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +- Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html), [code example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres)) \ 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/.gitignore b/Examples/S3_AWSSDK/.gitignore new file mode 100644 index 00000000..70799e05 --- /dev/null +++ b/Examples/S3_AWSSDK/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +.aws-sam/ +.build +samtemplate.toml +*/build/* +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc \ No newline at end of file diff --git a/Examples/S3_AWSSDK/Package.swift b/Examples/S3_AWSSDK/Package.swift new file mode 100644 index 00000000..b4f0dbbd --- /dev/null +++ b/Examples/S3_AWSSDK/Package.swift @@ -0,0 +1,57 @@ +// swift-tools-version: 6.2 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "AWSSDKExample", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "AWSSDKExample", targets: ["AWSSDKExample"]) + ], + 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", from: "2.0.0"), + .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: [ + .executableTarget( + name: "AWSSDKExample", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + .product(name: "AWSS3", package: "aws-sdk-swift"), + ] + ) + ] +) + +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/S3_AWSSDK/README.md b/Examples/S3_AWSSDK/README.md new file mode 100644 index 00000000..9e8fcf70 --- /dev/null +++ b/Examples/S3_AWSSDK/README.md @@ -0,0 +1,99 @@ +# List Amazon S3 Buckets with the AWS SDK for Swift + +This is a simple example of an AWS Lambda function that uses the [AWS SDK for Swift](https://github.com/awslabs/aws-sdk-swift) to read data from Amazon S3. + +## Code + +The Lambda function reads all bucket names from your AWS account and returns them as a String. + +The code creates a `LambdaRuntime` struct. In it's simplest form, the initializer takes a function as argument. The function is the handler that will be invoked when the API Gateway receives an HTTP request. + +The handler is `(event: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response`. The function takes two arguments: +- the event argument is a `APIGatewayV2Request`. It is the parameter passed by the API Gateway. It contains all data passed in the HTTP request and some meta data. +- the context argument is a `Lambda Context`. It is a description of the runtime context. + +The function must return a `APIGatewayV2Response`. + +`APIGatewayV2Request` and `APIGatewayV2Response` are defined in the [Swift AWS Lambda Events](https://github.com/swift-server/swift-aws-lambda-events) library. + +The handler creates an S3 client and `ListBucketsInput` object. It passes the input object to the client and receives an output response. +It then extracts the list of bucket names from the output and creates a `\n`-separated list of names, as a `String` + +## Build & Package + +To build the package, type the following commands. + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/AWSSDKExample/AWSSDKExample.zip` + +## Deploy + +The deployment must include the Lambda function and an API Gateway. We use the [Serverless Application Model (SAM)](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) to deploy the infrastructure. + +**Prerequisites** : Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) + +The example directory contains a file named `template.yaml` that describes the deployment. + +To actually deploy your Lambda function and create the infrastructure, type the following `sam` command. + +```bash +sam deploy \ +--resolve-s3 \ +--template-file template.yaml \ +--stack-name AWSSDKExample \ +--capabilities CAPABILITY_IAM +``` + +At the end of the deployment, the script lists the API Gateway endpoint. +The output is similar to this one. + +``` +----------------------------------------------------------------------------------------------------------------------------- +Outputs +----------------------------------------------------------------------------------------------------------------------------- +Key APIGatewayEndpoint +Description API Gateway endpoint URL" +Value https://a5q74es3k2.execute-api.us-east-1.amazonaws.com +----------------------------------------------------------------------------------------------------------------------------- +``` + +## Invoke your Lambda function + +To invoke the Lambda function, use this `curl` command line. + +```bash +curl https://a5q74es3k2.execute-api.us-east-1.amazonaws.com +``` + +Be sure to replace the URL with the API Gateway endpoint returned in the previous step. + +This should print text similar to + +```bash +my_bucket_1 +my_bucket_2 +... +``` + +## Delete the infrastructure + +When done testing, you can delete the infrastructure with this command. + +```bash +sam delete +``` + +## ⚠️ Security and Reliability Notice + +These are example applications for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: + +- Enable access logging on API Gateway ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)) +- Ensure that AWS Lambda function is configured for function-level concurrent execution limit ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +- Check encryption settings for Lambda environment variables ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +- Ensure that AWS Lambda function is configured for a Dead Letter Queue (DLQ) ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +- Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html), [code example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres)) \ No newline at end of file diff --git a/Examples/S3_AWSSDK/Sources/main.swift b/Examples/S3_AWSSDK/Sources/main.swift new file mode 100644 index 00000000..6665893c --- /dev/null +++ b/Examples/S3_AWSSDK/Sources/main.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// 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 AWSLambdaEvents +import AWSLambdaRuntime +@preconcurrency import AWSS3 + +let client = try await S3Client() + +let runtime = LambdaRuntime { + (event: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response in + + var response: APIGatewayV2Response + do { + // read the list of buckets + context.logger.debug("Reading list of buckets") + let output = try await client.listBuckets(input: ListBucketsInput()) + let bucketList = output.buckets?.compactMap { $0.name } + response = APIGatewayV2Response(statusCode: .ok, body: bucketList?.joined(separator: "\n")) + } catch { + context.logger.error("\(error)") + response = APIGatewayV2Response(statusCode: .internalServerError, body: "[ERROR] \(error)") + } + return response +} + +try await runtime.run() diff --git a/Examples/S3_AWSSDK/template.yaml b/Examples/S3_AWSSDK/template.yaml new file mode 100644 index 00000000..f14d966e --- /dev/null +++ b/Examples/S3_AWSSDK/template.yaml @@ -0,0 +1,59 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for AWS SDK Example + +# This is an example SAM template for the purpose of this project. +# When deploying such infrastructure in production environment, +# we strongly encourage you to follow these best practices for improved security and resiliency +# - Enable access loggin on API Gateway +# See: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html) +# - Ensure that AWS Lambda function is configured for function-level concurrent execution limit +# See: https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html +# https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html +# - Check encryption settings for Lambda environment variable +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html +# - Ensure that AWS Lambda function is configured for a Dead Letter Queue(DLQ) +# See: https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq +# - Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html +# Code Example: https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres + +Resources: + # Lambda function + AWSSDKExample: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/AWSSDKExample/AWSSDKExample.zip + Timeout: 60 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 128 + Architectures: + - arm64 + Environment: + Variables: + # by default, AWS Lambda runtime produces no log + # use `LOG_LEVEL: debug` for for lifecycle and event handling information + # use `LOG_LEVEL: trace` for detailed input event information + LOG_LEVEL: debug + + # Handles all methods of the REST API + Events: + Api: + Type: HttpApi + + # Add an IAM policy to this function. + # It grants the function permissions to read the list of buckets in your account. + Policies: + - Statement: + - Sid: ListAllS3BucketsInYourAccount + Effect: Allow + Action: + - s3:ListAllMyBuckets + Resource: '*' + +# print API endpoint +Outputs: + SwiftAPIEndpoint: + Description: "API Gateway endpoint URL for your application" + Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" diff --git a/Examples/S3_Soto/.gitignore b/Examples/S3_Soto/.gitignore new file mode 100644 index 00000000..70799e05 --- /dev/null +++ b/Examples/S3_Soto/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +.aws-sam/ +.build +samtemplate.toml +*/build/* +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc \ No newline at end of file diff --git a/Examples/S3_Soto/Package.swift b/Examples/S3_Soto/Package.swift new file mode 100644 index 00000000..8302b370 --- /dev/null +++ b/Examples/S3_Soto/Package.swift @@ -0,0 +1,58 @@ +// swift-tools-version: 6.2 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "SotoExample", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "SotoExample", targets: ["SotoExample"]) + ], + dependencies: [ + .package(url: "https://github.com/soto-project/soto.git", from: "7.0.0"), + + // 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", from: "2.0.0"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events", from: "1.0.0"), + ], + targets: [ + .executableTarget( + name: "SotoExample", + dependencies: [ + .product(name: "SotoS3", package: "soto"), + .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/S3_Soto/README.md b/Examples/S3_Soto/README.md new file mode 100644 index 00000000..578b6817 --- /dev/null +++ b/Examples/S3_Soto/README.md @@ -0,0 +1,99 @@ +# List Amazon S3 Buckets with Soto + +This is a simple example of an AWS Lambda function that uses the [Soto SDK for AWS](https://github.com/soto-project/soto) to read data from Amazon S3. + +## Code + +The Lambda function reads all bucket names from your AWS account and returns them as a String. + +The code creates a `LambdaRuntime` struct. In it's simplest form, the initializer takes a function as argument. The function is the handler that will be invoked when the API Gateway receives an HTTP request. + +The handler is `(event: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response`. The function takes two arguments: +- the event argument is a `APIGatewayV2Request`. It is the parameter passed by the API Gateway. It contains all data passed in the HTTP request and some meta data. +- the context argument is a `Lambda Context`. It is a description of the runtime context. + +The function must return a `APIGatewayV2Response`. + +`APIGatewayV2Request` and `APIGatewayV2Response` are defined in the [Swift AWS Lambda Events](https://github.com/swift-server/swift-aws-lambda-events) library. + +The handler creates two clients : an AWS client that manages the communication with AWS API and and the S3 client that expose the S3 API. Then, the handler calls `listBuckets()` on the S3 client and receives an output response. +Finally, the handler extracts the list of bucket names from the output to create a `\n`-separated list of names, as a `String`. + +## Build & Package + +To build the package, type the following command. + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/SotoExample/SotoExample.zip` + +## Deploy + +The deployment must include the Lambda function and an API Gateway. We use the [Serverless Application Model (SAM)](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) to deploy the infrastructure. + +**Prerequisites** : Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) + +The example directory contains a file named `template.yaml` that describes the deployment. + +To actually deploy your Lambda function and create the infrastructure, type the following `sam` command. + +```bash +sam deploy \ +--resolve-s3 \ +--template-file template.yaml \ +--stack-name SotoExample \ +--capabilities CAPABILITY_IAM +``` + +At the end of the deployment, the script lists the API Gateway endpoint. +The output is similar to this one. + +``` +----------------------------------------------------------------------------------------------------------------------------- +Outputs +----------------------------------------------------------------------------------------------------------------------------- +Key APIGatewayEndpoint +Description API Gateway endpoint URL" +Value https://a5q74es3k2.execute-api.us-east-1.amazonaws.com +----------------------------------------------------------------------------------------------------------------------------- +``` + +## Invoke your Lambda function + +To invoke the Lambda function, use this `curl` command line. + +```bash +curl https://a5q74es3k2.execute-api.us-east-1.amazonaws.com +``` + +Be sure to replace the URL with the API Gateway endpoint returned in the previous step. + +This should print text similar to + +```bash +my_bucket_1 +my_bucket_2 +... +``` + +## Delete the infrastructure + +When done testing, you can delete the infrastructure with this command. + +```bash +sam delete +``` + +## ⚠️ Security and Reliability Notice + +These are example applications for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: + +- Enable access logging on API Gateway ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)) +- Ensure that AWS Lambda function is configured for function-level concurrent execution limit ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +- Check encryption settings for Lambda environment variables ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +- Ensure that AWS Lambda function is configured for a Dead Letter Queue (DLQ) ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +- Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html), [code example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres)) \ No newline at end of file diff --git a/Examples/S3_Soto/Sources/main.swift b/Examples/S3_Soto/Sources/main.swift new file mode 100644 index 00000000..caa70116 --- /dev/null +++ b/Examples/S3_Soto/Sources/main.swift @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// 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 AWSLambdaEvents +import AWSLambdaRuntime +import SotoS3 + +let client = AWSClient() +let s3 = S3(client: client, region: .useast1) + +func handler(event: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response { + + var response: APIGatewayV2Response + do { + context.logger.debug("Reading list of buckets") + + // read the list of buckets + let bucketResponse = try await s3.listBuckets() + let bucketList = bucketResponse.buckets?.compactMap { $0.name } + response = APIGatewayV2Response(statusCode: .ok, body: bucketList?.joined(separator: "\n")) + } catch { + context.logger.error("\(error)") + response = APIGatewayV2Response(statusCode: .internalServerError, body: "[ERROR] \(error)") + } + return response +} + +let runtime = LambdaRuntime.init(body: handler) + +try await runtime.run() +try await client.shutdown() diff --git a/Examples/S3_Soto/template.yaml b/Examples/S3_Soto/template.yaml new file mode 100644 index 00000000..21ee4ee4 --- /dev/null +++ b/Examples/S3_Soto/template.yaml @@ -0,0 +1,59 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for AWS SDK Example + +# This is an example SAM template for the purpose of this project. +# When deploying such infrastructure in production environment, +# we strongly encourage you to follow these best practices for improved security and resiliency +# - Enable access loggin on API Gateway +# See: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html) +# - Ensure that AWS Lambda function is configured for function-level concurrent execution limit +# See: https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html +# https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html +# - Check encryption settings for Lambda environment variable +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html +# - Ensure that AWS Lambda function is configured for a Dead Letter Queue(DLQ) +# See: https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq +# - Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html +# Code Example: https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres + +Resources: + # Lambda function + SotoExample: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/SotoExample/SotoExample.zip + Timeout: 60 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 128 + Architectures: + - arm64 + Environment: + Variables: + # by default, AWS Lambda runtime produces no log + # use `LOG_LEVEL: debug` for for lifecycle and event handling information + # use `LOG_LEVEL: trace` for detailed input event information + LOG_LEVEL: debug + + # Handles all methods of the REST API + Events: + Api: + Type: HttpApi + + # Add an IAM policy to this function. + # It grants the function permissions to read the list of buckets in your account. + Policies: + - Statement: + - Sid: ListAllS3BucketsInYourAccount + Effect: Allow + Action: + - s3:ListAllMyBuckets + Resource: '*' + +# print API endpoint +Outputs: + SwiftAPIEndpoint: + Description: "API Gateway endpoint URL for your application" + Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" diff --git a/Examples/ServiceLifecycle+Postgres/.gitignore b/Examples/ServiceLifecycle+Postgres/.gitignore new file mode 100644 index 00000000..c35fd53d --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +.amazonq \ No newline at end of file diff --git a/Examples/ServiceLifecycle+Postgres/INFRASTRUCTURE.md b/Examples/ServiceLifecycle+Postgres/INFRASTRUCTURE.md new file mode 100644 index 00000000..55ab535a --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/INFRASTRUCTURE.md @@ -0,0 +1,161 @@ +# Infrastructure Architecture + +This document describes the AWS infrastructure deployed by the ServiceLifecycle example's SAM template. + +## Overview + +The infrastructure consists of a secure VPC setup with private subnets only, containing both the PostgreSQL RDS instance and Lambda function. The architecture is optimized for cost and security with complete network isolation. + +## Network Architecture + +### VPC Configuration +- **VPC**: Custom VPC with CIDR block `10.0.0.0/16` +- **DNS Support**: DNS hostnames and DNS resolution enabled + +### Subnet Layout +- **Private Subnets**: + - Private Subnet 1: `10.0.3.0/24` (AZ 1) + - Private Subnet 2: `10.0.4.0/24` (AZ 2) + - Used for RDS PostgreSQL database and Lambda function + - No public IP addresses assigned + - Complete isolation from internet + +### Network Components +- **VPC-only architecture**: No internet connectivity required +- **Route Tables**: Default VPC routing for internal communication + +## Security Groups + +### Lambda Security Group +- **Outbound Rules**: + - PostgreSQL (5432): Restricted to VPC CIDR `10.0.0.0/16` + +### Database Security Group +- **Inbound Rules**: + - PostgreSQL (5432): Only allows connections from the Lambda Security Group + +## Database Configuration + +### PostgreSQL RDS Instance +- **Instance Type**: `db.t3.micro` (cost-optimized) +- **Engine**: PostgreSQL 15.7 +- **Storage**: 20GB GP2 (SSD) +- **Network**: Deployed in private subnets with no public access +- **Security**: + - Storage encryption enabled + - SSL/TLS connections supported + - Credentials stored in AWS Secrets Manager +- **High Availability**: Multi-AZ disabled (development configuration) +- **Backup**: Automated backups disabled (development configuration) + +### Database Subnet Group +- Spans both private subnets for availability + +## Lambda Function Configuration + +### Service Lifecycle Lambda +- **Runtime**: Custom runtime (provided.al2) +- **Architecture**: ARM64 +- **Memory**: 128MB +- **Timeout**: 60 seconds +- **Network**: Deployed in private subnets with access to database within VPC +- **Environment Variables**: + - `LOG_LEVEL`: trace + - `DB_HOST`: RDS endpoint address + - `DB_USER`: Retrieved from Secrets Manager + - `DB_PASSWORD`: Retrieved from Secrets Manager + - `DB_NAME`: Database name from parameter + +## API Gateway + +- **Type**: HTTP API +- **Integration**: Direct Lambda integration +- **Authentication**: None (for demonstration purposes) + +## Secrets Management + +### Database Credentials +- **Storage**: AWS Secrets Manager +- **Secret Name**: `{StackName}-db-credentials` +- **Content**: + - Username: "postgres" + - Password: Auto-generated 16-character password + - Special characters excluded: `"@/\` + +## SAM Outputs + +The template provides several outputs to facilitate working with the deployed resources: + +- **APIGatewayEndpoint**: URL to invoke the Lambda function +- **DatabaseEndpoint**: Hostname for the PostgreSQL instance +- **DatabasePort**: Port number for PostgreSQL (5432) +- **DatabaseName**: Name of the created database +- **DatabaseSecretArn**: ARN of the secret containing credentials +- **DatabaseConnectionInstructions**: Instructions for retrieving connection details +- **ConnectionDetails**: Consolidated connection information + +## Security Considerations + +This infrastructure implements several security best practices: + +1. **Complete Network Isolation**: Both database and Lambda are in private subnets with no direct acces to or from the internet +2. **Least Privilege**: Security groups restrict traffic to only necessary ports and sources +3. **Encryption**: Database storage is encrypted at rest +4. **Secure Credentials**: Database credentials are managed through AWS Secrets Manager +5. **Secure Communication**: Lambda function connects to database over encrypted connections + +## Cost Analysis + +### Monthly Cost Breakdown (US East 1 Region) + +#### Billable AWS Resources: + +**1. RDS PostgreSQL Database** +- Instance (db.t3.micro): $13.87/month (730 hours × $0.019/hour) +- Storage (20GB GP2): $2.30/month (20GB × $0.115/GB/month) +- Backup Storage: $0 (BackupRetentionPeriod: 0) +- Multi-AZ: $0 (disabled) +- **RDS Subtotal: $16.17/month** + +**2. AWS Secrets Manager** +- Secret Storage: $0.40/month per secret +- API Calls: ~$0.05 per 10,000 calls (minimal for Lambda access) +- **Secrets Manager Subtotal: ~$0.45/month** + +**3. AWS Lambda** +- Memory: 512MB ARM64 +- Free Tier: 1M requests + 400,000 GB-seconds/month +- Development Usage: $0 (within free tier) +- **Lambda Subtotal: $0/month** + +**4. API Gateway (HTTP API)** +- Free Tier: 1M requests/month +- Development Usage: $0 (within free tier) +- **API Gateway Subtotal: $0/month** + +#### Free AWS Resources: +- VPC, Private Subnets, Security Groups, DB Subnet Group: $0 + +### Total Monthly Cost: + +| Service | Cost | Notes | +|---------|------|---------| +| RDS PostgreSQL | $16.17 | db.t3.micro + 20GB storage | +| Secrets Manager | $0.45 | 1 secret + minimal API calls | +| Lambda | $0.00 | Within free tier | +| API Gateway | $0.00 | Within free tier | +| VPC Components | $0.00 | No charges | +| **TOTAL** | **$16.62/month** | | + +### With RDS Free Tier (First 12 Months): +- RDS Instance: $0 (750 hours/month free) +- RDS Storage: $0 (20GB free) +- **Total with Free Tier: ~$0.45/month** + +### Production Scaling Estimates: +- Higher Lambda usage: +$0.20 per million requests +- More RDS storage: +$0.115 per additional GB/month +- Multi-AZ RDS: ~2x RDS instance cost +- Backup storage: $0.095/GB/month + +This architecture provides maximum cost efficiency while maintaining security and functionality for development workloads. \ No newline at end of file diff --git a/Examples/ServiceLifecycle+Postgres/Package.swift b/Examples/ServiceLifecycle+Postgres/Package.swift new file mode 100644 index 00000000..837269ee --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/Package.swift @@ -0,0 +1,58 @@ +// swift-tools-version: 6.2 +// 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: "LambdaWithServiceLifecycle", + platforms: [ + .macOS(.v15) + ], + dependencies: [ + .package(url: "https://github.com/vapor/postgres-nio.git", from: "1.26.0"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "2.0.0"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.0.0"), + .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.6.3"), + ], + targets: [ + .executableTarget( + name: "LambdaWithServiceLifecycle", + dependencies: [ + .product(name: "PostgresNIO", package: "postgres-nio"), + .product(name: "ServiceLifecycle", package: "swift-service-lifecycle"), + .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/ServiceLifecycle+Postgres/README.md b/Examples/ServiceLifecycle+Postgres/README.md new file mode 100644 index 00000000..2e32fd48 --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/README.md @@ -0,0 +1,263 @@ +# A swift Service Lifecycle Lambda function with a managed PostgreSQL database + +This example demonstrates a Swift Lambda function that uses Swift Service Lifecycle to manage a PostgreSQL connection. The function connects to an RDS PostgreSQL database in private subnets and queries user data. + +## Architecture + +- **Swift Lambda Function**: A network isolated Lambda function that Uses Swift ServiceLifecycle to manage PostgreSQL client lifecycle +- **PostgreSQL on Amazon RDS**: Database instance in private subnets with SSL/TLS encryption +- **HTTP API Gateway**: HTTP endpoint to invoke the Lambda function +- **VPC**: Custom VPC with private subnets only for complete network isolation +- **Security**: SSL/TLS connections with RDS root certificate verification, secure networking with security groups +- **Timeout Handling**: 3-second timeout mechanism to prevent database connection freeze +- **Secrets Manager**: Secure credential storage and management + +For detailed infrastructure and cost information, see `INFRASTRUCTURE.md`. + +## Implementation Details + +The Lambda function demonstrates several key concepts: + +1. **ServiceLifecycle Integration**: The PostgreSQL client and Lambda runtime are managed together using ServiceLifecycle, ensuring proper initialization and cleanup. + +2. **SSL/TLS Security**: Connections to RDS use SSL/TLS with full certificate verification using region-specific RDS root certificates. + +3. **Timeout Protection**: A custom timeout mechanism prevents the function from freezing when the database is unreachable (addresses PostgresNIO issue #489). + +4. **Structured Response**: Returns a JSON array of `User` objects, making it suitable for API integration. + +5. **Error Handling**: Comprehensive error handling for database connections, queries, and certificate loading. + +## Prerequisites + +- Swift 6.x toolchain +- Docker (for building Lambda functions) +- AWS CLI configured with appropriate permissions +- SAM CLI installed + +## Database Schema + +In the context of this demo, the Lambda function creates the table and populates it with data at first run. + +The Lambda function expects a `users` table with the following structure and returns results as `User` objects: + +```sql +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) NOT NULL +); + +-- Insert some sample data +INSERT INTO users (username) VALUES ('alice'), ('bob'), ('charlie'); +``` + +The Swift `User` model: +```swift +struct User: Codable { + let id: Int + let username: String +} +``` + +## Environment Variables + +The Lambda function uses the following environment variables for database connection: +- `DB_HOST`: Database hostname (set by CloudFormation from RDS endpoint) +- `DB_USER`: Database username (retrieved from Secrets Manager) +- `DB_PASSWORD`: Database password (retrieved from Secrets Manager) +- `DB_NAME`: Database name (defaults to "test") +- `AWS_REGION`: AWS region for selecting the correct RDS root certificate + +## Deployment + +### Option 1: Using the deployment script + +```bash +./deploy.sh +``` + +### Option 2: Manual deployment + +1. **Build the Lambda function:** + ```bash + swift package archive --allow-network-connections docker + ``` + +2. **Deploy with SAM:** + ```bash + sam deploy + ``` + +## Getting Connection Details + +After deployment, get the database and API Gateway connection details: + +```bash +aws cloudformation describe-stacks \ + --stack-name servicelifecycle-stack \ + --query 'Stacks[0].Outputs' +``` + +The output will include: +- **DatabaseEndpoint**: Hostname to connect to +- **DatabasePort**: Port number (5432) +- **DatabaseName**: Database name +- **DatabaseUsername**: Username +- **DatabasePassword**: Password +- **DatabaseConnectionString**: Complete connection string + +## Connecting to the Database + +The database is deployed in **private subnets** and is **not directly accessible** from the internet. This follows AWS security best practices. + +To connect to the database, you would need to create an Amazon EC2 instance in a public subnet (which you'd need to add to the VPC) or use AWS Systems Manager Session Manager for secure access to an EC2 instance in a private subnet. The current template uses a private-only architecture for maximum security. + +You can access the database connection details in the output of the SAM template: + +```bash +# Get the connection details from CloudFormation outputs +DB_HOST=$(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`DatabaseEndpoint`].OutputValue' --output text) +DB_PORT=$(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`DatabasePort`].OutputValue' --output text) +DB_NAME=$(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`DatabaseName`].OutputValue' --output text) + +# Get the database password from Secrets Manager +SECRET_ARN=$(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`DatabaseSecretArn`].OutputValue' --output text) +DB_USERNAME=$(aws secretsmanager get-secret-value --secret-id "$SECRET_ARN" --query 'SecretString' --output text | jq -r '.username') +DB_PASSWORD=$(aws secretsmanager get-secret-value --secret-id "$SECRET_ARN" --query 'SecretString' --output text | jq -r '.password') + +# Connect with psql on Amazon EC2 +psql -h "$DB_HOST:$DB_PORT" -U "$DB_USER" -d "$DB_NAME" +``` + +## Testing the Lambda Function + +Get the API Gateway endpoint and test the function: + +```bash +# Get the API endpoint +API_ENDPOINT=$(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`APIGatewayEndpoint`].OutputValue' --output text) + +# Test the function +curl "$API_ENDPOINT" +``` + +The function will: +1. Connect to the PostgreSQL database using SSL/TLS with RDS root certificate verification +2. Query the `users` table with a 3-second timeout to prevent freezing +3. Log the results for each user found +4. Return a JSON array of `User` objects with `id` and `username` fields + +Example response: +```json +[ + {"id": 1, "username": "alice"}, + {"id": 2, "username": "bob"}, + {"id": 3, "username": "charlie"} +] +``` + +## Monitoring + +Check the Lambda function logs: + +```bash +sam logs -n ServiceLifecycleLambda --stack-name servicelifecycle-stack --tail +``` + +## Security Considerations + +✅ **Security Best Practices Implemented**: + +This example follows AWS security best practices: + +1. **Private Database**: Database is deployed in private subnets with no internet access +2. **Complete Network Isolation**: Private subnets only with no internet connectivity +3. **Security Groups**: Restrictive security groups following least privilege principle +4. **Secrets Management**: Database credentials stored in AWS Secrets Manager +5. **Encryption**: SSL/TLS for database connections with certificate verification +6. **Minimal Attack Surface**: No public subnets or internet gateways + +The infrastructure implements secure networking patterns suitable for production workloads. + +## Cost Optimization + +The template is optimized for cost: +- `db.t3.micro` instance (eligible for free tier) +- Minimal storage allocation (20GB) +- No Multi-AZ deployment +- No automated backups +- No NAT Gateway or Internet Gateway +- Private-only architecture + +**Estimated cost: ~$16.62/month (or ~$0.45/month with RDS Free Tier)** + +For detailed cost breakdown, see `INFRASTRUCTURE.md`. + +## Cleanup + +To delete all resources: + +```bash +sam delete --stack-name servicelifecycle-stack +``` + +## SSL Certificate Support + +This example includes RDS root certificates for secure SSL/TLS connections. Currently supported regions: +- `us-east-1`: US East (N. Virginia) +- `eu-central-1`: Europe (Frankfurt) + +To add support for additional regions: +1. Download the appropriate root certificate from [AWS RDS SSL documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html) +2. Create a new Swift file in `Sources/RDSCertificates/` with the certificate PEM data +3. Add the region mapping to `rootRDSCertificates` dictionary in `RootRDSCert.swift` + +## Troubleshooting + +when deploying with SAM and the `template.yaml` file included in this example, there shouldn't be any error. However, when you try to create such infarstructure on your own or using different infrastructure as code (IaC) tools, it's likely to iterate before getting everything configured. We compiled a couple of the most common configuration errors and their solution: + +### Lambda can't connect to database + +1. Check security groups allow traffic on port 5432 between Lambda and RDS security groups +2. Verify both Lambda and RDS are deployed in the same private subnets +3. Verify database credentials are correctly retrieved from Secrets Manager and that the Lambda execution policies have permissions to read the secret +4. Ensure the RDS instance is running and healthy + +### Database connection timeout + +The PostgreSQL client may freeze if the database is unreachable. This example implements a 3-second timeout mechanism to prevent this issue. If the connection or query takes longer than 3 seconds, the function will timeout and return an empty array. Ensure: +1. Database is running and accessible +2. Security groups are properly configured +3. Network connectivity is available +4. SSL certificates are properly configured for your AWS region + +### Build failures + +Ensure you have: +1. Swift 6.x toolchain installed +2. Docker running +3. Proper network connectivity for downloading dependencies +4. All required dependencies: PostgresNIO, AWSLambdaRuntime, and ServiceLifecycle + +## Files + +- `template.yaml`: SAM template defining all AWS resources +- `INFRASTRUCTURE.md`: Detailed infrastructure architecture documentation +- `samconfig.toml`: SAM configuration file +- `deploy.sh`: Deployment script +- `Sources/Lambda.swift`: Swift Lambda function code with ServiceLifecycle integration +- `Sources/Timeout.swift`: Timeout utility to prevent database connection freezes +- `Sources/RDSCertificates/RootRDSCert.swift`: RDS root certificate management +- `Sources/RDSCertificates/us-east-1.swift`: US East 1 region root certificate +- `Sources/RDSCertificates/eu-central-1.swift`: EU Central 1 region root certificate +- `Package.swift`: Swift package definition with PostgresNIO, AWSLambdaRuntime, and ServiceLifecycle dependencies + +## ⚠️ Security and Reliability Notice + +These are example applications for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: + +- Enable access logging on API Gateway ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)) +- Ensure that AWS Lambda function is configured for function-level concurrent execution limit ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +- Check encryption settings for Lambda environment variables ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +- Ensure that AWS Lambda function is configured for a Dead Letter Queue (DLQ) ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +- Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html), [code example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres)) \ No newline at end of file diff --git a/Examples/ServiceLifecycle+Postgres/Sources/Lambda.swift b/Examples/ServiceLifecycle+Postgres/Sources/Lambda.swift new file mode 100644 index 00000000..a59654fb --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/Sources/Lambda.swift @@ -0,0 +1,192 @@ +//===----------------------------------------------------------------------===// +// +// 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 Logging +import PostgresNIO +import ServiceLifecycle + +struct User: Codable { + let id: Int + let username: String +} + +@main +struct LambdaFunction { + + private let pgClient: PostgresClient + private let logger: Logger + + private init() throws { + var logger = Logger(label: "ServiceLifecycleExample") + logger.logLevel = Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info + self.logger = logger + + self.pgClient = try LambdaFunction.createPostgresClient( + host: Lambda.env("DB_HOST") ?? "localhost", + user: Lambda.env("DB_USER") ?? "postgres", + password: Lambda.env("DB_PASSWORD") ?? "secret", + dbName: Lambda.env("DB_NAME") ?? "servicelifecycle", + logger: self.logger + ) + } + + /// Function entry point when the runtime environment is created + private func main() async throws { + + // Instantiate LambdaRuntime with a handler implementing the business logic of the Lambda function + let lambdaRuntime = LambdaRuntime(logger: self.logger, body: self.handler) + + // Use a prelude service to execute PG code before setting up the Lambda service + // the PG code will run only once and will create the database schema and populate it with initial data + let preludeService = PreludeService( + service: lambdaRuntime, + prelude: { + try await prepareDatabase() + } + ) + + /// Use ServiceLifecycle to manage the initialization and termination + /// of the PGClient together with the LambdaRuntime + let serviceGroup = ServiceGroup( + services: [self.pgClient, preludeService], + gracefulShutdownSignals: [.sigterm], + cancellationSignals: [.sigint], + logger: self.logger + ) + + // launch the service groups + // this call will return upon termination or cancellation of all the services + try await serviceGroup.run() + + // perform any cleanup here + } + + /// Function handler. This code is called at each function invocation + /// input event is ignored in this demo. + private func handler(event: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response { + + var result: [User] = [] + do { + // IMPORTANT - CURRENTLY, THIS CALL STOPS WHEN DB IS NOT REACHABLE + // See: https://github.com/vapor/postgres-nio/issues/489 + // This is why there is a timeout, as suggested Fabian + // See: https://github.com/vapor/postgres-nio/issues/489#issuecomment-2186509773 + result = try await timeout(deadline: .seconds(3)) { + // query users + logger.trace("Querying database") + return try await self.queryUsers() + } + } catch { + logger.error("Database Error", metadata: ["cause": "\(String(reflecting: error))"]) + } + + return try .init( + statusCode: .ok, + headers: ["content-type": "application/json"], + encodableBody: result + ) + } + + /// Prepare the database + /// At first run, this functions checks the database exist and is populated. + /// This is useful for demo purposes. In real life, the database will contain data already. + private func prepareDatabase() async throws { + do { + + // initial creation of the table. This will fails if it already exists + logger.trace("Testing if table exists") + try await self.pgClient.query(SQLStatements.createTable) + + // it did not fail, it means the table is new and empty + logger.trace("Populate table") + try await self.pgClient.query(SQLStatements.populateTable) + + } catch is PSQLError { + // when there is a database error, it means the table or values already existed + // ignore this error + logger.trace("Table exists already") + } catch { + // propagate other errors + throw error + } + } + + /// Query the database + private func queryUsers() async throws -> [User] { + var users: [User] = [] + let query = SQLStatements.queryAllUsers + let rows = try await self.pgClient.query(query) + for try await (id, username) in rows.decode((Int, String).self) { + self.logger.trace("\(id) : \(username)") + users.append(User(id: id, username: username)) + } + return users + } + + /// Create a postgres client + /// ...TODO + private static func createPostgresClient( + host: String, + user: String, + password: String, + dbName: String, + logger: Logger + ) throws -> PostgresClient { + + // Load the root certificate + let region = Lambda.env("AWS_REGION") ?? "us-east-1" + guard let pem = rootRDSCertificates[region] else { + logger.error("No root certificate found for the specified AWS region.") + throw LambdaErrors.missingRootCertificateForRegion(region) + } + let certificatePEM = Array(pem.utf8) + let rootCert = try NIOSSLCertificate.fromPEMBytes(certificatePEM) + + // Add the root certificate to the TLS configuration + var tlsConfig = TLSConfiguration.makeClientConfiguration() + tlsConfig.trustRoots = .certificates(rootCert) + + // Enable full verification + tlsConfig.certificateVerification = .fullVerification + + let config = PostgresClient.Configuration( + host: host, + port: 5432, + username: user, + password: password, + database: dbName, + tls: .prefer(tlsConfig) + ) + + return PostgresClient(configuration: config) + } + + private struct SQLStatements { + static let createTable: PostgresQuery = + "CREATE TABLE users (id SERIAL PRIMARY KEY, username VARCHAR(50) NOT NULL);" + static let populateTable: PostgresQuery = "INSERT INTO users (username) VALUES ('alice'), ('bob'), ('charlie');" + static let queryAllUsers: PostgresQuery = "SELECT id, username FROM users" + } + + static func main() async throws { + try await LambdaFunction().main() + } + +} + +public enum LambdaErrors: Error { + case missingRootCertificateForRegion(String) +} diff --git a/Examples/ServiceLifecycle+Postgres/Sources/PreludeService.swift b/Examples/ServiceLifecycle+Postgres/Sources/PreludeService.swift new file mode 100644 index 00000000..a64053a2 --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/Sources/PreludeService.swift @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2024 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Copied from https://github.com/hummingbird-project/hummingbird/blob/main/Sources/Hummingbird/Utils/PreludeService.swift + +import ServiceLifecycle + +/// Wrap another service to run after a prelude closure has completed +struct PreludeService: Service, CustomStringConvertible { + let prelude: @Sendable () async throws -> Void + let service: S + + var description: String { + "PreludeService<\(S.self)>" + } + + init(service: S, prelude: @escaping @Sendable () async throws -> Void) { + self.service = service + self.prelude = prelude + } + + func run() async throws { + try await self.prelude() + try await self.service.run() + } +} + +extension Service { + /// Build existential ``PreludeService`` from an existential `Service` + func withPrelude(_ prelude: @escaping @Sendable () async throws -> Void) -> Service { + PreludeService(service: self, prelude: prelude) + } +} diff --git a/Examples/ServiceLifecycle+Postgres/Sources/RDSCertificates/RootRDSCert.swift b/Examples/ServiceLifecycle+Postgres/Sources/RDSCertificates/RootRDSCert.swift new file mode 100644 index 00000000..f89c5fbb --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/Sources/RDSCertificates/RootRDSCert.swift @@ -0,0 +1,22 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// you can download the root certificate for your RDS instance region from the following link: +// https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html + +let rootRDSCertificates = [ + "eu-central-1": eu_central_1_bundle_pem, + "us-east-1": us_east_1_bundle_pem, + // add more regions as needed +] diff --git a/Examples/ServiceLifecycle+Postgres/Sources/RDSCertificates/eu-central-1.swift b/Examples/ServiceLifecycle+Postgres/Sources/RDSCertificates/eu-central-1.swift new file mode 100644 index 00000000..e602ebf2 --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/Sources/RDSCertificates/eu-central-1.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +let eu_central_1_bundle_pem = """ + -----BEGIN CERTIFICATE----- + MIICtDCCAjmgAwIBAgIQenQbcP/Zbj9JxvZ+jXbRnTAKBggqhkjOPQQDAzCBmTEL + MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x + EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTIwMAYDVQQDDClBbWF6 + b24gUkRTIGV1LWNlbnRyYWwtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwH + U2VhdHRsZTAgFw0yMTA1MjEyMjMzMjRaGA8yMTIxMDUyMTIzMzMyNFowgZkxCzAJ + BgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMw + EQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEyMDAGA1UEAwwpQW1hem9u + IFJEUyBldS1jZW50cmFsLTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl + YXR0bGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATlBHiEM9LoEb1Hdnd5j2VpCDOU + 5nGuFoBD8ROUCkFLFh5mHrHfPXwBc63heW9WrP3qnDEm+UZEUvW7ROvtWCTPZdLz + Z4XaqgAlSqeE2VfUyZOZzBSgUUJk7OlznXfkCMOjQjBAMA8GA1UdEwEB/wQFMAMB + Af8wHQYDVR0OBBYEFDT/ThjQZl42Nv/4Z/7JYaPNMly2MA4GA1UdDwEB/wQEAwIB + hjAKBggqhkjOPQQDAwNpADBmAjEAnZWmSgpEbmq+oiCa13l5aGmxSlfp9h12Orvw + Dq/W5cENJz891QD0ufOsic5oGq1JAjEAp5kSJj0MxJBTHQze1Aa9gG4sjHBxXn98 + 4MP1VGsQuhfndNHQb4V0Au7OWnOeiobq + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIIEBTCCAu2gAwIBAgIRAO8bekN7rUReuNPG8pSTKtEwDQYJKoZIhvcNAQELBQAw + gZoxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ + bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEzMDEGA1UEAwwq + QW1hem9uIFJEUyBldS1jZW50cmFsLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYD + VQQHDAdTZWF0dGxlMCAXDTIxMDUyMTIyMjM0N1oYDzIwNjEwNTIxMjMyMzQ3WjCB + mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu + Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB + bWF6b24gUkRTIGV1LWNlbnRyYWwtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNV + BAcMB1NlYXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCTTYds + Tray+Q9VA5j5jTh5TunHKFQzn68ZbOzdqaoi/Rq4ohfC0xdLrxCpfqn2TGDHN6Zi + 2qGK1tWJZEd1H0trhzd9d1CtGK+3cjabUmz/TjSW/qBar7e9MA67/iJ74Gc+Ww43 + A0xPNIWcL4aLrHaLm7sHgAO2UCKsrBUpxErOAACERScVYwPAfu79xeFcX7DmcX+e + lIqY16pQAvK2RIzrekSYfLFxwFq2hnlgKHaVgZ3keKP+nmXcXmRSHQYUUr72oYNZ + HcNYl2+gxCc9ccPEHM7xncVEKmb5cWEWvVoaysgQ+osi5f5aQdzgC2X2g2daKbyA + XL/z5FM9GHpS5BJjAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE + FBDAiJ7Py9/A9etNa/ebOnx5l5MGMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0B + AQsFAAOCAQEALMh/+81fFPdJV/RrJUeoUvFCGMp8iaANu97NpeJyKitNOv7RoeVP + WjivS0KcCqZaDBs+p6IZ0sLI5ZH098LDzzytcfZg0PsGqUAb8a0MiU/LfgDCI9Ee + jsOiwaFB8k0tfUJK32NPcIoQYApTMT2e26lPzYORSkfuntme2PTHUnuC7ikiQrZk + P+SZjWgRuMcp09JfRXyAYWIuix4Gy0eZ4rpRuaTK6mjAb1/LYoNK/iZ/gTeIqrNt + l70OWRsWW8jEmSyNTIubGK/gGGyfuZGSyqoRX6OKHESkP6SSulbIZHyJ5VZkgtXo + 2XvyRyJ7w5pFyoofrL3Wv0UF8yt/GDszmg== + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIIGBDCCA+ygAwIBAgIQM4C8g5iFRucSWdC8EdqHeDANBgkqhkiG9w0BAQwFADCB + mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu + Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB + bWF6b24gUkRTIGV1LWNlbnRyYWwtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNV + BAcMB1NlYXR0bGUwIBcNMjEwNTIxMjIyODI2WhgPMjEyMTA1MjEyMzI4MjZaMIGa + MQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5j + LjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMzAxBgNVBAMMKkFt + YXpvbiBSRFMgZXUtY2VudHJhbC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE + BwwHU2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANeTsD/u + 6saPiY4Sg0GlJlMXMBltnrcGAEkwq34OKQ0bCXqcoNJ2rcAMmuFC5x9Ho1Y3YzB7 + NO2GpIh6bZaO76GzSv4cnimcv9n/sQSYXsGbPD+bAtnN/RvNW1avt4C0q0/ghgF1 + VFS8JihIrgPYIArAmDtGNEdl5PUrdi9y6QGggbRfidMDdxlRdZBe1C18ZdgERSEv + UgSTPRlVczONG5qcQkUGCH83MMqL5MKQiby/Br5ZyPq6rxQMwRnQ7tROuElzyYzL + 7d6kke+PNzG1mYy4cbYdjebwANCtZ2qYRSUHAQsOgybRcSoarv2xqcjO9cEsDiRU + l97ToadGYa4VVERuTaNZxQwrld4mvzpyKuirqZltOqg0eoy8VUsaRPL3dc5aChR0 + dSrBgRYmSAClcR2/2ZCWpXemikwgt031Dsc0A/+TmVurrsqszwbr0e5xqMow9LzO + MI/JtLd0VFtoOkL/7GG2tN8a+7gnLFxpv+AQ0DH5n4k/BY/IyS+H1erqSJhOTQ11 + vDOFTM5YplB9hWV9fp5PRs54ILlHTlZLpWGs3I2BrJwzRtg/rOlvsosqcge9ryai + AKm2j+JBg5wJ19R8oxRy8cfrNTftZePpISaLTyV2B16w/GsSjqixjTQe9LRN2DHk + cC+HPqYyzW2a3pUVyTGHhW6a7YsPBs9yzt6hAgMBAAGjQjBAMA8GA1UdEwEB/wQF + MAMBAf8wHQYDVR0OBBYEFIqA8QkOs2cSirOpCuKuOh9VDfJfMA4GA1UdDwEB/wQE + AwIBhjANBgkqhkiG9w0BAQwFAAOCAgEAOUI90mEIsa+vNJku0iUwdBMnHiO4gm7E + 5JloP7JG0xUr7d0hypDorMM3zVDAL+aZRHsq8n934Cywj7qEp1304UF6538ByGdz + tkfacJsUSYfdlNJE9KbA4T+U+7SNhj9jvePpVjdQbhgzxITE9f8CxY/eM40yluJJ + PhbaWvOiRagzo74wttlcDerzLT6Y/JrVpWhnB7IY8HvzK+BwAdaCsBUPC3HF+kth + CIqLq7J3YArTToejWZAp5OOI6DLPM1MEudyoejL02w0jq0CChmZ5i55ElEMnapRX + 7GQTARHmjgAOqa95FjbHEZzRPqZ72AtZAWKFcYFNk+grXSeWiDgPFOsq6mDg8DDB + 0kfbYwKLFFCC9YFmYzR2YrWw2NxAScccUc2chOWAoSNHiqBbHR8ofrlJSWrtmKqd + YRCXzn8wqXnTS3NNHNccqJ6dN+iMr9NGnytw8zwwSchiev53Fpc1mGrJ7BKTWH0t + ZrA6m32wzpMymtKozlOPYoE5mtZEzrzHEXfa44Rns7XIHxVQSXVWyBHLtIsZOrvW + U5F41rQaFEpEeUQ7sQvqUoISfTUVRNDn6GK6YaccEhCji14APLFIvhRQUDyYMIiM + 4vll0F/xgVRHTgDVQ8b8sxdhSYlqB4Wc2Ym41YRz+X2yPqk3typEZBpc4P5Tt1/N + 89cEIGdbjsA= + -----END CERTIFICATE----- + """ diff --git a/Examples/ServiceLifecycle+Postgres/Sources/RDSCertificates/us-east-1.swift b/Examples/ServiceLifecycle+Postgres/Sources/RDSCertificates/us-east-1.swift new file mode 100644 index 00000000..f68a6781 --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/Sources/RDSCertificates/us-east-1.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +let us_east_1_bundle_pem = """ + -----BEGIN CERTIFICATE----- + MIID/zCCAuegAwIBAgIRAPVSMfFitmM5PhmbaOFoGfUwDQYJKoZIhvcNAQELBQAw + gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ + bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn + QW1hem9uIFJEUyB1cy1lYXN0LTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH + DAdTZWF0dGxlMCAXDTIxMDUyNTIyMzQ1N1oYDzIwNjEwNTI1MjMzNDU3WjCBlzEL + MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x + EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 + b24gUkRTIHVzLWVhc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcMB1Nl + YXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDu9H7TBeGoDzMr + dxN6H8COntJX4IR6dbyhnj5qMD4xl/IWvp50lt0VpmMd+z2PNZzx8RazeGC5IniV + 5nrLg0AKWRQ2A/lGGXbUrGXCSe09brMQCxWBSIYe1WZZ1iU1IJ/6Bp4D2YEHpXrW + bPkOq5x3YPcsoitgm1Xh8ygz6vb7PsvJvPbvRMnkDg5IqEThapPjmKb8ZJWyEFEE + QRrkCIRueB1EqQtJw0fvP4PKDlCJAKBEs/y049FoOqYpT3pRy0WKqPhWve+hScMd + 6obq8kxTFy1IHACjHc51nrGII5Bt76/MpTWhnJIJrCnq1/Uc3Qs8IVeb+sLaFC8K + DI69Sw6bAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE7PCopt + lyOgtXX0Y1lObBUxuKaCMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC + AQEAFj+bX8gLmMNefr5jRJfHjrL3iuZCjf7YEZgn89pS4z8408mjj9z6Q5D1H7yS + jNETVV8QaJip1qyhh5gRzRaArgGAYvi2/r0zPsy+Tgf7v1KGL5Lh8NT8iCEGGXwF + g3Ir+Nl3e+9XUp0eyyzBIjHtjLBm6yy8rGk9p6OtFDQnKF5OxwbAgip42CD75r/q + p421maEDDvvRFR4D+99JZxgAYDBGqRRceUoe16qDzbMvlz0A9paCZFclxeftAxv6 + QlR5rItMz/XdzpBJUpYhdzM0gCzAzdQuVO5tjJxmXhkSMcDP+8Q+Uv6FA9k2VpUV + E/O5jgpqUJJ2Hc/5rs9VkAPXeA== + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIIF/jCCA+agAwIBAgIQaRHaEqqacXN20e8zZJtmDDANBgkqhkiG9w0BAQwFADCB + lzELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu + Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdB + bWF6b24gUkRTIHVzLWVhc3QtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcM + B1NlYXR0bGUwIBcNMjEwNTI1MjIzODM1WhgPMjEyMTA1MjUyMzM4MzVaMIGXMQsw + CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET + MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpv + biBSRFMgdXMtZWFzdC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwHU2Vh + dHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAInfBCaHuvj6Rb5c + L5Wmn1jv2PHtEGMHm+7Z8dYosdwouG8VG2A+BCYCZfij9lIGszrTXkY4O7vnXgru + JUNdxh0Q3M83p4X+bg+gODUs3jf+Z3Oeq7nTOk/2UYvQLcxP4FEXILxDInbQFcIx + yen1ESHggGrjEodgn6nbKQNRfIhjhW+TKYaewfsVWH7EF2pfj+cjbJ6njjgZ0/M9 + VZifJFBgat6XUTOf3jwHwkCBh7T6rDpgy19A61laImJCQhdTnHKvzTpxcxiLRh69 + ZObypR7W04OAUmFS88V7IotlPmCL8xf7kwxG+gQfvx31+A9IDMsiTqJ1Cc4fYEKg + bL+Vo+2Ii4W2esCTGVYmHm73drznfeKwL+kmIC/Bq+DrZ+veTqKFYwSkpHRyJCEe + U4Zym6POqQ/4LBSKwDUhWLJIlq99bjKX+hNTJykB+Lbcx0ScOP4IAZQoxmDxGWxN + S+lQj+Cx2pwU3S/7+OxlRndZAX/FKgk7xSMkg88HykUZaZ/ozIiqJqSnGpgXCtED + oQ4OJw5ozAr+/wudOawaMwUWQl5asD8fuy/hl5S1nv9XxIc842QJOtJFxhyeMIXt + LVECVw/dPekhMjS3Zo3wwRgYbnKG7YXXT5WMxJEnHu8+cYpMiRClzq2BEP6/MtI2 + AZQQUFu2yFjRGL2OZA6IYjxnXYiRAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8w + HQYDVR0OBBYEFADCcQCPX2HmkqQcmuHfiQ2jjqnrMA4GA1UdDwEB/wQEAwIBhjAN + BgkqhkiG9w0BAQwFAAOCAgEASXkGQ2eUmudIKPeOIF7RBryCoPmMOsqP0+1qxF8l + pGkwmrgNDGpmd9s0ArfIVBTc1jmpgB3oiRW9c6n2OmwBKL4UPuQ8O3KwSP0iD2sZ + KMXoMEyphCEzW1I2GRvYDugL3Z9MWrnHkoaoH2l8YyTYvszTvdgxBPpM2x4pSkp+ + 76d4/eRpJ5mVuQ93nC+YG0wXCxSq63hX4kyZgPxgCdAA+qgFfKIGyNqUIqWgeyTP + n5OgKaboYk2141Rf2hGMD3/hsGm0rrJh7g3C0ZirPws3eeJfulvAOIy2IZzqHUSY + jkFzraz6LEH3IlArT3jUPvWKqvh2lJWnnp56aqxBR7qHH5voD49UpJWY1K0BjGnS + OHcurpp0Yt/BIs4VZeWdCZwI7JaSeDcPMaMDBvND3Ia5Fga0thgYQTG6dE+N5fgF + z+hRaujXO2nb0LmddVyvE8prYlWRMuYFv+Co8hcMdJ0lEZlfVNu0jbm9/GmwAZ+l + 9umeYO9yz/uC7edC8XJBglMAKUmVK9wNtOckUWAcCfnPWYLbYa/PqtXBYcxrso5j + iaS/A7iEW51uteHBGrViCy1afGG+hiUWwFlesli+Rq4dNstX3h6h2baWABaAxEVJ + y1RnTQSz6mROT1VmZSgSVO37rgIyY0Hf0872ogcTS+FfvXgBxCxsNWEbiQ/XXva4 + 0Ws= + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIICrjCCAjSgAwIBAgIRAPAlEk8VJPmEzVRRaWvTh2AwCgYIKoZIzj0EAwMwgZYx + CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu + MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1h + em9uIFJEUyB1cy1lYXN0LTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl + YXR0bGUwIBcNMjEwNTI1MjI0MTU1WhgPMjEyMTA1MjUyMzQxNTVaMIGWMQswCQYD + VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG + A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExLzAtBgNVBAMMJkFtYXpvbiBS + RFMgdXMtZWFzdC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdTZWF0dGxl + MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEx5xjrup8II4HOJw15NTnS3H5yMrQGlbj + EDA5MMGnE9DmHp5dACIxmPXPMe/99nO7wNdl7G71OYPCgEvWm0FhdvVUeTb3LVnV + BnaXt32Ek7/oxGk1T+Df03C+W0vmuJ+wo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0G + A1UdDgQWBBTGXmqBWN/1tkSea4pNw0oHrjk2UDAOBgNVHQ8BAf8EBAMCAYYwCgYI + KoZIzj0EAwMDaAAwZQIxAIqqZWCSrIkZ7zsv/FygtAusW6yvlL935YAWYPVXU30m + jkMFLM+/RJ9GMvnO8jHfCgIwB+whlkcItzE9CRQ6CsMo/d5cEHDUu/QW6jSIh9BR + OGh9pTYPVkUbBiKPA7lVVhre + -----END CERTIFICATE----- + """ diff --git a/Examples/ServiceLifecycle+Postgres/Sources/Timeout.swift b/Examples/ServiceLifecycle+Postgres/Sources/Timeout.swift new file mode 100644 index 00000000..6a8dc5dc --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/Sources/Timeout.swift @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// as suggested by https://github.com/vapor/postgres-nio/issues/489#issuecomment-2186509773 +func timeout( + deadline: Duration, + _ closure: @escaping @Sendable () async throws -> Success +) async throws -> Success { + + let clock = ContinuousClock() + + let result = await withTaskGroup(of: TimeoutResult.self, returning: Result.self) { + taskGroup in + taskGroup.addTask { + do { + try await clock.sleep(until: clock.now + deadline, tolerance: nil) + return .deadlineHit + } catch { + return .deadlineCancelled + } + } + + taskGroup.addTask { + do { + let success = try await closure() + return .workFinished(.success(success)) + } catch let error { + return .workFinished(.failure(error)) + } + } + + var r: Swift.Result? + while let taskResult = await taskGroup.next() { + switch taskResult { + case .deadlineCancelled: + continue // loop + + case .deadlineHit: + taskGroup.cancelAll() + + case .workFinished(let result): + taskGroup.cancelAll() + r = result + } + } + return r! + } + + return try result.get() +} + +enum TimeoutResult { + case deadlineHit + case deadlineCancelled + case workFinished(Result) +} diff --git a/Examples/ServiceLifecycle+Postgres/deploy.sh b/Examples/ServiceLifecycle+Postgres/deploy.sh new file mode 100755 index 00000000..262cbf65 --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/deploy.sh @@ -0,0 +1,36 @@ +#!/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 +## +##===----------------------------------------------------------------------===## + +# ServiceLifecycle Lambda Deployment Script +set -e + +echo "🚀 Building and deploying ServiceLifecycle Lambda with PostgreSQL..." + +# Build the Lambda function +echo "📦 Building Swift Lambda function..." +swift package --disable-sandbox archive --allow-network-connections docker + +# Deploy with SAM +echo "🌩️ Deploying with SAM..." +sam deploy + +echo "✅ Deployment complete!" +echo "" +echo "📋 To get the database connection details, run:" +echo "aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs'" +echo "" +echo "🧪 To test the Lambda function:" +# shellcheck disable=SC2006,SC2016 +echo "curl $(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`APIGatewayEndpoint`].OutputValue' --output text)" diff --git a/Examples/ServiceLifecycle+Postgres/events/sample-request.json b/Examples/ServiceLifecycle+Postgres/events/sample-request.json new file mode 100644 index 00000000..e75466fd --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/events/sample-request.json @@ -0,0 +1,49 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/", + "rawQueryString": "", + "body": "", + "headers": { + "x-amzn-tls-cipher-suite": "TLS_AES_128_GCM_SHA256", + "x-amzn-tls-version": "TLSv1.3", + "x-amzn-trace-id": "Root=1-68762f44-4f6a87d1639e7fc356aa6f96", + "x-amz-date": "20250715T103651Z", + "x-forwarded-proto": "https", + "host": "zvnsvhpx7u5gn3l3euimg4jjou0jvbfe.lambda-url.us-east-1.on.aws", + "x-forwarded-port": "443", + "x-forwarded-for": "2a01:...:b9f", + "accept": "*/*", + "user-agent": "curl/8.7.1" + }, + "requestContext": { + "accountId": "0123456789", + "apiId": "zvnsvhpx7u5gn3l3euimg4jjou0jvbfe", + "authorizer": { + "iam": { + "accessKey": "AKIA....", + "accountId": "0123456789", + "callerId": "AIDA...", + "cognitoIdentity": null, + "principalOrgId": "o-rlrup7z3ao", + "userArn": "arn:aws:iam::0123456789:user/sst", + "userId": "AIDA..." + } + }, + "domainName": "zvnsvhpx7u5gn3l3euimg4jjou0jvbfe.lambda-url.us-east-1.on.aws", + "domainPrefix": "zvnsvhpx7u5gn3l3euimg4jjou0jvbfe", + "http": { + "method": "GET", + "path": "/", + "protocol": "HTTP/1.1", + "sourceIp": "2a01:...:b9f", + "userAgent": "curl/8.7.1" + }, + "requestId": "f942509a-283f-4c4f-94f8-0d4ccc4a00f8", + "routeKey": "$default", + "stage": "$default", + "time": "15/Jul/2025:10:36:52 +0000", + "timeEpoch": 1752575812081 + }, + "isBase64Encoded": false +} \ No newline at end of file diff --git a/Examples/ServiceLifecycle+Postgres/localdb.sh b/Examples/ServiceLifecycle+Postgres/localdb.sh new file mode 100644 index 00000000..db615b4d --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/localdb.sh @@ -0,0 +1,45 @@ +#!/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 +## +##===----------------------------------------------------------------------===## + +# For testing purposes, this script sets up a local PostgreSQL database using Docker. + +# Create a named volume for PostgreSQL data +docker volume create pgdata + +# Run PostgreSQL container with the volume mounted +docker run -d \ + --name postgres-db \ + -e POSTGRES_PASSWORD=secret \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_DB=test \ + -p 5432:5432 \ + -v pgdata:/var/lib/postgresql/data \ + postgres:latest + +# Stop the container +docker stop postgres-db + +# Start it again (data persists) +docker start postgres-db + +# Connect to the database using psql in a new container +docker run -it --rm --network host \ + -e PGPASSWORD=secret \ + postgres:latest \ + psql -h localhost -U postgres -d servicelifecycle + +# Alternative: Connect using the postgres-db container itself +docker exec -it postgres-db psql -U postgres -d servicelifecycle + diff --git a/Examples/ServiceLifecycle+Postgres/samconfig.toml b/Examples/ServiceLifecycle+Postgres/samconfig.toml new file mode 100644 index 00000000..4171fc12 --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/samconfig.toml @@ -0,0 +1,29 @@ +# SAM configuration file for ServiceLifecycle example +version = 0.1 + +[default.global.parameters] +stack_name = "servicelifecycle-stack" + +[default.build.parameters] +cached = true +parallel = true + +[default.deploy.parameters] +capabilities = "CAPABILITY_IAM" +confirm_changeset = true +resolve_s3 = true +s3_prefix = "servicelifecycle" +region = "us-east-1" +image_repositories = [] + +[default.package.parameters] +resolve_s3 = true + +[default.sync.parameters] +watch = true + +[default.local_start_api.parameters] +warm_containers = "EAGER" + +[default.local_start_lambda.parameters] +warm_containers = "EAGER" diff --git a/Examples/ServiceLifecycle+Postgres/template.yaml b/Examples/ServiceLifecycle+Postgres/template.yaml new file mode 100644 index 00000000..9d34b14b --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/template.yaml @@ -0,0 +1,229 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for ServiceLifecycle Lambda with PostgreSQL RDS + +# This is an example SAM template for the purpose of this project. +# When deploying such infrastructure in production environment, +# we strongly encourage you to follow these best practices for improved security and resiliency +# - Enable access loggin on API Gateway +# See: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html) +# - Ensure that AWS Lambda function is configured for function-level concurrent execution limit +# See: https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html +# https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html +# - Check encryption settings for Lambda environment variable +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html +# - Ensure that AWS Lambda function is configured for a Dead Letter Queue(DLQ) +# See: https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq + +Parameters: + + DBName: + Type: String + Default: servicelifecycle + Description: Database name + MinLength: "1" + MaxLength: "64" + AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*' + ConstraintDescription: Must begin with a letter and contain only alphanumeric characters + +Resources: + # VPC for RDS and Lambda + VPC: + Type: AWS::EC2::VPC + Properties: + CidrBlock: 10.0.0.0/16 + EnableDnsHostnames: true + EnableDnsSupport: true + Tags: + - Key: Name + Value: ServiceLifecycle-VPC + + # Private Subnet 1 for RDS + PrivateSubnet1: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + AvailabilityZone: !Select [0, !GetAZs ''] + CidrBlock: 10.0.3.0/24 + MapPublicIpOnLaunch: false + Tags: + - Key: Name + Value: ServiceLifecycle-Private-Subnet-1 + + # Private Subnet 2 for RDS + PrivateSubnet2: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + AvailabilityZone: !Select [1, !GetAZs ''] + CidrBlock: 10.0.4.0/24 + MapPublicIpOnLaunch: false + Tags: + - Key: Name + Value: ServiceLifecycle-Private-Subnet-2 + + # Security Group for RDS + DatabaseSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupName: ServiceLifecycle-DB-SG + GroupDescription: Security group for PostgreSQL database + VpcId: !Ref VPC + Tags: + - Key: Name + Value: ServiceLifecycle-DB-SecurityGroup + + # Security Group for Lambda + LambdaSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupName: ServiceLifecycle-Lambda-SG + GroupDescription: Security group for Lambda function + VpcId: !Ref VPC + SecurityGroupEgress: + - IpProtocol: tcp + FromPort: 5432 + ToPort: 5432 + CidrIp: 10.0.0.0/16 + Description: Allow PostgreSQL access within VPC only + Tags: + - Key: Name + Value: ServiceLifecycle-Lambda-SecurityGroup + + # DB Subnet Group (required for RDS) + DatabaseSubnetGroup: + Type: AWS::RDS::DBSubnetGroup + Properties: + DBSubnetGroupDescription: Subnet group for PostgreSQL database + SubnetIds: + - !Ref PrivateSubnet1 + - !Ref PrivateSubnet2 + Tags: + - Key: Name + Value: ServiceLifecycle-DB-SubnetGroup + + # Database credentials stored in Secrets Manager + DatabaseSecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Sub "${AWS::StackName}-db-credentials" + Description: RDS database credentials + GenerateSecretString: + SecretStringTemplate: '{"username":"postgres"}' + GenerateStringKey: "password" + PasswordLength: 16 + ExcludeCharacters: '"@/\\' + + # Database Security Group Ingress Rule (added separately to avoid circular dependency) + DatabaseSecurityGroupIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref DatabaseSecurityGroup + IpProtocol: tcp + FromPort: 5432 + ToPort: 5432 + SourceSecurityGroupId: !Ref LambdaSecurityGroup + Description: Allow PostgreSQL access from Lambda security group + + # PostgreSQL RDS Instance + PostgreSQLDatabase: + Type: AWS::RDS::DBInstance + DeletionPolicy: Delete + Properties: + DBInstanceIdentifier: servicelifecycle-postgres + DBInstanceClass: db.t3.micro + Engine: postgres + EngineVersion: '17.6' + MasterUsername: !Join ['', ['{{resolve:secretsmanager:', !Ref DatabaseSecret, ':SecretString:username}}']] + MasterUserPassword: !Join ['', ['{{resolve:secretsmanager:', !Ref DatabaseSecret, ':SecretString:password}}']] + DBName: !Ref DBName + AllocatedStorage: "20" + StorageType: gp2 + VPCSecurityGroups: + - !Ref DatabaseSecurityGroup + DBSubnetGroupName: !Ref DatabaseSubnetGroup + PubliclyAccessible: false + BackupRetentionPeriod: 0 + MultiAZ: false + StorageEncrypted: true + DeletionProtection: false + Tags: + - Key: Name + Value: ServiceLifecycle-PostgreSQL + + # Lambda function + ServiceLifecycleLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/LambdaWithServiceLifecycle/LambdaWithServiceLifecycle.zip + Timeout: 60 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 128 + Architectures: + - arm64 + VpcConfig: + SecurityGroupIds: + - !Ref LambdaSecurityGroup + SubnetIds: + - !Ref PrivateSubnet1 + - !Ref PrivateSubnet2 + Environment: + Variables: + LOG_LEVEL: trace + DB_HOST: !GetAtt PostgreSQLDatabase.Endpoint.Address + DB_USER: !Join ['', ['{{resolve:secretsmanager:', !Ref DatabaseSecret, ':SecretString:username}}']] + DB_PASSWORD: !Join ['', ['{{resolve:secretsmanager:', !Ref DatabaseSecret, ':SecretString:password}}']] + DB_NAME: !Ref DBName + Events: + HttpApiEvent: + Type: HttpApi + +Outputs: + # API Gateway endpoint + APIGatewayEndpoint: + Description: API Gateway endpoint URL for the Lambda function + Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" + Export: + Name: !Sub "${AWS::StackName}-APIEndpoint" + + # Database connection details + DatabaseEndpoint: + Description: PostgreSQL database endpoint hostname + Value: !GetAtt PostgreSQLDatabase.Endpoint.Address + Export: + Name: !Sub "${AWS::StackName}-DBEndpoint" + + DatabasePort: + Description: PostgreSQL database port + Value: !GetAtt PostgreSQLDatabase.Endpoint.Port + Export: + Name: !Sub "${AWS::StackName}-DBPort" + + DatabaseName: + Description: PostgreSQL database name + Value: !Ref DBName + Export: + Name: !Sub "${AWS::StackName}-DBName" + + DatabaseSecretArn: + Description: ARN of the secret containing database credentials + Value: !Ref DatabaseSecret + Export: + Name: !Sub "${AWS::StackName}-DBSecretArn" + + # Connection string instructions + DatabaseConnectionInstructions: + Description: Instructions to get the connection string + Value: !Sub "Use 'aws secretsmanager get-secret-value --secret-id ${DatabaseSecret}' to retrieve credentials" + Export: + Name: !Sub "${AWS::StackName}-DBConnectionInstructions" + + # Individual connection details for manual connection + ConnectionDetails: + Description: Database connection details + Value: !Sub | + Hostname: ${PostgreSQLDatabase.Endpoint.Address} + Port: ${PostgreSQLDatabase.Endpoint.Port} + Database: ${DBName} + Credentials: Use AWS Secrets Manager to retrieve username and password diff --git a/Examples/Streaming+Codable/Package.swift b/Examples/Streaming+Codable/Package.swift new file mode 100644 index 00000000..259499c9 --- /dev/null +++ b/Examples/Streaming+Codable/Package.swift @@ -0,0 +1,59 @@ +// swift-tools-version: 6.2 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "StreamingCodable", + platforms: [.macOS(.v15)], + 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", from: "2.0.0"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.2.0"), + ], + targets: [ + .executableTarget( + name: "StreamingCodable", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ] + ), + .testTarget( + name: "Streaming+CodableTests", + dependencies: [ + "StreamingCodable", + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + ] + ), + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} diff --git a/Examples/Streaming+Codable/README.md b/Examples/Streaming+Codable/README.md new file mode 100644 index 00000000..e98c9897 --- /dev/null +++ b/Examples/Streaming+Codable/README.md @@ -0,0 +1,300 @@ +# Streaming Codable Lambda function + +This example demonstrates how to use a `StreamingLambdaHandlerWithEvent` protocol to create Lambda functions, exposed through a FunctionUrl, that: + +1. **Receive JSON input**: Automatically decode JSON events into Swift structs +2. **Stream responses**: Send data incrementally as it becomes available +3. **Execute background work**: Perform additional processing after the response is sent + +## When to Use This Approach + +**⚠️ Important Limitations:** + +1. **Function URL Only**: This streaming codable approach only works with Lambda functions exposed through [Lambda Function URLs](https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html) +2. **Limited Request Access**: This approach hides the details of the `FunctionURLRequest` (like HTTP headers, query parameters, etc.) from developers + +**Decision Rule:** + +- **Use this streaming codable approach when:** + - Your function is exposed through a Lambda Function URL + - You have a JSON payload that you want automatically decoded + - You don't need to inspect HTTP headers, query parameters, or other request details + - You prioritize convenience over flexibility + +- **Use the ByteBuffer `StreamingLambdaHandler` approach when:** + - You need full control over the `FunctionURLRequest` details + - You're invoking the Lambda through other means (API Gateway, direct invocation, etc.) + - You need access to HTTP headers, query parameters, or request metadata + - You require maximum flexibility (requires writing more code) + +This example balances convenience and flexibility. The streaming codable interface combines the benefits of: +- Type-safe JSON input decoding (like regular `LambdaHandler`) +- Response streaming capabilities (like `StreamingLambdaHandler`) +- Background work execution after response completion + +Streaming responses incurs a cost. For more information, see [AWS Lambda Pricing](https://aws.amazon.com/lambda/pricing/). + +You can stream responses through [Lambda function URLs](https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html), the AWS SDK, or using the Lambda [InvokeWithResponseStream](https://docs.aws.amazon.com/lambda/latest/dg/API_InvokeWithResponseStream.html) API. + +## Code + +The sample code creates a `StreamingFromEventHandler` struct that conforms to the `StreamingLambdaHandlerWithEvent` protocol provided by the Swift AWS Lambda Runtime. + +The `handle(...)` method of this protocol receives incoming events as a decoded Swift struct (`StreamingRequest`) and returns the output through a `LambdaResponseStreamWriter`. + +The Lambda function expects a JSON payload with the following structure: + +```json +{ + "count": 5, + "message": "Hello from streaming Lambda!", + "delayMs": 1000 +} +``` + +Where: +- `count`: Number of messages to stream (1-100) +- `message`: The message content to repeat +- `delayMs`: Optional delay between messages in milliseconds (defaults to 500ms) + +The response is streamed through the `LambdaResponseStreamWriter`, which is passed as an argument in the `handle` function. The code calls the `write(_:)` function of the `LambdaResponseStreamWriter` with partial data written repeatedly before finally closing the response stream by calling `finish()`. Developers can also choose to return the entire output and not stream the response by calling `writeAndFinish(_:)`. + +An error is thrown if `finish()` is called multiple times or if it is called after having called `writeAndFinish(_:)`. + +The `handle(...)` method is marked as `mutating` to allow handlers to be implemented with a `struct`. + +Once the struct is created and the `handle(...)` method is defined, the sample code creates a `LambdaRuntime` struct and initializes it with the handler just created. Then, the code calls `run()` to start the interaction with the AWS Lambda control plane. + +Key features demonstrated: +- **JSON Input Decoding**: The function automatically parses the JSON input into a `StreamingRequest` struct +- **Input Validation**: Validates the count parameter and returns an error message if invalid +- **Progressive Streaming**: Sends messages one by one with configurable delays +- **Timestamped Output**: Each message includes an ISO8601 timestamp +- **Background Processing**: Performs cleanup and logging after the response is complete +- **Error Handling**: Gracefully handles invalid input with descriptive error messages + +## Build & Package + +To build & archive the package, type the following commands. + +```bash +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingFromEvent/StreamingFromEvent.zip` + +## Test locally + +You can test the function locally before deploying: + +```bash +swift run + +# In another terminal, test with curl: +curl -v \ + --header "Content-Type: application/json" \ + --data '{"count": 3, "message": "Hello World!", "delayMs": 1000}' \ + http://127.0.0.1:7000/invoke +``` + +Or simulate a call from a Lambda Function URL (where the body is encapsulated in a Lambda Function URL request): + +```bash +curl -v \ + --header "Content-Type: application/json" \ + --data @events/sample-request.json \ + http://127.0.0.1:7000/invoke + ``` + +## Deploy with the AWS CLI + +Here is how to deploy using the `aws` command line. + +### Step 1: Create the function + +```bash +# Replace with your AWS Account ID +AWS_ACCOUNT_ID=012345678901 +aws lambda create-function \ +--function-name StreamingFromEvent \ +--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingFromEvent/StreamingFromEvent.zip \ +--runtime provided.al2 \ +--handler provided \ +--architectures arm64 \ +--role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution +``` + +> [!IMPORTANT] +> The timeout value must be bigger than the time it takes for your function to stream its output. Otherwise, the Lambda control plane will terminate the execution environment before your code has a chance to finish writing the stream. Here, the sample function stream responses during 10 seconds and we set the timeout for 15 seconds. + +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 set `AWS_ACCOUNT_ID` with your actual AWS account ID (for example: 012345678901). + +### Step 2: Give permission to invoke that function through a URL + +Anyone with a valid signature from your AWS account will have permission to invoke the function through its URL. + +```bash +aws lambda add-permission \ + --function-name StreamingFromEvent \ + --action lambda:InvokeFunctionUrl \ + --principal ${AWS_ACCOUNT_ID} \ + --function-url-auth-type AWS_IAM \ + --statement-id allowURL +``` + +### Step 3: Create the URL + +This creates [a URL with IAM authentication](https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html). Only calls with a valid signature will be authorized. + +```bash +aws lambda create-function-url-config \ + --function-name StreamingFromEvent \ + --auth-type AWS_IAM \ + --invoke-mode RESPONSE_STREAM +``` +This call returns various information, including the URL to invoke your function. + +```json +{ + "FunctionUrl": "https://ul3nf4dogmgyr7ffl5r5rs22640fwocc.lambda-url.us-east-1.on.aws/", + "FunctionArn": "arn:aws:lambda:us-east-1:012345678901:function:StreamingFromEvent", + "AuthType": "AWS_IAM", + "CreationTime": "2024-10-22T07:57:23.112599Z", + "InvokeMode": "RESPONSE_STREAM" +} +``` + +### Invoke your Lambda function + +To invoke the Lambda function, use `curl` with the AWS Sigv4 option to generate the signature. + +Read the [AWS Credentials and Signature](../README.md/#AWS-Credentials-and-Signature) section for more details about the AWS Sigv4 protocol and how to obtain AWS credentials. + +When you have the `aws` command line installed and configured, you will find the credentials in the `~/.aws/credentials` file. + +```bash +URL=https://ul3nf4dogmgyr7ffl5r5rs22640fwocc.lambda-url.us-east-1.on.aws/ +REGION=us-east-1 +ACCESS_KEY=AK... +SECRET_KEY=... +AWS_SESSION_TOKEN=... + +curl --user "${ACCESS_KEY}":"${SECRET_KEY}" \ + --aws-sigv4 "aws:amz:${REGION}:lambda" \ + -H "x-amz-security-token: ${AWS_SESSION_TOKEN}" \ + --no-buffer \ + --header "Content-Type: application/json" \ + --data '{"count": 3, "message": "Hello World!", "delayMs": 1000}' \ + "$URL" +``` + +This should output the following result, with configurable delays between each message: + +``` +[2024-07-15T05:00:00Z] Message 1/3: Hello World! +[2024-07-15T05:00:01Z] Message 2/3: Hello World! +[2024-07-15T05:00:02Z] Message 3/3: Hello World! +✅ Successfully sent 3 messages +``` + +### Undeploy + +When done testing, you can delete the Lambda function with this command. + +```bash +aws lambda delete-function --function-name StreamingFromEvent +``` + +## Deploy with AWS SAM + +Alternatively, you can use [AWS SAM](https://aws.amazon.com/serverless/sam/) to deploy the Lambda function. + +**Prerequisites** : Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) + +### SAM Template + +The template file is provided as part of the example in the `template.yaml` file. It defines a Lambda function based on the binary ZIP file. It creates the function url with IAM authentication and sets the function timeout to 15 seconds. + +```yaml +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for StreamingFromEvent Example + +Resources: + # Lambda function + StreamingNumbers: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingFromEvent/StreamingFromEvent.zip + Timeout: 15 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 128 + Architectures: + - arm64 + FunctionUrlConfig: + AuthType: AWS_IAM + InvokeMode: RESPONSE_STREAM + +Outputs: + # print Lambda function URL + LambdaURL: + Description: Lambda URL + Value: !GetAtt StreamingNumbersUrl.FunctionUrl +``` + +### Deploy with SAM + +```bash +sam deploy \ +--resolve-s3 \ +--template-file template.yaml \ +--stack-name StreamingFromEvent \ +--capabilities CAPABILITY_IAM +``` + +The URL of the function is provided as part of the output. + +``` +CloudFormation outputs from deployed stack +----------------------------------------------------------------------------------------------------------------------------- +Outputs +----------------------------------------------------------------------------------------------------------------------------- +Key LambdaURL +Description Lambda URL +Value https://gaudpin2zjqizfujfnqxstnv6u0czrfu.lambda-url.us-east-1.on.aws/ +----------------------------------------------------------------------------------------------------------------------------- +``` + +Once the function is deployed, you can invoke it with `curl`, similarly to what you did when deploying with the AWS CLI. + +```bash +curl -X POST \ + --data '{"count": 3, "message": "Hello World!", "delayMs": 1000}' \ + --user "$ACCESS_KEY":"$SECRET_KEY" \ + --aws-sigv4 "aws:amz:${REGION}:lambda" \ + -H "x-amz-security-token: $AWS_SESSION_TOKEN" \ + --no-buffer \ + "$URL" +``` + +### Undeploy with SAM + +When done testing, you can delete the infrastructure with this command. + +```bash +sam delete +``` + +## ⚠️ Security and Reliability Notice + +These are example applications for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: + +- Enable access logging on API Gateway ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)) +- Ensure that AWS Lambda function is configured for function-level concurrent execution limit ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +- Check encryption settings for Lambda environment variables ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +- Ensure that AWS Lambda function is configured for a Dead Letter Queue (DLQ) ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +- Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html), [code example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres)) \ No newline at end of file diff --git a/Examples/Streaming+Codable/Sources/LambdaStreaming+Codable.swift b/Examples/Streaming+Codable/Sources/LambdaStreaming+Codable.swift new file mode 100644 index 00000000..4b447fcb --- /dev/null +++ b/Examples/Streaming+Codable/Sources/LambdaStreaming+Codable.swift @@ -0,0 +1,185 @@ +//===----------------------------------------------------------------------===// +// +// 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 Logging +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +/// A streaming handler protocol that receives a decoded JSON event and can stream responses. +/// This handler protocol supports response streaming and background work execution. +/// Background work can be executed after closing the response stream by calling +/// ``LambdaResponseStreamWriter/finish()`` or ``LambdaResponseStreamWriter/writeAndFinish(_:)``. +public protocol StreamingLambdaHandlerWithEvent: _Lambda_SendableMetatype { + /// Generic input type that will be decoded from JSON. + associatedtype Event: Decodable + + /// The handler function that receives a decoded event and can stream responses. + /// - Parameters: + /// - event: The decoded event object. + /// - responseWriter: A ``LambdaResponseStreamWriter`` to write the invocation's response to. + /// If no response or error is written to `responseWriter` an error will be reported to the invoker. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + /// - Throws: + /// How the thrown error will be handled by the runtime: + /// - An invocation error will be reported if the error is thrown before the first call to + /// ``LambdaResponseStreamWriter/write(_:)``. + /// - If the error is thrown after call(s) to ``LambdaResponseStreamWriter/write(_:)`` but before + /// a call to ``LambdaResponseStreamWriter/finish()``, the response stream will be closed and trailing + /// headers will be sent. + /// - If ``LambdaResponseStreamWriter/finish()`` has already been called before the error is thrown, the + /// error will be logged. + mutating func handle( + _ event: Event, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws +} + +/// Adapts a ``StreamingLambdaHandlerWithEvent`` to work as a ``StreamingLambdaHandler`` +/// by handling JSON decoding of the input event. +public struct StreamingLambdaCodableAdapter< + Handler: StreamingLambdaHandlerWithEvent, + Decoder: LambdaEventDecoder +>: StreamingLambdaHandler where Handler.Event: Decodable { + @usableFromInline var handler: Handler + @usableFromInline let decoder: Decoder + + /// Initialize with a custom decoder and handler. + /// - Parameters: + /// - decoder: The decoder to use for parsing the input event. + /// - handler: The streaming handler that works with decoded events. + @inlinable + public init(decoder: sending Decoder, handler: sending Handler) { + self.decoder = decoder + self.handler = handler + } + + /// Handles the raw ByteBuffer by decoding it and passing to the underlying handler. + /// This function attempts to decode the event as a `FunctionURLRequest` first, which allows for + /// handling Function URL requests that may have a base64-encoded body. + /// If the decoding fails, it falls back to decoding the event "as-is" with the provided JSON type. + /// - Parameters: + /// - event: The raw ByteBuffer event to decode. + /// - responseWriter: The response writer to pass to the underlying handler. + /// - context: The Lambda context. + @inlinable + public mutating func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + + var decodedBody: Handler.Event! + + // try to decode the event as a FunctionURLRequest, then fetch its body attribute + if let request = try? self.decoder.decode(FunctionURLRequest.self, from: event) { + // decode the body as user-provided JSON type + // this function handles the base64 decoding when needed + decodedBody = try request.decodeBody(Handler.Event.self) + } else { + // try to decode the event "as-is" with the provided JSON type + decodedBody = try self.decoder.decode(Handler.Event.self, from: event) + } + + // and pass it to the handler + try await self.handler.handle(decodedBody, responseWriter: responseWriter, context: context) + } +} + +/// A closure-based streaming handler that works with decoded JSON events. +/// Allows for a streaming handler to be defined in a clean manner, leveraging Swift's trailing closure syntax. +public struct StreamingFromEventClosureHandler: StreamingLambdaHandlerWithEvent { + let body: @Sendable (Event, LambdaResponseStreamWriter, LambdaContext) async throws -> Void + + /// Initialize with a closure that receives a decoded event. + /// - Parameter body: The handler closure that receives a decoded event, response writer, and context. + public init( + body: @Sendable @escaping (Event, LambdaResponseStreamWriter, LambdaContext) async throws -> Void + ) { + self.body = body + } + + /// Calls the provided closure with the decoded event. + /// - Parameters: + /// - event: The decoded event object. + /// - responseWriter: The response writer for streaming output. + /// - context: The Lambda context. + public func handle( + _ event: Event, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + try await self.body(event, responseWriter, context) + } +} + +extension StreamingLambdaCodableAdapter { + /// Initialize with a JSON decoder and handler. + /// - Parameters: + /// - decoder: The JSON decoder to use. Defaults to `JSONDecoder()`. + /// - handler: The streaming handler that works with decoded events. + public init( + decoder: JSONDecoder = JSONDecoder(), + handler: sending Handler + ) where Decoder == LambdaJSONEventDecoder { + self.init(decoder: LambdaJSONEventDecoder(decoder), handler: handler) + } +} + +extension LambdaRuntime { + /// Initialize with a streaming handler that receives decoded JSON events. + /// - Parameters: + /// - decoder: The JSON decoder to use. Defaults to `JSONDecoder()`. + /// - logger: The logger to use. Defaults to a logger with label "LambdaRuntime". + /// - streamingBody: The handler closure that receives a decoded event. + public convenience init( + decoder: JSONDecoder = JSONDecoder(), + logger: Logger = Logger(label: "LambdaRuntime"), + streamingBody: @Sendable @escaping (Event, LambdaResponseStreamWriter, LambdaContext) async throws -> Void + ) + where + Handler == StreamingLambdaCodableAdapter< + StreamingFromEventClosureHandler, + LambdaJSONEventDecoder + > + { + let closureHandler = StreamingFromEventClosureHandler(body: streamingBody) + let adapter = StreamingLambdaCodableAdapter( + decoder: decoder, + handler: closureHandler + ) + self.init(handler: adapter, logger: logger) + } + + /// Initialize with a custom streaming handler that receives decoded events. + /// - Parameters: + /// - decoder: The decoder to use for parsing input events. + /// - handler: The streaming handler. + /// - logger: The logger to use. + public convenience init( + decoder: sending Decoder, + handler: sending StreamingHandler, + logger: Logger = Logger(label: "LambdaRuntime") + ) where Handler == StreamingLambdaCodableAdapter { + let adapter = StreamingLambdaCodableAdapter(decoder: decoder, handler: handler) + self.init(handler: adapter, logger: logger) + } +} diff --git a/Examples/Streaming+Codable/Sources/main.swift b/Examples/Streaming+Codable/Sources/main.swift new file mode 100644 index 00000000..bf559dd8 --- /dev/null +++ b/Examples/Streaming+Codable/Sources/main.swift @@ -0,0 +1,71 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// Define your input event structure +struct StreamingRequest: Decodable { + let count: Int + let message: String + let delayMs: Int? + + // Provide default values for optional fields + var delay: Int { + delayMs ?? 500 + } +} + +// Use the new streaming handler with JSON decoding +let runtime = LambdaRuntime { (event: StreamingRequest, responseWriter, context: LambdaContext) in + context.logger.info("Received request to send \(event.count) messages: '\(event.message)'") + + // Validate input + guard event.count > 0 && event.count <= 100 else { + let errorMessage = "Count must be between 1 and 100, got: \(event.count)" + context.logger.error("\(errorMessage)") + try await responseWriter.writeAndFinish(ByteBuffer(string: "Error: \(errorMessage)\n")) + return + } + + // Stream the messages + for i in 1...event.count { + let response = "[\(Date().ISO8601Format())] Message \(i)/\(event.count): \(event.message)\n" + try await responseWriter.write(ByteBuffer(string: response)) + + // Optional delay between messages + if event.delay > 0 { + try await Task.sleep(for: .milliseconds(event.delay)) + } + } + + // Send completion message and finish the stream + let completionMessage = "✅ Successfully sent \(event.count) messages\n" + try await responseWriter.writeAndFinish(ByteBuffer(string: completionMessage)) + + // Optional: Do background work here after response is sent + context.logger.info("Background work: cleaning up resources and logging metrics") + + // Simulate some background processing + try await Task.sleep(for: .milliseconds(100)) + context.logger.info("Background work completed") +} + +try await runtime.run() diff --git a/Examples/Streaming+Codable/Tests/LambdaStreamingCodableTests.swift b/Examples/Streaming+Codable/Tests/LambdaStreamingCodableTests.swift new file mode 100644 index 00000000..b95388aa --- /dev/null +++ b/Examples/Streaming+Codable/Tests/LambdaStreamingCodableTests.swift @@ -0,0 +1,341 @@ +//===----------------------------------------------------------------------===// +// +// 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 NIOCore +import Synchronization +import Testing + +@testable import AWSLambdaRuntime +@testable import StreamingCodable + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@Suite("Streaming Codable Lambda Handler Tests") +struct LambdaStreamingFromEventTests { + + // MARK: - Test Data Structures + + struct TestEvent: Decodable, Equatable { + let message: String + let count: Int + let delay: Int? + } + + struct SimpleEvent: Decodable, Equatable { + let value: String + } + + // MARK: - Mock Response Writer + + actor MockResponseWriter: LambdaResponseStreamWriter { + private var writtenBuffers: [ByteBuffer] = [] + private var isFinished = false + private var writeAndFinishCalled = false + + func write(_ buffer: ByteBuffer, hasCustomHeaders: Bool = false) async throws { + guard !isFinished else { + throw MockError.writeAfterFinish + } + writtenBuffers.append(buffer) + } + + func finish() async throws { + guard !isFinished else { + throw MockError.alreadyFinished + } + isFinished = true + } + + func writeAndFinish(_ buffer: ByteBuffer) async throws { + try await write(buffer) + try await finish() + writeAndFinishCalled = true + } + + // Test helpers + func getWrittenData() -> [String] { + writtenBuffers.compactMap { buffer in + buffer.getString(at: buffer.readerIndex, length: buffer.readableBytes) + } + } + + func getFinished() -> Bool { + isFinished + } + + func getWriteAndFinishCalled() -> Bool { + writeAndFinishCalled + } + } + + enum MockError: Error { + case writeAfterFinish + case alreadyFinished + case decodingFailed + case handlerError + } + + // MARK: - Test StreamingFromEventClosureHandler + + @Test("StreamingFromEventClosureHandler handles decoded events correctly") + func testStreamingFromEventClosureHandler() async throws { + let responseWriter = MockResponseWriter() + let context = LambdaContext.makeTest() + + let handler = StreamingFromEventClosureHandler { event, writer, context in + let message = "Received: \(event.message) (count: \(event.count))" + try await writer.writeAndFinish(ByteBuffer(string: message)) + } + + let testEvent = TestEvent(message: "Hello", count: 42, delay: nil) + + try await handler.handle(testEvent, responseWriter: responseWriter, context: context) + + let writtenData = await responseWriter.getWrittenData() + let isFinished = await responseWriter.getFinished() + + #expect(writtenData == ["Received: Hello (count: 42)"]) + #expect(isFinished == true) + } + + @Test("StreamingFromEventClosureHandler can stream multiple responses") + func testStreamingMultipleResponses() async throws { + let responseWriter = MockResponseWriter() + let context = LambdaContext.makeTest() + + let handler = StreamingFromEventClosureHandler { event, writer, context in + for i in 1...event.count { + try await writer.write(ByteBuffer(string: "\(i): \(event.message)\n")) + } + try await writer.finish() + } + + let testEvent = TestEvent(message: "Test", count: 3, delay: nil) + + try await handler.handle(testEvent, responseWriter: responseWriter, context: context) + + let writtenData = await responseWriter.getWrittenData() + let isFinished = await responseWriter.getFinished() + + #expect(writtenData == ["1: Test\n", "2: Test\n", "3: Test\n"]) + #expect(isFinished == true) + } + + // MARK: - Test StreamingLambdaCodableAdapter + + @Test("StreamingLambdaCodableAdapter decodes JSON and calls handler") + func testStreamingLambdaCodableAdapter() async throws { + let responseWriter = MockResponseWriter() + let context = LambdaContext.makeTest() + + let closureHandler = StreamingFromEventClosureHandler { event, writer, context in + try await writer.writeAndFinish(ByteBuffer(string: "Echo: \(event.value)")) + } + + var adapter = StreamingLambdaCodableAdapter( + decoder: LambdaJSONEventDecoder(JSONDecoder()), + handler: closureHandler + ) + + let jsonData = #"{"value": "test message"}"# + let inputBuffer = ByteBuffer(string: jsonData) + + try await adapter.handle(inputBuffer, responseWriter: responseWriter, context: context) + + let writtenData = await responseWriter.getWrittenData() + let isFinished = await responseWriter.getFinished() + + #expect(writtenData == ["Echo: test message"]) + #expect(isFinished == true) + } + + @Test("StreamingLambdaCodableAdapter handles JSON decoding errors") + func testStreamingLambdaCodableAdapterDecodingError() async throws { + let responseWriter = MockResponseWriter() + let context = LambdaContext.makeTest() + + let closureHandler = StreamingFromEventClosureHandler { event, writer, context in + try await writer.writeAndFinish(ByteBuffer(string: "Should not reach here")) + } + + var adapter = StreamingLambdaCodableAdapter( + decoder: LambdaJSONEventDecoder(JSONDecoder()), + handler: closureHandler + ) + + let invalidJsonData = #"{"invalid": "json structure"}"# + let inputBuffer = ByteBuffer(string: invalidJsonData) + + await #expect(throws: DecodingError.self) { + try await adapter.handle(inputBuffer, responseWriter: responseWriter, context: context) + } + + let writtenData = await responseWriter.getWrittenData() + #expect(writtenData.isEmpty) + } + + @Test("StreamingLambdaCodableAdapter with convenience JSON initializer") + func testStreamingLambdaCodableAdapterJSONConvenience() async throws { + let responseWriter = MockResponseWriter() + let context = LambdaContext.makeTest() + + let closureHandler = StreamingFromEventClosureHandler { event, writer, context in + try await writer.write(ByteBuffer(string: "Message: \(event.message)\n")) + try await writer.write(ByteBuffer(string: "Count: \(event.count)\n")) + try await writer.finish() + } + + var adapter = StreamingLambdaCodableAdapter(handler: closureHandler) + + let jsonData = #"{"message": "Hello World", "count": 5, "delay": 100}"# + let inputBuffer = ByteBuffer(string: jsonData) + + try await adapter.handle(inputBuffer, responseWriter: responseWriter, context: context) + + let writtenData = await responseWriter.getWrittenData() + let isFinished = await responseWriter.getFinished() + + #expect(writtenData == ["Message: Hello World\n", "Count: 5\n"]) + #expect(isFinished == true) + } + + // MARK: - Test Error Handling + + @Test("Handler errors are properly propagated") + func testHandlerErrorPropagation() async throws { + let responseWriter = MockResponseWriter() + let context = LambdaContext.makeTest() + + let closureHandler = StreamingFromEventClosureHandler { event, writer, context in + throw MockError.handlerError + } + + var adapter = StreamingLambdaCodableAdapter( + decoder: LambdaJSONEventDecoder(JSONDecoder()), + handler: closureHandler + ) + + let jsonData = #"{"value": "test"}"# + let inputBuffer = ByteBuffer(string: jsonData) + + await #expect(throws: MockError.self) { + try await adapter.handle(inputBuffer, responseWriter: responseWriter, context: context) + } + } + + // MARK: - Test Custom Handler Implementation + + struct CustomStreamingHandler: StreamingLambdaHandlerWithEvent { + typealias Event = TestEvent + + func handle( + _ event: Event, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + context.logger.trace("Processing event with message: \(event.message)") + + let response = "Processed: \(event.message) with count \(event.count)" + try await responseWriter.writeAndFinish(ByteBuffer(string: response)) + } + } + + @Test("Custom StreamingLambdaHandlerWithEvent implementation works") + func testCustomStreamingHandler() async throws { + let responseWriter = MockResponseWriter() + let context = LambdaContext.makeTest() + + let handler = CustomStreamingHandler() + let testEvent = TestEvent(message: "Custom Handler Test", count: 10, delay: nil) + + try await handler.handle(testEvent, responseWriter: responseWriter, context: context) + + let writtenData = await responseWriter.getWrittenData() + let isFinished = await responseWriter.getFinished() + + #expect(writtenData == ["Processed: Custom Handler Test with count 10"]) + #expect(isFinished == true) + } + + @Test("Custom handler with adapter works end-to-end") + func testCustomHandlerWithAdapter() async throws { + let responseWriter = MockResponseWriter() + let context = LambdaContext.makeTest() + + let customHandler = CustomStreamingHandler() + var adapter = StreamingLambdaCodableAdapter(handler: customHandler) + + let jsonData = #"{"message": "End-to-end test", "count": 7}"# + let inputBuffer = ByteBuffer(string: jsonData) + + try await adapter.handle(inputBuffer, responseWriter: responseWriter, context: context) + + let writtenData = await responseWriter.getWrittenData() + let isFinished = await responseWriter.getFinished() + + #expect(writtenData == ["Processed: End-to-end test with count 7"]) + #expect(isFinished == true) + } + + // MARK: - Test Background Work Simulation + + @Test("Handler can perform background work after streaming") + func testBackgroundWorkAfterStreaming() async throws { + let responseWriter = MockResponseWriter() + let context = LambdaContext.makeTest() + + let backgroundWorkCompleted = Atomic(false) + + let handler = StreamingFromEventClosureHandler { event, writer, context in + // Send response first + try await writer.writeAndFinish(ByteBuffer(string: "Response: \(event.value)")) + + // Simulate background work + try await Task.sleep(for: .milliseconds(10)) + backgroundWorkCompleted.store(true, ordering: .relaxed) + } + + let testEvent = SimpleEvent(value: "background test") + + try await handler.handle(testEvent, responseWriter: responseWriter, context: context) + + let writtenData = await responseWriter.getWrittenData() + let isFinished = await responseWriter.getFinished() + let writeAndFinishCalled = await responseWriter.getWriteAndFinishCalled() + + #expect(writtenData == ["Response: background test"]) + #expect(isFinished == true) + #expect(writeAndFinishCalled == true) + #expect(backgroundWorkCompleted.load(ordering: .relaxed) == true) + } +} + +// MARK: - Test Helpers + +extension LambdaContext { + static func makeTest() -> LambdaContext { + LambdaContext.__forTestsOnly( + requestID: "test-request-id", + traceID: "test-trace-id", + invokedFunctionARN: "arn:aws:lambda:us-east-1:123456789012:function:test", + timeout: .seconds(30), + logger: Logger(label: "test") + ) + } +} diff --git a/Examples/Streaming+Codable/events/sample-request.json b/Examples/Streaming+Codable/events/sample-request.json new file mode 100644 index 00000000..145cb315 --- /dev/null +++ b/Examples/Streaming+Codable/events/sample-request.json @@ -0,0 +1,49 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/", + "rawQueryString": "", + "body": "{\"count\": 5, \"message\": \"Hello from streaming Lambda!\", \"delayMs\": 1000}", + "headers": { + "x-amzn-tls-cipher-suite": "TLS_AES_128_GCM_SHA256", + "x-amzn-tls-version": "TLSv1.3", + "x-amzn-trace-id": "Root=1-68762f44-4f6a87d1639e7fc356aa6f96", + "x-amz-date": "20250715T103651Z", + "x-forwarded-proto": "https", + "host": "zvnsvhpx7u5gn3l3euimg4jjou0jvbfe.lambda-url.us-east-1.on.aws", + "x-forwarded-port": "443", + "x-forwarded-for": "2a01:cb0c:6de:8300:a1be:8004:e31a:b9f", + "accept": "*/*", + "user-agent": "curl/8.7.1" + }, + "requestContext": { + "accountId": "0123456789", + "apiId": "zvnsvhpx7u5gn3l3euimg4jjou0jvbfe", + "authorizer": { + "iam": { + "accessKey": "AKIA....", + "accountId": "0123456789", + "callerId": "AIDA...", + "cognitoIdentity": null, + "principalOrgId": "o-rlrup7z3ao", + "userArn": "arn:aws:iam::0123456789:user/sst", + "userId": "AIDA..." + } + }, + "domainName": "zvnsvhpx7u5gn3l3euimg4jjou0jvbfe.lambda-url.us-east-1.on.aws", + "domainPrefix": "zvnsvhpx7u5gn3l3euimg4jjou0jvbfe", + "http": { + "method": "GET", + "path": "/", + "protocol": "HTTP/1.1", + "sourceIp": "2a01:...:b9f", + "userAgent": "curl/8.7.1" + }, + "requestId": "f942509a-283f-4c4f-94f8-0d4ccc4a00f8", + "routeKey": "$default", + "stage": "$default", + "time": "15/Jul/2025:10:36:52 +0000", + "timeEpoch": 1752575812081 + }, + "isBase64Encoded": false +} \ No newline at end of file diff --git a/Examples/Streaming+Codable/template.yaml b/Examples/Streaming+Codable/template.yaml new file mode 100644 index 00000000..fc3e462f --- /dev/null +++ b/Examples/Streaming+Codable/template.yaml @@ -0,0 +1,41 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for StreamingfromEvent Example + +# This is an example SAM template for the purpose of this project. +# When deploying such infrastructure in production environment, +# we strongly encourage you to follow these best practices for improved security and resiliency +# - Enable access loggin on API Gateway +# See: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html) +# - Ensure that AWS Lambda function is configured for function-level concurrent execution limit +# See: https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html +# https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html +# - Check encryption settings for Lambda environment variable +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html +# - Ensure that AWS Lambda function is configured for a Dead Letter Queue(DLQ) +# See: https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq +# - Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html +# Code Example: https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres + +Resources: + # Lambda function + StreamingCodable: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingCodable/StreamingCodable.zip + Timeout: 15 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 128 + Architectures: + - arm64 + FunctionUrlConfig: + AuthType: AWS_IAM + InvokeMode: RESPONSE_STREAM + +Outputs: + # print Lambda function URL + LambdaURL: + Description: Lambda URL + Value: !GetAtt StreamingCodableUrl.FunctionUrl diff --git a/Examples/Streaming/.gitignore b/Examples/Streaming/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/Examples/Streaming/.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/Streaming/Package.swift b/Examples/Streaming/Package.swift new file mode 100644 index 00000000..d972e995 --- /dev/null +++ b/Examples/Streaming/Package.swift @@ -0,0 +1,54 @@ +// swift-tools-version:6.2 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "StreamingNumbers", targets: ["StreamingNumbers"]) + ], + 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", from: "2.0.0") + ], + targets: [ + .executableTarget( + name: "StreamingNumbers", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: "Sources" + ) + ] +) + +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/Streaming/README.md b/Examples/Streaming/README.md new file mode 100644 index 00000000..2c40df57 --- /dev/null +++ b/Examples/Streaming/README.md @@ -0,0 +1,340 @@ +# Streaming Lambda function + +You can configure your Lambda function to stream response payloads back to clients. Response streaming can benefit latency sensitive applications by improving time to first byte (TTFB) performance. This is because you can send partial responses back to the client as they become available. Additionally, you can use response streaming to build functions that return larger payloads. Response stream payloads have a soft limit of 200 MB as compared to the 6 MB limit for buffered responses. Streaming a response also means that your function doesn’t need to fit the entire response in memory. For very large responses, this can reduce the amount of memory you need to configure for your function. + +Streaming responses incurs a cost. For more information, see [AWS Lambda Pricing](https://aws.amazon.com/lambda/pricing/). + +You can stream responses through [Lambda function URLs](https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html), the AWS SDK, or using the Lambda [InvokeWithResponseStream](https://docs.aws.amazon.com/lambda/latest/dg/API_InvokeWithResponseStream.html) API. In this example, we create an authenticated Lambda function URL. + + +## Code + +The sample code creates a `SendNumbersWithPause` struct that conforms to the `StreamingLambdaHandler` protocol provided by the Swift AWS Lambda Runtime. + +The `handle(...)` method of this protocol receives incoming events as a Swift NIO `ByteBuffer` and returns the output as a `ByteBuffer`. + +The response is streamed through the `LambdaResponseStreamWriter`, which is passed as an argument in the `handle` function. + +### Setting HTTP Status Code and Headers + +Before streaming the response body, you can set the HTTP status code and headers using the `writeStatusAndHeaders(_:)` method: + +```swift +try await responseWriter.writeStatusAndHeaders( + StreamingLambdaStatusAndHeadersResponse( + statusCode: 200, + headers: [ + "Content-Type": "text/plain", + "x-my-custom-header": "streaming-example" + ] + ) +) +``` + +The `StreamingLambdaStatusAndHeadersResponse` structure allows you to specify: +- **statusCode**: HTTP status code (e.g., 200, 404, 500) +- **headers**: Dictionary of single-value HTTP headers (optional) + +### Streaming the Response Body + +After setting headers, you can stream the response body by calling the `write(_:)` function of the `LambdaResponseStreamWriter` with partial data repeatedly before finally closing the response stream by calling `finish()`. Developers can also choose to return the entire output and not stream the response by calling `writeAndFinish(_:)`. + +```swift +// Stream data in chunks +for i in 1...3 { + try await responseWriter.write(ByteBuffer(string: "Number: \(i)\n")) + try await Task.sleep(for: .milliseconds(1000)) +} + +// Close the response stream +try await responseWriter.finish() +``` + +An error is thrown if `finish()` is called multiple times or if it is called after having called `writeAndFinish(_:)`. + +### Example Usage Patterns + +The example includes two handler implementations: + +1. **SendNumbersWithPause**: Demonstrates basic streaming with headers, sending numbers with delays +2. **ConditionalStreamingHandler**: Shows how to handle different response scenarios, including error responses with appropriate status codes + +The `handle(...)` method is marked as `mutating` to allow handlers to be implemented with a `struct`. + +Once the struct is created and the `handle(...)` method is defined, the sample code creates a `LambdaRuntime` struct and initializes it with the handler just created. Then, the code calls `run()` to start the interaction with the AWS Lambda control plane. + +## Build & Package + +To build & archive the package, type the following commands. + +```bash +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingNumbers/StreamingNumbers.zip` + +## Test locally + +You can test the function locally before deploying: + +```bash +swift run + +# In another terminal, test with curl: +curl -v \ + --header "Content-Type: application/json" \ + --data '"this is not used"' \ + http://127.0.0.1:7000/invoke +``` + +## Deploy with the AWS CLI + +Here is how to deploy using the `aws` command line. + +### Step 1: Create the function + +```bash +# Replace with your AWS Account ID +AWS_ACCOUNT_ID=012345678901 +aws lambda create-function \ +--function-name StreamingNumbers \ +--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingNumbers/StreamingNumbers.zip \ +--runtime provided.al2 \ +--handler provided \ +--architectures arm64 \ +--role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution \ +--timeout 15 +``` + +> [!IMPORTANT] +> The timeout value must be bigger than the time it takes for your function to stream its output. Otherwise, the Lambda control plane will terminate the execution environment before your code has a chance to finish writing the stream. Here, the sample function stream responses during 3 seconds and we set the timeout for 5 seconds. + +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 set `AWS_ACCOUNT_ID` with your actual AWS account ID (for example: 012345678901). + +### Step2: Give permission to invoke that function through an URL + +Anyone with a valid signature from your AWS account will have permission to invoke the function through its URL. + +```bash +aws lambda add-permission \ + --function-name StreamingNumbers \ + --action lambda:InvokeFunctionUrl \ + --principal ${AWS_ACCOUNT_ID} \ + --function-url-auth-type AWS_IAM \ + --statement-id allowURL +``` + +### Step3: Create the URL + +This creates [a URL with IAM authentication](https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html). Only calls with a valid signature will be authorized. + +```bash +aws lambda create-function-url-config \ + --function-name StreamingNumbers \ + --auth-type AWS_IAM \ + --invoke-mode RESPONSE_STREAM +``` +This calls return various information, including the URL to invoke your function. + +```json +{ + "FunctionUrl": "https://ul3nf4dogmgyr7ffl5r5rs22640fwocc.lambda-url.us-east-1.on.aws/", + "FunctionArn": "arn:aws:lambda:us-east-1:012345678901:function:StreamingNumbers", + "AuthType": "AWS_IAM", + "CreationTime": "2024-10-22T07:57:23.112599Z", + "InvokeMode": "RESPONSE_STREAM" +} +``` + +### Invoke your Lambda function + +To invoke the Lambda function, use `curl` with the AWS Sigv4 option to generate the signature. + +Read the [AWS Credentials and Signature](../README.md/#AWS-Credentials-and-Signature) section for more details about the AWS Sigv4 protocol and how to obtain AWS credentials. + +When you have the `aws` command line installed and configured, you will find the credentials in the `~/.aws/credentials` file. + +```bash +URL=https://ul3nf4dogmgyr7ffl5r5rs22640fwocc.lambda-url.us-east-1.on.aws/ +REGION=us-east-1 +ACCESS_KEY=AK... +SECRET_KEY=... +AWS_SESSION_TOKEN=... + +curl "$URL" \ + --user "${ACCESS_KEY}":"${SECRET_KEY}" \ + --aws-sigv4 "aws:amz:${REGION}:lambda" \ + -H "x-amz-security-token: ${AWS_SESSION_TOKEN}" \ + --no-buffer +``` + +Note that there is no payload required for this example. + +This should output the following result, with a one-second delay between each numbers. + +``` +1 +2 +3 +Streaming complete! +``` + +### Undeploy + +When done testing, you can delete the Lambda function with this command. + +```bash +aws lambda delete-function --function-name StreamingNumbers +``` + +## Deploy with AWS SAM + +Alternatively, you can use [AWS SAM](https://aws.amazon.com/serverless/sam/) to deploy the Lambda function. + +**Prerequisites** : Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) + +### SAM Template + +The template file is provided as part of the example in the `template.yaml` file. It defines a Lambda function based on the binary ZIP file. It creates the function url with IAM authentication and sets the function timeout to 15 seconds. + +```yaml +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for StreamingLambda Example + +Resources: + # Lambda function + StreamingNumbers: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingNumbers/StreamingNumbers.zip + Timeout: 15 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 128 + Architectures: + - arm64 + FunctionUrlConfig: + AuthType: AWS_IAM + InvokeMode: RESPONSE_STREAM + +Outputs: + # print Lambda function URL + LambdaURL: + Description: Lambda URL + Value: !GetAtt StreamingNumbersUrl.FunctionUrl +``` + +### Deploy with SAM + +```bash +sam deploy \ +--resolve-s3 \ +--template-file template.yaml \ +--stack-name StreamingNumbers \ +--capabilities CAPABILITY_IAM +``` + +The URL of the function is provided as part of the output. + +``` +CloudFormation outputs from deployed stack +----------------------------------------------------------------------------------------------------------------------------- +Outputs +----------------------------------------------------------------------------------------------------------------------------- +Key LambdaURL +Description Lambda URL +Value https://gaudpin2zjqizfujfnqxstnv6u0czrfu.lambda-url.us-east-1.on.aws/ +----------------------------------------------------------------------------------------------------------------------------- +``` + +Once the function is deployed, you can invoke it with `curl`, similarly to what you did when deploying with the AWS CLI. + +```bash +curl "$URL" \ + --user "$ACCESS_KEY":"$SECRET_KEY" \ + --aws-sigv4 "aws:amz:${REGION}:lambda" \ + -H "x-amz-security-token: $AWS_SESSION_TOKEN" \ + --no-buffer +``` + +### Undeploy with SAM + +When done testing, you can delete the infrastructure with this command. + +```bash +sam delete +``` + +## Payload decoding + +The content of the input `ByteBuffer` depends on how you invoke the function: + +- when you use [`InvokeWithResponseStream` API](https://docs.aws.amazon.com/lambda/latest/api/API_InvokeWithResponseStream.html) to invoke the function, the function incoming payload is what you pass to the API. You can decode the `ByteBuffer` with a [`JSONDecoder.decode()`](https://developer.apple.com/documentation/foundation/jsondecoder) function call. +- when you invoke the function through a [Lambda function URL](https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html), the incoming `ByteBuffer` contains a payload that gives developer access to the underlying HTTP call. The payload contains information about the HTTP verb used, the headers received, the authentication method and so on. The [AWS documentation contains the details](https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html) of the payload. The [Swift Lambda Event library](https://github.com/swift-server/swift-aws-lambda-events) contains a [`FunctionURL` type](https://github.com/swift-server/swift-aws-lambda-events/blob/main/Sources/AWSLambdaEvents/FunctionURL.swift) ready to use in your projects. + +Here is an example of Lambda function URL payload: + +```json +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/", + "rawQueryString": "", + "headers": { + "x-amzn-tls-cipher-suite": "TLS_AES_128_GCM_SHA256", + "x-amzn-tls-version": "TLSv1.3", + "x-amzn-trace-id": "Root=1-68762f44-4f6a87d1639e7fc356aa6f96", + "x-amz-date": "20250715T103651Z", + "x-forwarded-proto": "https", + "host": "zvnsvhpx7u5gn3l3euimg4jjou0jvbfe.lambda-url.us-east-1.on.aws", + "x-forwarded-port": "443", + "x-forwarded-for": "2a01:cb0c:6de:8300:a1be:8004:e31a:b9f", + "accept": "*/*", + "user-agent": "curl/8.7.1" + }, + "requestContext": { + "accountId": "0123456789", + "apiId": "zvnsvhpx7u5gn3l3euimg4jjou0jvbfe", + "authorizer": { + "iam": { + "accessKey": "AKIA....", + "accountId": "0123456789", + "callerId": "AIDA...", + "cognitoIdentity": null, + "principalOrgId": "o-rlrup7z3ao", + "userArn": "arn:aws:iam::0123456789:user/sst", + "userId": "AIDA..." + } + }, + "domainName": "zvnsvhpx7u5gn3l3euimg4jjou0jvbfe.lambda-url.us-east-1.on.aws", + "domainPrefix": "zvnsvhpx7u5gn3l3euimg4jjou0jvbfe", + "http": { + "method": "GET", + "path": "/", + "protocol": "HTTP/1.1", + "sourceIp": "2a01:...:b9f", + "userAgent": "curl/8.7.1" + }, + "requestId": "f942509a-283f-4c4f-94f8-0d4ccc4a00f8", + "routeKey": "$default", + "stage": "$default", + "time": "15/Jul/2025:10:36:52 +0000", + "timeEpoch": 1752575812081 + }, + "isBase64Encoded": false +} +``` + +## ⚠️ Security and Reliability Notice + +These are example applications for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: + +- Enable access logging on API Gateway ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)) +- Ensure that AWS Lambda function is configured for function-level concurrent execution limit ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +- Check encryption settings for Lambda environment variables ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +- Ensure that AWS Lambda function is configured for a Dead Letter Queue (DLQ) ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +- Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html), [code example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres)) \ No newline at end of file diff --git a/Examples/Streaming/Sources/main.swift b/Examples/Streaming/Sources/main.swift new file mode 100644 index 00000000..d8831976 --- /dev/null +++ b/Examples/Streaming/Sources/main.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +struct SendNumbersWithPause: StreamingLambdaHandler { + func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + + // Send HTTP status code and headers before streaming the response body + try await responseWriter.writeStatusAndHeaders( + StreamingLambdaStatusAndHeadersResponse( + statusCode: 418, // I'm a tea pot + headers: [ + "Content-Type": "text/plain", + "x-my-custom-header": "streaming-example", + ] + ) + ) + + // Stream numbers with pauses to demonstrate streaming functionality + for i in 1...3 { + // Send partial data + try await responseWriter.write(ByteBuffer(string: "Number: \(i)\n")) + + // Perform some long asynchronous work to simulate processing + try await Task.sleep(for: .milliseconds(1000)) + } + + // Send final message + try await responseWriter.write(ByteBuffer(string: "Streaming complete!\n")) + + // All data has been sent. Close off the response stream. + try await responseWriter.finish() + } +} + +let runtime = LambdaRuntime(handler: SendNumbersWithPause()) +try await runtime.run() diff --git a/Examples/Streaming/samconfig.toml b/Examples/Streaming/samconfig.toml new file mode 100644 index 00000000..6601b7de --- /dev/null +++ b/Examples/Streaming/samconfig.toml @@ -0,0 +1,8 @@ +version = 0.1 +[default.deploy.parameters] +stack_name = "StreamingNumbers" +resolve_s3 = true +s3_prefix = "StreamingNumbers" +region = "us-east-1" +capabilities = "CAPABILITY_IAM" +image_repositories = [] diff --git a/Examples/Streaming/template.yaml b/Examples/Streaming/template.yaml new file mode 100644 index 00000000..033481c9 --- /dev/null +++ b/Examples/Streaming/template.yaml @@ -0,0 +1,44 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for Streaming Example + +# This is an example SAM template for the purpose of this project. +# When deploying such infrastructure in production environment, +# we strongly encourage you to follow these best practices for improved security and resiliency +# - Enable access loggin on API Gateway +# See: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html) +# - Ensure that AWS Lambda function is configured for function-level concurrent execution limit +# See: https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html +# https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html +# - Check encryption settings for Lambda environment variable +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html +# - Ensure that AWS Lambda function is configured for a Dead Letter Queue(DLQ) +# See: https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq +# - Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html +# Code Example: https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres + +Resources: + # Lambda function + StreamingNumbers: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingNumbers/StreamingNumbers.zip + Timeout: 5 # Must be bigger than the time it takes to stream the output + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 128 + Architectures: + - arm64 + FunctionUrlConfig: + AuthType: AWS_IAM + InvokeMode: RESPONSE_STREAM + Environment: + Variables: + LOG_LEVEL: trace + +Outputs: + # print Lambda function URL + LambdaURL: + Description: Lambda URL + Value: !GetAtt StreamingNumbersUrl.FunctionUrl diff --git a/Examples/Testing/Package.swift b/Examples/Testing/Package.swift index b29e9b19..64757eff 100644 --- a/Examples/Testing/Package.swift +++ b/Examples/Testing/Package.swift @@ -1,35 +1,64 @@ -// swift-tools-version:5.7 +// swift-tools-version:6.2 -import class Foundation.ProcessInfo // needed for CI to test the local version of the library import PackageDescription +// needed for CI to test the local version of the library +import struct Foundation.URL + let package = Package( name: "swift-aws-lambda-runtime-example", - platforms: [ - .macOS(.v12), - ], + platforms: [.macOS(.v15)], products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), + .executable(name: "APIGatewayLambda", targets: ["APIGatewayLambda"]) ], dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), + // 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", from: "2.0.0"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.0.0"), ], targets: [ .executableTarget( - name: "MyLambda", + name: "APIGatewayLambda", dependencies: [ .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - .product(name: "AWSLambdaTesting", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), ], path: "Sources" ), - .testTarget(name: "MyLambdaTests", dependencies: ["MyLambda"], path: "Tests"), + .testTarget( + name: "LambdaFunctionTests", + dependencies: ["APIGatewayLambda"], + path: "Tests", + resources: [ + .process("event.json") + ] + ), ] ) -// for CI to test the local version of the library -if ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"] != nil { - package.dependencies = [ - .package(name: "swift-aws-lambda-runtime", path: "../.."), +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/Testing/README.md b/Examples/Testing/README.md new file mode 100644 index 00000000..f3e76c5a --- /dev/null +++ b/Examples/Testing/README.md @@ -0,0 +1,181 @@ +# Swift Testing Example + +This is a simple example to show different testing strategies for your Swift Lambda functions. +For this example, we developed a simple Lambda function that returns the body of the API Gateway payload in lowercase, except for the first letter, which is in uppercase. + +In this document, we describe four different testing strategies: + * [Unit Testing your business logic](#unit-testing-your-business-logic) + * [Integration testing the handler function](#integration-testing-the-handler-function) + * [Local invocation using the Swift AWS Lambda Runtime](#local-invocation-using-the-swift-aws-lambda-runtime) + * [Local invocation using the AWS SAM CLI](#local-invocation-using-the-aws-sam-cli) + +> [!IMPORTANT] +> In this example, the API Gateway sends an event to your Lambda function as a JSON string. Your business payload is in the `body` section of the API Gateway event. It is base64-encoded. You can find an example of the API Gateway event in the `event.json` file. The API Gateway event format is documented in [Create AWS Lambda proxy integrations for HTTP APIs in API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html). + +To include a sample event in your test targets, you must add the `event.json` file from the `Tests` directory to the binary bundle. To do so, add a `resources` section in your `Package.swift` file: + +```swift + .testTarget( + name: "LambdaFunctionTests", + dependencies: ["APIGatewayLambda"], + path: "Tests", + resources: [ + .process("event.json") + ] + ) +``` + +## Unit Testing your business logic + +You can test the business logic of your Lambda function by writing unit tests for your business code used in the handler function, just like usual. + +1. Create your Swift Test code in the `Tests` directory. + +```swift +let valuesToTest: [(String, String)] = [ + ("hello world", "Hello world"), // happy path + ("", ""), // Empty string + ("a", "A"), // Single character +] + +@Suite("Business Tests") +class BusinessTests { + + @Test("Uppercased First", arguments: valuesToTest) + func uppercasedFirst(_ arg: (String,String)) { + let input = arg.0 + let expectedOutput = arg.1 + #expect(input.uppercasedFirst() == expectedOutput) + } +} +``` + +2. Add a test target to your `Package.swift` file. +```swift + .testTarget( + name: "BusinessTests", + dependencies: ["APIGatewayLambda"], + path: "Tests" + ) +``` + +3. run `swift test` to run the tests. + +## Integration Testing the handler function + +You can test the handler function by creating an input event, a mock Lambda context, and calling your Lambda handler function from your test. +Your Lambda handler function must be declared separatly from the `LambdaRuntime`. For example: + +```swift +public struct MyHandler: Sendable { + + public func handler(event: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response { + context.logger.debug("HTTP API Message received") + context.logger.trace("Event: \(event)") + + var header = HTTPHeaders() + header["content-type"] = "application/json" + + if let payload = event.body { + // call our business code to process the payload and return a response + return APIGatewayV2Response(statusCode: .ok, headers: header, body: payload.uppercasedFirst()) + } else { + return APIGatewayV2Response(statusCode: .badRequest) + } + } +} + +let runtime = LambdaRuntime(body: MyHandler().handler) +try await runtime.run() +``` + +Then, the test looks like this: + +```swift +@Suite("Handler Tests") +public struct HandlerTest { + + @Test("Invoke handler") + public func invokeHandler() async throws { + + // read event.json file + let testBundle = Bundle.module + guard let eventURL = testBundle.url(forResource: "event", withExtension: "json") else { + Issue.record("event.json not found in test bundle") + return + } + let eventData = try Data(contentsOf: eventURL) + + // decode the event + let apiGatewayRequest = try JSONDecoder().decode(APIGatewayV2Request.self, from: eventData) + + // create a mock LambdaContext + let lambdaContext = LambdaContext.__forTestsOnly( + requestID: UUID().uuidString, + traceID: UUID().uuidString, + invokedFunctionARN: "arn:", + timeout: .milliseconds(6000), + logger: Logger(label: "fakeContext") + ) + + // call the handler with the event and context + let response = try await MyHandler().handler(event: apiGatewayRequest, context: lambdaContext) + + // assert the response + #expect(response.statusCode == .ok) + #expect(response.body == "Hello world of swift lambda!") + } +} +``` + +## Local invocation using the Swift AWS Lambda Runtime + +You can test your Lambda function locally by invoking it with the Swift AWS Lambda Runtime. + +You must pass an event to the Lambda function. You can use the `Tests/event.json` file for this purpose. The return value is a `APIGatewayV2Response` object in this example. + +Just type `swift run` to run the Lambda function locally, this starts a local HTTP endpoint on localhost:7000. + +```sh +LOG_LEVEL=trace swift run + +# from another terminal +# the `-X POST` flag is implied when using `--data`. It is here for clarity only. +curl -X POST "http://127.0.0.1:7000/invoke" --data @Tests/event.json +``` + +This returns the following response: + +```text +{"statusCode":200,"headers":{"content-type":"application\/json"},"body":"Hello world of swift lambda!"} +``` + +## Local invocation using the AWS SAM CLI + +The AWS SAM CLI provides you with a local testing environment for your Lambda functions. It deploys and invokes your function locally in a Docker container designed to mimic the AWS Lambda environment. + +You must pass an event to the Lambda function. You can use the `event.json` file for this purpose. The return value is a `APIGatewayV2Response` object in this example. + +```sh +sam local invoke -e Tests/event.json + +START RequestId: 3270171f-46d3-45f9-9bb6-3c2e5e9dc625 Version: $LATEST +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 +REPORT RequestId: 5b71587a-39da-445e-855d-27a700e57efd Init Duration: 0.04 ms Duration: 21.57 ms Billed Duration: 22 ms Memory Size: 512 MB Max Memory Used: 512 MB + +{"body": "Hello world of swift lambda!", "statusCode": 200, "headers": {"content-type": "application/json"}} +``` + +## ⚠️ Security and Reliability Notice + +These are example applications for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: + +- Enable access logging on API Gateway ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)) +- Ensure that AWS Lambda function is configured for function-level concurrent execution limit ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +- Check encryption settings for Lambda environment variables ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +- Ensure that AWS Lambda function is configured for a Dead Letter Queue (DLQ) ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +- Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html), [code example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres)) \ No newline at end of file diff --git a/Examples/Testing/Sources/Business.swift b/Examples/Testing/Sources/Business.swift new file mode 100644 index 00000000..af95b8e5 --- /dev/null +++ b/Examples/Testing/Sources/Business.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +extension String { + /// Returns a new string with the first character capitalized and the remaining characters in lowercase. + /// + /// This method capitalizes the first character of the string and converts the remaining characters to lowercase. + /// It is useful for formatting strings where only the first character should be uppercase. + /// + /// - Returns: A new string with the first character capitalized and the remaining characters in lowercase. + /// + /// - Example: + /// ``` + /// let example = "hello world" + /// print(example.uppercasedFirst()) // Prints "Hello world" + /// ``` + func uppercasedFirst() -> String { + let firstCharacter = prefix(1).capitalized + let remainingCharacters = dropFirst().lowercased() + return firstCharacter + remainingCharacters + } +} diff --git a/Examples/Testing/Sources/main.swift b/Examples/Testing/Sources/main.swift new file mode 100644 index 00000000..af76e02c --- /dev/null +++ b/Examples/Testing/Sources/main.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// 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 AWSLambdaEvents +import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +public struct MyHandler: Sendable { + + public func handler(event: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response { + context.logger.debug("HTTP API Message received") + context.logger.trace("Event: \(event)") + + var header = HTTPHeaders() + header["content-type"] = "application/json" + + // API Gateway sends text or URL encoded data as a Base64 encoded string + if let base64EncodedString = event.body, + let decodedData = Data(base64Encoded: base64EncodedString), + let decodedString = String(data: decodedData, encoding: .utf8) + { + + // call our business code to process the payload and return a response + return APIGatewayV2Response(statusCode: .ok, headers: header, body: decodedString.uppercasedFirst()) + } else { + return APIGatewayV2Response(statusCode: .badRequest) + } + } +} + +let runtime = LambdaRuntime(body: MyHandler().handler) +try await runtime.run() diff --git a/Examples/Testing/Tests/BusinessTests.swift b/Examples/Testing/Tests/BusinessTests.swift new file mode 100644 index 00000000..85f821e1 --- /dev/null +++ b/Examples/Testing/Tests/BusinessTests.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// 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 Testing + +@testable import APIGatewayLambda // to access the business code + +let valuesToTest: [(String, String)] = [ + ("hello world", "Hello world"), // happy path + ("", ""), // Empty string + ("a", "A"), // Single character + ("A", "A"), // Single uppercase character + ("HELLO WORLD", "Hello world"), // All uppercase + ("hello world", "Hello world"), // All lowercase + ("hElLo WoRlD", "Hello world"), // Mixed case + ("123abc", "123abc"), // Numeric string + ("!@#abc", "!@#abc"), // Special characters +] + +@Suite("Business Tests") +class BusinessTests { + + @Test("Uppercased First", arguments: valuesToTest) + func uppercasedFirst(_ arg: (String, String)) { + let input = arg.0 + let expectedOutput = arg.1 + #expect(input.uppercasedFirst() == expectedOutput) + } +} diff --git a/Examples/Testing/Tests/HandlerTests.swift b/Examples/Testing/Tests/HandlerTests.swift new file mode 100644 index 00000000..85cc4e4e --- /dev/null +++ b/Examples/Testing/Tests/HandlerTests.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// 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 AWSLambdaEvents +import AWSLambdaRuntime +import Logging +import Testing + +@testable import APIGatewayLambda // to access the business code +@testable import AWSLambdaRuntime // to access the LambdaContext + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@Suite("Handler Tests") +public struct HandlerTest { + + @Test("Invoke handler") + public func invokeHandler() async throws { + + // read event.json file + let testBundle = Bundle.module + guard let eventURL = testBundle.url(forResource: "event", withExtension: "json") else { + Issue.record("event.json not found in test bundle") + return + } + let eventData = try Data(contentsOf: eventURL) + + // decode the event + let apiGatewayRequest = try JSONDecoder().decode(APIGatewayV2Request.self, from: eventData) + + // create a mock LambdaContext + let lambdaContext = LambdaContext.__forTestsOnly( + requestID: UUID().uuidString, + traceID: UUID().uuidString, + invokedFunctionARN: "arn:", + timeout: .milliseconds(6000), + logger: Logger(label: "fakeContext") + ) + + // call the handler with the event and context + let response = try await MyHandler().handler(event: apiGatewayRequest, context: lambdaContext) + + // assert the response + #expect(response.statusCode == .ok) + #expect(response.body == "Hello world of swift lambda!") + } +} diff --git a/Examples/Testing/Tests/event.json b/Examples/Testing/Tests/event.json new file mode 100644 index 00000000..213f8bee --- /dev/null +++ b/Examples/Testing/Tests/event.json @@ -0,0 +1,35 @@ +{ + "version": "2.0", + "rawPath": "/", + "body": "aGVsbG8gd29ybGQgb2YgU1dJRlQgTEFNQkRBIQ==", + "requestContext": { + "domainPrefix": "a5q74es3k2", + "stage": "$default", + "timeEpoch": 1727726558220, + "http": { + "protocol": "HTTP/1.1", + "method": "GET", + "userAgent": "curl/8.7.1", + "path": "/", + "sourceIp": "81.0.0.43" + }, + "apiId": "a5q74es3k2", + "accountId": "012345678901", + "requestId": "e72KxgsRoAMEMSA=", + "domainName": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", + "time": "30/Sep/2024:20:02:38 +0000" + }, + "rawQueryString": "", + "routeKey": "$default", + "headers": { + "x-forwarded-for": "81.0.0.43", + "user-agent": "curl/8.7.1", + "host": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", + "accept": "*/*", + "x-amzn-trace-id": "Root=1-66fb03de-07533930192eaf5f540db0cb", + "content-length": "0", + "x-forwarded-proto": "https", + "x-forwarded-port": "443" + }, + "isBase64Encoded": false +} diff --git a/Examples/Testing/template.yaml b/Examples/Testing/template.yaml new file mode 100644 index 00000000..ad8f856f --- /dev/null +++ b/Examples/Testing/template.yaml @@ -0,0 +1,47 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for APIGateway Lambda Example + +# This is an example SAM template for the purpose of this project. +# When deploying such infrastructure in production environment, +# we strongly encourage you to follow these best practices for improved security and resiliency +# - Enable access loggin on API Gateway +# See: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html) +# - Ensure that AWS Lambda function is configured for function-level concurrent execution limit +# See: https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html +# https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html +# - Check encryption settings for Lambda environment variable +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html +# - Ensure that AWS Lambda function is configured for a Dead Letter Queue(DLQ) +# See: https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq +# - Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html +# Code Example: https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres + +Resources: + # Lambda function + APIGatewayLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip + Timeout: 60 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 128 + Architectures: + - arm64 + Environment: + Variables: + # by default, AWS Lambda runtime produces no log + # use `LOG_LEVEL: debug` for for lifecycle and event handling information + # use `LOG_LEVEL: trace` for detailed input event information + LOG_LEVEL: trace + Events: + HttpApiEvent: + Type: HttpApi + +Outputs: + # print API Gateway endpoint + APIGatewayEndpoint: + Description: API Gateway endpoint UR" + Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" diff --git a/Examples/Tutorial/.gitignore b/Examples/Tutorial/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/Examples/Tutorial/.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/Tutorial/Package.swift b/Examples/Tutorial/Package.swift new file mode 100644 index 00000000..6158884f --- /dev/null +++ b/Examples/Tutorial/Package.swift @@ -0,0 +1,51 @@ +// swift-tools-version: 6.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +import struct Foundation.URL + +let package = Package( + name: "Palindrome", + platforms: [.macOS(.v15)], + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "2.0.0") + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .executableTarget( + name: "Palindrome", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ] + ) + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} diff --git a/Examples/Tutorial/Sources/main.swift b/Examples/Tutorial/Sources/main.swift new file mode 100644 index 00000000..db28931d --- /dev/null +++ b/Examples/Tutorial/Sources/main.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// 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 AWSLambdaRuntime + +// the data structure to represent the input parameter +struct Request: Decodable { + let text: String +} + +// the data structure to represent the response parameter +struct Response: Encodable { + let text: String + let isPalindrome: Bool + let message: String +} + +// the business function +func isPalindrome(_ text: String) -> Bool { + let cleanedText = text.lowercased().filter { $0.isLetter } + return cleanedText == String(cleanedText.reversed()) +} + +// the lambda handler function +let runtime = LambdaRuntime { + (event: Request, context: LambdaContext) -> Response in + + let result = isPalindrome(event.text) + return Response( + text: event.text, + isPalindrome: result, + message: "Your text is \(result ? "a" : "not a") palindrome" + ) +} + +// start the runtime +try await runtime.run() diff --git a/Examples/_MyFirstFunction/.gitignore b/Examples/_MyFirstFunction/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/Examples/_MyFirstFunction/.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/_MyFirstFunction/clean.sh b/Examples/_MyFirstFunction/clean.sh new file mode 100755 index 00000000..457e1c8a --- /dev/null +++ b/Examples/_MyFirstFunction/clean.sh @@ -0,0 +1,36 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2017-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 +## +##===----------------------------------------------------------------------===## + +echo "This script deletes the Lambda function and the IAM role created in the previous step and deletes the project files." +read -r -p "Are you you sure you want to delete everything that was created? [y/n] " continue +if [[ ! $continue =~ ^[Yy]$ ]]; then + echo "OK, try again later when you feel ready" + exit 1 +fi + +echo "🚀 Deleting the Lambda function and the role" +aws lambda delete-function --function-name MyLambda +aws iam detach-role-policy \ + --role-name lambda_basic_execution \ + --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole +aws iam delete-role --role-name lambda_basic_execution + +echo "🚀 Deleting the project files" +rm -rf .build +rm -rf ./Sources +rm trust-policy.json +rm Package.swift Package.resolved + +echo "🎉 Done! Your project is cleaned up and ready for a fresh start." \ No newline at end of file diff --git a/Examples/_MyFirstFunction/create_and_deploy_function.sh b/Examples/_MyFirstFunction/create_and_deploy_function.sh new file mode 100755 index 00000000..1020648a --- /dev/null +++ b/Examples/_MyFirstFunction/create_and_deploy_function.sh @@ -0,0 +1,194 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2017-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 +## +##===----------------------------------------------------------------------===## + +# Stop the script execution if an error occurs +set -e -o pipefail + +check_prerequisites() { + # check if docker is installed + which docker > /dev/null || (echo "Docker is not installed. Please install Docker and try again." && exit 1) + + # check if aws cli is installed + which aws > /dev/null || (echo "AWS CLI is not installed. Please install AWS CLI and try again." && exit 1) + + # check if user has an access key and secret access key + echo "This script creates and deploys a Lambda function on your AWS Account. + + You must have an AWS account and know an AWS access key, secret access key, and an optional session token. + These values are read from '~/.aws/credentials'. + " + + read -r -p "Are you ready to create your first Lambda function in Swift? [y/n] " continue + if [[ ! $continue =~ ^[Yy]$ ]]; then + echo "OK, try again later when you feel ready" + exit 1 + fi +} + +create_lambda_execution_role() { + role_name=$1 + + # Allow the Lambda service to assume the IAM role + cat < trust-policy.json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} +EOF + + # Create the IAM role + echo "🔐 Create the IAM role for the Lambda function" + aws iam create-role \ + --role-name "${role_name}" \ + --assume-role-policy-document file://trust-policy.json > /dev/null 2>&1 + + # Attach basic permissions to the role + # The AWSLambdaBasicExecutionRole policy grants permissions to write logs to CloudWatch Logs + echo "🔒 Attach basic permissions to the role" + aws iam attach-role-policy \ + --role-name "${role_name}" \ + --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole > /dev/null 2>&1 + + echo "⏰ Waiting 10 secs for IAM role to propagate..." + sleep 10 +} + +create_swift_project() { + echo "⚡️ Create your Swift Lambda project" + swift package init --type executable --name MyLambda > /dev/null + + echo "📦 Add the AWS Lambda Swift runtime to your project" + # The following commands are commented out until the `lambad-init` plugin will be release + # swift package add-dependency https://github.com/swift-server/swift-aws-lambda-runtime.git --from 2.0.0 + # swift package add-dependency https://github.com/swift-server/swift-aws-lambda-events.git --from 1.0.0 + # swift package add-target-dependency AWSLambdaRuntime MyLambda --package swift-aws-lambda-runtime + # swift package add-target-dependency AWSLambdaEvents MyLambda --package swift-aws-lambda-events + cat < Package.swift +// swift-tools-version:6.2 + +import PackageDescription + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "MyLambda", targets: ["MyLambda"]) + ], + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "2.0.0") + ], + targets: [ + .executableTarget( + name: "MyLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: "." + ) + ] +) +EOF + + echo "📝 Write the Swift code" + # The following command is commented out until the `lambad-init` plugin will be release + # swift package lambda-init --allow-writing-to-package-directory + cat < Sources/main.swift +import AWSLambdaRuntime + +let runtime = LambdaRuntime { + (event: String, context: LambdaContext) in + "Hello \(event)" +} + +try await runtime.run() +EOF + + echo "📦 Compile and package the function for deployment (this might take a while)" + swift package archive --allow-network-connections docker > /dev/null 2>&1 +} + +deploy_lambda_function() { + echo "🚀 Deploy to AWS Lambda" + + # retrieve your AWS Account ID + echo "🔑 Retrieve your AWS Account ID" + AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) + export AWS_ACCOUNT_ID + + # Check if the role already exists + echo "🔍 Check if a Lambda execution IAM role already exists" + aws iam get-role --role-name lambda_basic_execution > /dev/null 2>&1 || create_lambda_execution_role lambda_basic_execution + + # Create the Lambda function + echo "🚀 Create the Lambda function" + aws lambda create-function \ + --function-name MyLambda \ + --zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip \ + --runtime provided.al2 \ + --handler provided \ + --architectures "$(uname -m)" \ + --role arn:aws:iam::"${AWS_ACCOUNT_ID}":role/lambda_basic_execution > /dev/null 2>&1 + + echo "⏰ Waiting 10 secs for the Lambda function to be ready..." + sleep 10 +} + +invoke_lambda_function() { + # Invoke the Lambda function + echo "🔗 Invoke the Lambda function" + aws lambda invoke \ + --function-name MyLambda \ + --cli-binary-format raw-in-base64-out \ + --payload '"Lambda Swift"' \ + output.txt > /dev/null 2>&1 + + echo "👀 Your Lambda function returned:" + cat output.txt && rm output.txt +} + +main() { + # + # Check prerequisites + # + check_prerequisites + + # + # Create the Swift project + # + create_swift_project + + # + # Now the function is ready to be deployed to AWS Lambda + # + deploy_lambda_function + + # + # Invoke the Lambda function + # + invoke_lambda_function + + echo "" + echo "🎉 Done! Your first Lambda function in Swift is now deployed on AWS Lambda. 🚀" +} + +main "$@" \ No newline at end of file diff --git a/NOTICE.txt b/NOTICE.txt index 9631ce9f..0ff96e27 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -27,7 +27,7 @@ components that this product depends on. ------------------------------------------------------------------------------- -This product contains a derivation various scripts from SwiftNIO. +This product contains a derivation various code and scripts from SwiftNIO. * LICENSE (Apache License 2.0): * https://www.apache.org/licenses/LICENSE-2.0 diff --git a/Package.swift b/Package.swift index 1b47e1d0..99ea9667 100644 --- a/Package.swift +++ b/Package.swift @@ -1,71 +1,93 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.2 import PackageDescription +let defaultSwiftSettings: [SwiftSetting] = + [ + .enableExperimentalFeature( + "AvailabilityMacro=LambdaSwift 2.0:macOS 15.0" + ) + ] + let package = Package( name: "swift-aws-lambda-runtime", - platforms: [ - .macOS(.v12), - .iOS(.v15), - .tvOS(.v15), - .watchOS(.v8), - ], products: [ - // 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"]), // plugin to package the lambda, creating an archive that can be uploaded to AWS + // requires Linux or at least macOS v15 .plugin(name: "AWSLambdaPackager", targets: ["AWSLambdaPackager"]), - // 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", .upToNextMajor(from: "2.43.1")), - .package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.4.2")), - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.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.8.0"), ], targets: [ - .target(name: "AWSLambdaRuntime", dependencies: [ - .byName(name: "AWSLambdaRuntimeCore"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOFoundationCompat", package: "swift-nio"), - ]), - .target(name: "AWSLambdaRuntimeCore", dependencies: [ - .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"), - ]), + .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", + condition: .when(traits: ["ServiceLifecycleSupport"]) + ), + ], + swiftSettings: defaultSwiftSettings + ), .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." - ) + 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." + ) + ] ) ), - .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: "NIO", package: "swift-nio"), - ]), - .testTarget(name: "AWSLambdaTestingTests", dependencies: ["AWSLambdaTesting"]), + .testTarget( + name: "AWSLambdaRuntimeTests", + dependencies: [ + .byName(name: "AWSLambdaRuntime"), + .product(name: "NIOTestUtils", package: "swift-nio"), + .product(name: "NIOFoundationCompat", package: "swift-nio"), + ], + swiftSettings: defaultSwiftSettings + ), + // for perf testing - .executableTarget(name: "MockServer", dependencies: [ - .product(name: "NIOHTTP1", package: "swift-nio"), - .product(name: "NIO", package: "swift-nio"), - ]), + .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: defaultSwiftSettings + ), ] ) diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift deleted file mode 100644 index a4559656..00000000 --- a/Package@swift-5.7.swift +++ /dev/null @@ -1,73 +0,0 @@ -// swift-tools-version:5.7 - -import PackageDescription - -let package = Package( - name: "swift-aws-lambda-runtime", - platforms: [ - .macOS(.v12), - .iOS(.v15), - .tvOS(.v15), - .watchOS(.v8), - ], - products: [ - // 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"]), - // plugin to package the lambda, creating an archive that can be uploaded to AWS - .plugin(name: "AWSLambdaPackager", targets: ["AWSLambdaPackager"]), - // for testing only - .library(name: "AWSLambdaTesting", targets: ["AWSLambdaTesting"]), - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.43.1")), - .package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.4.2")), - .package(url: "https://github.com/swift-server/swift-backtrace.git", .upToNextMajor(from: "1.2.3")), - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), - ], - targets: [ - .target(name: "AWSLambdaRuntime", dependencies: [ - .byName(name: "AWSLambdaRuntimeCore"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOFoundationCompat", package: "swift-nio"), - ]), - .target(name: "AWSLambdaRuntimeCore", dependencies: [ - .product(name: "Logging", package: "swift-log"), - .product(name: "Backtrace", package: "swift-backtrace"), - .product(name: "NIOHTTP1", package: "swift-nio"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), - .product(name: "NIOPosix", package: "swift-nio"), - ]), - .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." - ) - ) - ), - .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: "NIO", package: "swift-nio"), - ]), - .testTarget(name: "AWSLambdaTestingTests", dependencies: ["AWSLambdaTesting"]), - // for perf testing - .executableTarget(name: "MockServer", dependencies: [ - .product(name: "NIOHTTP1", package: "swift-nio"), - .product(name: "NIO", package: "swift-nio"), - ]), - ] -) diff --git a/Package@swift-5.8.swift b/Package@swift-5.8.swift deleted file mode 100644 index a4559656..00000000 --- a/Package@swift-5.8.swift +++ /dev/null @@ -1,73 +0,0 @@ -// swift-tools-version:5.7 - -import PackageDescription - -let package = Package( - name: "swift-aws-lambda-runtime", - platforms: [ - .macOS(.v12), - .iOS(.v15), - .tvOS(.v15), - .watchOS(.v8), - ], - products: [ - // 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"]), - // plugin to package the lambda, creating an archive that can be uploaded to AWS - .plugin(name: "AWSLambdaPackager", targets: ["AWSLambdaPackager"]), - // for testing only - .library(name: "AWSLambdaTesting", targets: ["AWSLambdaTesting"]), - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.43.1")), - .package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.4.2")), - .package(url: "https://github.com/swift-server/swift-backtrace.git", .upToNextMajor(from: "1.2.3")), - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), - ], - targets: [ - .target(name: "AWSLambdaRuntime", dependencies: [ - .byName(name: "AWSLambdaRuntimeCore"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOFoundationCompat", package: "swift-nio"), - ]), - .target(name: "AWSLambdaRuntimeCore", dependencies: [ - .product(name: "Logging", package: "swift-log"), - .product(name: "Backtrace", package: "swift-backtrace"), - .product(name: "NIOHTTP1", package: "swift-nio"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), - .product(name: "NIOPosix", package: "swift-nio"), - ]), - .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." - ) - ) - ), - .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: "NIO", package: "swift-nio"), - ]), - .testTarget(name: "AWSLambdaTestingTests", dependencies: ["AWSLambdaTesting"]), - // for perf testing - .executableTarget(name: "MockServer", dependencies: [ - .product(name: "NIOHTTP1", package: "swift-nio"), - .product(name: "NIO", package: "swift-nio"), - ]), - ] -) diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift new file mode 100644 index 00000000..5f4021d4 --- /dev/null +++ b/Package@swift-6.0.swift @@ -0,0 +1,78 @@ +// swift-tools-version:6.0 + +import PackageDescription + +let defaultSwiftSettings: [SwiftSetting] = [ + .define("FoundationJSONSupport"), + .define("ServiceLifecycleSupport"), + .define("LocalServerSupport"), + .enableExperimentalFeature( + "AvailabilityMacro=LambdaSwift 2.0:macOS 15.0" + ), +] + +let package = Package( + name: "swift-aws-lambda-runtime", + products: [ + .library(name: "AWSLambdaRuntime", targets: ["AWSLambdaRuntime"]), + // plugin to package the lambda, creating an archive that can be uploaded to AWS + // requires Linux or at least macOS v15 + .plugin(name: "AWSLambdaPackager", targets: ["AWSLambdaPackager"]), + ], + 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.8.0"), + ], + 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: defaultSwiftSettings + ), + .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." + ) + ] + ) + ), + .testTarget( + name: "AWSLambdaRuntimeTests", + dependencies: [ + .byName(name: "AWSLambdaRuntime"), + .product(name: "NIOTestUtils", package: "swift-nio"), + .product(name: "NIOFoundationCompat", package: "swift-nio"), + ], + swiftSettings: defaultSwiftSettings + ), + // 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: defaultSwiftSettings + ), + ] +) diff --git a/Plugins/AWSLambdaPackager/Plugin.swift b/Plugins/AWSLambdaPackager/Plugin.swift index cd76f5c3..10d9c8a3 100644 --- a/Plugins/AWSLambdaPackager/Plugin.swift +++ b/Plugins/AWSLambdaPackager/Plugin.swift @@ -12,28 +12,32 @@ // //===----------------------------------------------------------------------===// -import Dispatch import Foundation import PackagePlugin -#if canImport(Glibc) -import Glibc -#endif - @main +@available(macOS 15.0, *) struct AWSLambdaPackager: CommandPlugin { func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws { let configuration = try Configuration(context: context, arguments: arguments) + + if configuration.help { + self.displayHelpMessage() + return + } + guard !configuration.products.isEmpty else { throw Errors.unknownProduct("no appropriate products found to package") } if configuration.products.count > 1 && !configuration.explicitProducts { let productNames = configuration.products.map(\.name) - print("No explicit products named, building all executable products: '\(productNames.joined(separator: "', '"))'") + print( + "No explicit products named, building all executable products: '\(productNames.joined(separator: "', '"))'" + ) } - let builtProducts: [LambdaProduct: Path] + let builtProducts: [LambdaProduct: URL] if self.isAmazonLinux2() { // build directly on the machine builtProducts = try self.build( @@ -46,9 +50,9 @@ struct AWSLambdaPackager: CommandPlugin { // build with docker builtProducts = try self.buildInDocker( packageIdentity: context.package.id, - packageDirectory: context.package.directory, + packageDirectory: context.package.directoryURL, products: configuration.products, - toolsProvider: { name in try context.tool(named: name).path }, + toolsProvider: { name in try context.tool(named: name).url }, outputDirectory: configuration.outputDirectory, baseImage: configuration.baseDockerImage, disableDockerImageUpdate: configuration.disableDockerImageUpdate, @@ -59,29 +63,32 @@ struct AWSLambdaPackager: CommandPlugin { // create the archive let archives = try self.package( + packageName: context.package.displayName, products: builtProducts, - toolsProvider: { name in try context.tool(named: name).path }, + toolsProvider: { name in try context.tool(named: name).url }, outputDirectory: configuration.outputDirectory, verboseLogging: configuration.verboseLogging ) - print("\(archives.count > 0 ? archives.count.description : "no") archive\(archives.count != 1 ? "s" : "") created") + print( + "\(archives.count > 0 ? archives.count.description : "no") archive\(archives.count != 1 ? "s" : "") created" + ) for (product, archivePath) in archives { - print(" * \(product.name) at \(archivePath.string)") + print(" * \(product.name) at \(archivePath.path())") } } private func buildInDocker( packageIdentity: Package.ID, - packageDirectory: Path, + packageDirectory: URL, products: [Product], - toolsProvider: (String) throws -> Path, - outputDirectory: Path, + toolsProvider: (String) throws -> URL, + outputDirectory: URL, baseImage: String, disableDockerImageUpdate: Bool, buildConfiguration: PackageManager.BuildConfiguration, verboseLogging: Bool - ) throws -> [LambdaProduct: Path] { + ) throws -> [LambdaProduct: URL] { let dockerToolPath = try toolsProvider("docker") print("-------------------------------------------------------------------------") @@ -91,38 +98,64 @@ struct AWSLambdaPackager: CommandPlugin { if !disableDockerImageUpdate { // update the underlying docker image, if necessary print("updating \"\(baseImage)\" docker image") - try self.execute( + try Utils.execute( executable: dockerToolPath, arguments: ["pull", baseImage], - logLevel: .output + logLevel: verboseLogging ? .debug : .output ) } // get the build output path let buildOutputPathCommand = "swift build -c \(buildConfiguration.rawValue) --show-bin-path" - let dockerBuildOutputPath = try self.execute( + let dockerBuildOutputPath = try Utils.execute( executable: dockerToolPath, - arguments: ["run", "--rm", "-v", "\(packageDirectory.string):/workspace", "-w", "/workspace", baseImage, "bash", "-cl", buildOutputPathCommand], + arguments: [ + "run", "--rm", "-v", "\(packageDirectory.path()):/workspace", "-w", "/workspace", baseImage, "bash", + "-cl", buildOutputPathCommand, + ], logLevel: verboseLogging ? .debug : .silent ) guard let buildPathOutput = dockerBuildOutputPath.split(separator: "\n").last else { throw Errors.failedParsingDockerOutput(dockerBuildOutputPath) } - let buildOutputPath = Path(buildPathOutput.replacingOccurrences(of: "/workspace", with: packageDirectory.string)) + let buildOutputPath = URL( + string: buildPathOutput.replacingOccurrences(of: "/workspace/", with: packageDirectory.description) + )! // build the products - var builtProducts = [LambdaProduct: Path]() + var builtProducts = [LambdaProduct: URL]() for product in products { print("building \"\(product.name)\"") - let buildCommand = "swift build -c \(buildConfiguration.rawValue) --product \(product.name) --static-swift-stdlib" - try self.execute( - executable: dockerToolPath, - arguments: ["run", "--rm", "-v", "\(packageDirectory.string):/workspace", "-w", "/workspace", baseImage, "bash", "-cl", buildCommand], - logLevel: verboseLogging ? .debug : .output - ) - let productPath = buildOutputPath.appending(product.name) - guard FileManager.default.fileExists(atPath: productPath.string) else { - Diagnostics.error("expected '\(product.name)' binary at \"\(productPath.string)\"") + let buildCommand = + "swift build -c \(buildConfiguration.rawValue) --product \(product.name) --static-swift-stdlib" + if let localPath = ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"] { + // when developing locally, we must have the full swift-aws-lambda-runtime project in the container + // because Examples' Package.swift have a dependency on ../.. + // just like Package.swift's examples assume ../.., we assume we are two levels below the root project + let slice = packageDirectory.pathComponents.suffix(2) + try Utils.execute( + executable: dockerToolPath, + arguments: [ + "run", "--rm", "--env", "LAMBDA_USE_LOCAL_DEPS=\(localPath)", "-v", + "\(packageDirectory.path())../..:/workspace", "-w", + "/workspace/\(slice.joined(separator: "/"))", baseImage, "bash", "-cl", buildCommand, + ], + logLevel: verboseLogging ? .debug : .output + ) + } else { + try Utils.execute( + executable: dockerToolPath, + arguments: [ + "run", "--rm", "-v", "\(packageDirectory.path()):/workspace", "-w", "/workspace", baseImage, + "bash", "-cl", buildCommand, + ], + logLevel: verboseLogging ? .debug : .output + ) + } + let productPath = buildOutputPath.appending(path: product.name) + + guard FileManager.default.fileExists(atPath: productPath.path()) else { + Diagnostics.error("expected '\(product.name)' binary at \"\(productPath.path())\"") throw Errors.productExecutableNotFound(product.name) } builtProducts[.init(product)] = productPath @@ -135,12 +168,12 @@ struct AWSLambdaPackager: CommandPlugin { products: [Product], buildConfiguration: PackageManager.BuildConfiguration, verboseLogging: Bool - ) throws -> [LambdaProduct: Path] { + ) throws -> [LambdaProduct: URL] { print("-------------------------------------------------------------------------") print("building \"\(packageIdentity)\"") print("-------------------------------------------------------------------------") - var results = [LambdaProduct: Path]() + var results = [LambdaProduct: URL]() for product in products { print("building \"\(product.name)\"") var parameters = PackageManager.BuildParameters() @@ -155,50 +188,98 @@ struct AWSLambdaPackager: CommandPlugin { guard let artifact = result.executableArtifact(for: product) else { throw Errors.productExecutableNotFound(product.name) } - results[.init(product)] = artifact.path + results[.init(product)] = artifact.url } return results } // TODO: explore using ziplib or similar instead of shelling out private func package( - products: [LambdaProduct: Path], - toolsProvider: (String) throws -> Path, - outputDirectory: Path, + packageName: String, + products: [LambdaProduct: URL], + toolsProvider: (String) throws -> URL, + outputDirectory: URL, verboseLogging: Bool - ) throws -> [LambdaProduct: Path] { + ) throws -> [LambdaProduct: URL] { let zipToolPath = try toolsProvider("zip") - var archives = [LambdaProduct: Path]() + var archives = [LambdaProduct: URL]() for (product, artifactPath) in products { print("-------------------------------------------------------------------------") print("archiving \"\(product.name)\"") print("-------------------------------------------------------------------------") // prep zipfile location - let workingDirectory = outputDirectory.appending(product.name) - let zipfilePath = workingDirectory.appending("\(product.name).zip") - if FileManager.default.fileExists(atPath: workingDirectory.string) { - try FileManager.default.removeItem(atPath: workingDirectory.string) + let workingDirectory = outputDirectory.appending(path: product.name) + let zipfilePath = workingDirectory.appending(path: "\(product.name).zip") + if FileManager.default.fileExists(atPath: workingDirectory.path()) { + try FileManager.default.removeItem(atPath: workingDirectory.path()) } - try FileManager.default.createDirectory(atPath: workingDirectory.string, withIntermediateDirectories: true) + try FileManager.default.createDirectory(atPath: workingDirectory.path(), withIntermediateDirectories: true) // rename artifact to "bootstrap" - let relocatedArtifactPath = workingDirectory.appending(artifactPath.lastComponent) - let symbolicLinkPath = workingDirectory.appending("bootstrap") - try FileManager.default.copyItem(atPath: artifactPath.string, toPath: relocatedArtifactPath.string) - try FileManager.default.createSymbolicLink(atPath: symbolicLinkPath.string, withDestinationPath: relocatedArtifactPath.lastComponent) + let relocatedArtifactPath = workingDirectory.appending(path: "bootstrap") + try FileManager.default.copyItem(atPath: artifactPath.path(), toPath: relocatedArtifactPath.path()) + var arguments: [String] = [] #if os(macOS) || os(Linux) - let arguments = ["--junk-paths", "--symlinks", zipfilePath.string, relocatedArtifactPath.string, symbolicLinkPath.string] + arguments = [ + "--recurse-paths", + "--symlinks", + zipfilePath.lastPathComponent, + relocatedArtifactPath.lastPathComponent, + ] #else - throw Error.unsupportedPlatform("can't or don't know how to create a zip file on this platform") + throw Errors.unsupportedPlatform("can't or don't know how to create a zip file on this platform") #endif + // add resources + var artifactPathComponents = artifactPath.pathComponents + _ = artifactPathComponents.removeFirst() // Get rid of beginning "/" + _ = artifactPathComponents.removeLast() // Get rid of the name of the package + let artifactDirectory = "/\(artifactPathComponents.joined(separator: "/"))" + for fileInArtifactDirectory in try FileManager.default.contentsOfDirectory(atPath: artifactDirectory) { + guard let artifactURL = URL(string: "\(artifactDirectory)/\(fileInArtifactDirectory)") else { + continue + } + + guard artifactURL.pathExtension == "resources" else { + continue // Not resources, so don't copy + } + let resourcesDirectoryName = artifactURL.lastPathComponent + let relocatedResourcesDirectory = workingDirectory.appending(path: resourcesDirectoryName) + if FileManager.default.fileExists(atPath: artifactURL.path()) { + do { + arguments.append(resourcesDirectoryName) + try FileManager.default.copyItem( + atPath: artifactURL.path(), + toPath: relocatedResourcesDirectory.path() + ) + } 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 + } + } + } + // run the zip tool - try self.execute( + try Utils.execute( executable: zipToolPath, arguments: arguments, + customWorkingDirectory: workingDirectory, logLevel: verboseLogging ? .debug : .silent ) @@ -207,88 +288,58 @@ struct AWSLambdaPackager: CommandPlugin { return archives } - @discardableResult - private func execute( - executable: Path, - arguments: [String], - customWorkingDirectory: Path? = .none, - logLevel: ProcessLogLevel - ) throws -> String { - if logLevel >= .debug { - print("\(executable.string) \(arguments.joined(separator: " "))") - } - - var output = "" - let outputSync = DispatchGroup() - let outputQueue = DispatchQueue(label: "AWSLambdaPackager.output") - let outputHandler = { (data: Data?) in - dispatchPrecondition(condition: .onQueue(outputQueue)) - - outputSync.enter() - defer { outputSync.leave() } - - guard let _output = data.flatMap({ String(data: $0, encoding: .utf8)?.trimmingCharacters(in: CharacterSet(["\n"])) }), !_output.isEmpty else { - return - } - - output += _output + "\n" - - switch logLevel { - case .silent: - break - case .debug(let outputIndent), .output(let outputIndent): - print(String(repeating: " ", count: outputIndent), terminator: "") - print(_output) - fflush(stdout) - } - } - - let pipe = Pipe() - pipe.fileHandleForReading.readabilityHandler = { fileHandle in outputQueue.async { outputHandler(fileHandle.availableData) } } - - let process = Process() - process.standardOutput = pipe - process.standardError = pipe - process.executableURL = URL(fileURLWithPath: executable.string) - process.arguments = arguments - if let workingDirectory = customWorkingDirectory { - process.currentDirectoryURL = URL(fileURLWithPath: workingDirectory.string) - } - process.terminationHandler = { _ in - outputQueue.async { - outputHandler(try? pipe.fileHandleForReading.readToEnd()) - } - } - - try process.run() - process.waitUntilExit() - - // wait for output to be full processed - outputSync.wait() - - if process.terminationStatus != 0 { - // print output on failure and if not already printed - if logLevel < .output { - print(output) - fflush(stdout) - } - throw Errors.processFailed([executable.string] + arguments, process.terminationStatus) - } - - return output - } - private func isAmazonLinux2() -> Bool { - if let data = FileManager.default.contents(atPath: "/etc/system-release"), let release = String(data: data, encoding: .utf8) { + if let data = FileManager.default.contents(atPath: "/etc/system-release"), + let release = String(data: data, encoding: .utf8) + { return release.hasPrefix("Amazon Linux release 2") } else { return false } } + + private func displayHelpMessage() { + print( + """ + OVERVIEW: A SwiftPM plugin to build and package your lambda function. + + REQUIREMENTS: To use this plugin, you must have docker installed and started. + + USAGE: swift package --allow-network-connections docker archive + [--help] [--verbose] + [--output-path ] + [--products ] + [--configuration debug | release] + [--swift-version ] + [--base-docker-image ] + [--disable-docker-image-update] + + + OPTIONS: + --verbose Produce verbose output for debugging. + --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) + --configuration The build configuration (debug or release) + (default is release) + --swift-version The swift version to use for building. + (default is latest) + This parameter cannot be used when --base-docker-image is specified. + --base-docker-image The name of the base docker image to use for the build. + (default : swift-:amazonlinux2) + This parameter cannot be used when --swift-version is specified. + --disable-docker-image-update Do not attempt to update the docker image + --help Show help information. + """ + ) + } } +@available(macOS 15.0, *) private struct Configuration: CustomStringConvertible { - public let outputDirectory: Path + public let help: Bool + public let outputDirectory: URL public let products: [Product] public let explicitProducts: Bool public let buildConfiguration: PackageManager.BuildConfiguration @@ -308,17 +359,27 @@ private struct Configuration: CustomStringConvertible { let swiftVersionArgument = argumentExtractor.extractOption(named: "swift-version") let baseDockerImageArgument = argumentExtractor.extractOption(named: "base-docker-image") let disableDockerImageUpdateArgument = argumentExtractor.extractFlag(named: "disable-docker-image-update") > 0 + let helpArgument = argumentExtractor.extractFlag(named: "help") > 0 + + // help required ? + self.help = helpArgument + // verbose logging required ? self.verboseLogging = verboseArgument if let outputPath = outputPathArgument.first { + #if os(Linux) + var isDirectory: Bool = false + #else var isDirectory: ObjCBool = false - guard FileManager.default.fileExists(atPath: outputPath, isDirectory: &isDirectory), isDirectory.boolValue else { + #endif + guard FileManager.default.fileExists(atPath: outputPath, isDirectory: &isDirectory) + else { throw Errors.invalidArgument("invalid output directory '\(outputPath)'") } - self.outputDirectory = Path(outputPath) + self.outputDirectory = URL(string: outputPath)! } else { - self.outputDirectory = context.pluginWorkDirectory.appending(subpath: "\(AWSLambdaPackager.self)") + self.outputDirectory = context.pluginWorkDirectoryURL.appending(path: "\(AWSLambdaPackager.self)") } self.explicitProducts = !productsArgument.isEmpty @@ -348,8 +409,9 @@ private struct Configuration: CustomStringConvertible { throw Errors.invalidArgument("--swift-version and --base-docker-image are mutually exclusive") } - let swiftVersion = swiftVersionArgument.first ?? .none // undefined version will yield the latest docker image - self.baseDockerImage = baseDockerImageArgument.first ?? "swift:\(swiftVersion.map { $0 + "-" } ?? "")amazonlinux2" + let swiftVersion = swiftVersionArgument.first ?? .none // undefined version will yield the latest docker image + self.baseDockerImage = + baseDockerImageArgument.first ?? "swift:\(swiftVersion.map { $0 + "-" } ?? "")amazonlinux2" self.disableDockerImageUpdate = disableDockerImageUpdateArgument @@ -455,7 +517,9 @@ private struct LambdaProduct: Hashable { extension PackageManager.BuildResult { // find the executable produced by the build func executableArtifact(for product: Product) -> PackageManager.BuildResult.BuiltArtifact? { - let executables = self.builtArtifacts.filter { $0.kind == .executable && $0.path.lastComponent == product.name } + let executables = self.builtArtifacts.filter { + $0.kind == .executable && $0.url.lastPathComponent == product.name + } guard !executables.isEmpty else { return nil } diff --git a/Plugins/AWSLambdaPackager/PluginUtils.swift b/Plugins/AWSLambdaPackager/PluginUtils.swift new file mode 100644 index 00000000..f4e8cb02 --- /dev/null +++ b/Plugins/AWSLambdaPackager/PluginUtils.swift @@ -0,0 +1,159 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 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 +// +//===----------------------------------------------------------------------===// + +import Dispatch +import Foundation +import PackagePlugin +import Synchronization + +@available(macOS 15.0, *) +struct Utils { + @discardableResult + static func execute( + executable: URL, + arguments: [String], + customWorkingDirectory: URL? = .none, + logLevel: ProcessLogLevel + ) throws -> String { + if logLevel >= .debug { + print("\(executable.path()) \(arguments.joined(separator: " "))") + if let customWorkingDirectory { + print("Working directory: \(customWorkingDirectory.path())") + } + } + + let fd = dup(1) + let stdout = fdopen(fd, "rw") + defer { if let so = stdout { fclose(so) } } + + // We need to use an unsafe transfer here to get the fd into our Sendable closure. + // This transfer is fine, because we write to the variable from a single SerialDispatchQueue here. + // We wait until the process is run below process.waitUntilExit(). + // This means no further writes to output will happen. + // This makes it save for us to read the output + struct UnsafeTransfer: @unchecked Sendable { + let value: Value + } + + let outputMutex = Mutex("") + let outputSync = DispatchGroup() + let outputQueue = DispatchQueue(label: "AWSLambdaPackager.output") + let unsafeTransfer = UnsafeTransfer(value: stdout) + let outputHandler = { @Sendable (data: Data?) in + dispatchPrecondition(condition: .onQueue(outputQueue)) + + outputSync.enter() + defer { outputSync.leave() } + + guard + let _output = data.flatMap({ + String(data: $0, encoding: .utf8)?.trimmingCharacters(in: CharacterSet(["\n"])) + }), !_output.isEmpty + else { + return + } + + outputMutex.withLock { output in + output += _output + "\n" + } + + switch logLevel { + case .silent: + break + case .debug(let outputIndent), .output(let outputIndent): + print(String(repeating: " ", count: outputIndent), terminator: "") + print(_output) + fflush(unsafeTransfer.value) + } + } + + let pipe = Pipe() + pipe.fileHandleForReading.readabilityHandler = { fileHandle in + outputQueue.async { outputHandler(fileHandle.availableData) } + } + + let process = Process() + process.standardOutput = pipe + process.standardError = pipe + process.executableURL = executable + process.arguments = arguments + if let customWorkingDirectory { + process.currentDirectoryURL = URL(fileURLWithPath: customWorkingDirectory.path()) + } + process.terminationHandler = { _ in + outputQueue.async { + outputHandler(try? pipe.fileHandleForReading.readToEnd()) + } + } + + try process.run() + process.waitUntilExit() + + // wait for output to be full processed + outputSync.wait() + + let output = outputMutex.withLock { $0 } + + if process.terminationStatus != 0 { + // print output on failure and if not already printed + if logLevel < .output { + print(output) + fflush(stdout) + } + throw ProcessError.processFailed([executable.path()] + arguments, process.terminationStatus) + } + + return output + } + + enum ProcessError: Error, CustomStringConvertible { + case processFailed([String], Int32) + + var description: String { + switch self { + case .processFailed(let arguments, let code): + return "\(arguments.joined(separator: " ")) failed with code \(code)" + } + } + } + + enum ProcessLogLevel: Comparable { + case silent + case output(outputIndent: Int) + case debug(outputIndent: Int) + + var naturalOrder: Int { + switch self { + case .silent: + return 0 + case .output: + return 1 + case .debug: + return 2 + } + } + + static var output: Self { + .output(outputIndent: 2) + } + + static var debug: Self { + .debug(outputIndent: 2) + } + + static func < (lhs: ProcessLogLevel, rhs: ProcessLogLevel) -> Bool { + lhs.naturalOrder < rhs.naturalOrder + } + } +} diff --git a/Plugins/Documentation.docc/Proposals/0001-v2-plugins.md b/Plugins/Documentation.docc/Proposals/0001-v2-plugins.md new file mode 100644 index 00000000..5b9514f2 --- /dev/null +++ b/Plugins/Documentation.docc/Proposals/0001-v2-plugins.md @@ -0,0 +1,531 @@ +# v2 Plugin Proposal for swift-aws-lambda-runtime + +`swift-aws-lambda-runtime` is a library for the Swift on Server ecosystem. The initial version of the library focused on the API, enabling developers to write Lambda functions in the Swift programming language. The library provided developers with basic support for building and packaging their functions. + +We believe it is time to consider the end-to-end developer experience, from project scaffolding to deployment, taking into account the needs of Swift developers that are new to AWS and Lambda. + +This document describes a proposal for the v2 plugins for `swift-aws-lambda-runtime`. The plugins will focus on project scaffolding, building, archiving, and deployment of Lambda functions. + +## Overview + +Versions: + +* v1 (2024-12-25): Initial version +* v2 (2025-03-13): +- Include [comments from the community](https://forums.swift.org/t/lambda-plugins-for-v2/76859). +- [init] Add the templates for `main.swift` +- [build] Add the section **Cross-compiling options** +- [deploy] Add details about locating AWS Credentials. +- [deploy] Add `--input-path` parameter. +- [deploy] Add details how the function name is computed. +- [deploy] Add `--architecture` option and details how the default is computed. + +## Motivation + +The current version of `swift-aws-lambda-runtime` provides a solid foundation for Swift developers to write Lambda functions. However, the developer experience can be improved. For example, the current version does not provide any support for project scaffolding or deployment of Lambda functions. + +This creates a high barrier to entry for Swift developers new to AWS and Lambda, as well as for AWS professionals learning Swift. We propose to lower this barrier by providing a set of plugins that will assist developers in creating, building, packaging, and deploying Lambda functions. + +As a source of inspiration, we looked at the Rust community, which created Cargo-Lambda ([https://www.cargo-lambda.info/guide/what-is-cargo-lambda.html](https://www.cargo-lambda.info/guide/what-is-cargo-lambda.html)). Cargo-Lambda helps developers deploy Rust Lambda functions. We aim to provide a similar experience for Swift developers. + +### Current Limitations + +The current version of the `archive` plugin support the following tasks: + +* The cross-compilation using Docker. +* The archiving of the Lambda function and it's resources as a ZIP file. + +The current version of `swift-aws-lambda-runtime` does not provide support for project **scaffolding** or **deployment** of Lambda functions. This makes it difficult for Swift developers new to AWS and Lambda, or AWS Professionals new to Swift, to get started. + +### New Plugins + +To address the limitations of the `archive` plugin, we propose creating three new plugins: + +* `lambda-init`: This plugin will assist developers in creating a new Lambda project from scratch by scaffolding the project structure and its dependencies. + +* `lambda-build`: This plugin will help developers build and package their Lambda function (similar to the current `archive` plugin). This plugin will allow for multiple cross-compilation options. We will retain the current Docker-based cross-compilation but will also provide a way to cross-compile without Docker, such as using the [Swift Static Linux SDK](https://www.swift.org/documentation/articles/static-linux-getting-started.html) (with musl) or a (non-existent at the time of this writing) custom Swift SDK for Amazon Linux (built with the [Custom SDK Generator](https://github.com/swiftlang/swift-sdk-generator)). The plugin will also provide an option to package the binary as a ZIP file or as a Docker image. + +* `lambda-deploy`: This plugin will assist developers in deploying their Lambda function to AWS. This plugin will handle the deployment of the Lambda function, including the creation of the IAM role, the creation of the Lambda function, and the optional configuration of a Lambda function URL. + +We may consider additional plugins in a future release. For example, we could consider a plugin to help developers invoke their Lambda function (`lambda-invoke`) or to monitor CloudWatch logs (`lambda-logs`). + +## Detailed Solution + +The proposed solution consists of three new plugins: `lambda-init`, `lambda-build`, and `lambda-deploy`. These plugins will assist developers in creating, building, packaging, and deploying Lambda functions. + +### Create a New Project (lambda-init) + +The `lambda-init` plugin will assist developers in creating a new Lambda project from scratch. The plugin will scaffold the project code. It will create a ready-to-build `main.swift` file containing a simple Lambda function. The plugin will allow users to choose from a selection of basic templates, such as a simple "Hello World" Lambda function or a function invoked by a URL. + +The plugin cannot be invoked without the required dependency on `swift-aws-lambda-runtime` project being configured in `Package.swift`. The process of creating a new project will consist of three steps and four commands, all executable from the command line: + +```bash +# Step 1: Create a new Swift executable package +swift package init --type executable --name MyLambda + +# Step 2: Add the Swift AWS Lambda Runtime dependency +swift package add-dependency https://github.com/swift-server/swift-aws-lambda-runtime.git --branch main +swift package add-target-dependency AWSLambdaRuntime MyLambda --package swift-aws-lambda-runtime + +# Step 3: Call the lambda-init plugin +swift package lambda-init +``` + +The plugin will offer the following options: + +```text +OVERVIEW: A SwiftPM plugin to scaffold a Hello World Lambda function. + + By default, it creates a Lambda function that receives a JSON document and responds with another JSON document. + +USAGE: swift package lambda-init + [--help] [--verbose] + [--with-url] + [--allow-writing-to-package-directory] + +OPTIONS: +--with-url Create a Lambda function exposed by a URL +--allow-writing-to-package-directory Don't ask for permission to write files. +--verbose Produce verbose output for debugging. +--help Show help information. +``` + +The initial implementation will use hardcoded templates. In a future release, we might consider fetching the templates from a GitHub repository and allowing developers to create custom templates. + +The default templates are currently implemented in the [sebsto/new-plugins branch of this repo](https://github.com/sebsto/swift-aws-lambda-runtime/blob/sebsto/new-plugins/Sources/AWSLambdaPluginHelper/lambda-init/Template.swift). + +### Default template + +```swift +import AWSLambdaRuntime + +// the data structure to represent the input parameter +struct HelloRequest: Decodable { + let name: String + let age: Int +} + +// the data structure to represent the output response +struct HelloResponse: Encodable { + let greetings: String +} + +// in this example we receive a HelloRequest JSON and we return a HelloResponse JSON + +// the Lambda runtime +let runtime = LambdaRuntime { + (event: HelloRequest, context: LambdaContext) in + + HelloResponse( + greetings: "Hello \(event.name). You look \(event.age > 30 ? "younger" : "older") than your age." + ) +} + +// start the loop +try await runtime.run() +``` + +### URL Template + +```swift +import AWSLambdaRuntime +import AWSLambdaEvents + +// in this example we receive a FunctionURLRequest and we return a FunctionURLResponse +// https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads + +let runtime = LambdaRuntime { + (event: FunctionURLRequest, context: LambdaContext) -> FunctionURLResponse in + + guard let name = event.queryStringParameters?["name"] else { + return FunctionURLResponse(statusCode: .badRequest) + } + + return FunctionURLResponse(statusCode: .ok, body: #"{ "message" : "Hello \#\#(name)" } "#) +} + +try await runtime.run() +``` + +### Build and Package (lambda-build) + +The `lambda-build` plugin will assist developers in building and packaging their Lambda function. It will allow for multiple cross-compilation options. We will retain the current Docker-based cross-compilation but also provide a way to cross-compile without Docker, such as using the Swift Static Linux SDK (with musl) or a custom Swift SDK for Amazon Linux. + +We also propose to automatically strip the binary of debug symbols (`-Xlinker -s`) to reduce the size of the ZIP file. Our tests showed that this can reduce the size by up to 50%. An option to disable stripping will be provided. + +The `lambda-build` plugin is similar to the existing `archive` plugin. We propose to keep the same interface to facilitate migration of existing projects and CI chains. If technically feasible, we will also consider keeping the `archive` plugin as an alias to the `lambda-build` plugin. + +The plugin interface is based on the existing `archive` plugin, with the addition of the `--no-strip` and `--cross-compile` options: + +```text +OVERVIEW: A SwiftPM plugin to build and package your Lambda function. + +REQUIREMENTS: To use this plugin, Docker must be installed and running. + +USAGE: swift package archive + [--help] [--verbose] + [--output-directory ] + [--products ] + [--configuration debug | release] + [--swift-version ] + [--base-docker-image ] + [--disable-docker-image-update] + [--no-strip] + [--cross-compile ] + [--allow-network-connections docker] + +OPTIONS: +--output-directory The path of the binary package. + (default: .build/plugins/AWSLambdaPackager/outputs/...) +--products The list of executable targets to build. + (default: taken from Package.swift) +--configuration The build configuration (debug or release) + (default: release) +--swift-version The Swift version to use for building. + (default: latest) + This parameter cannot be used with --base-docker-image. +--base-docker-image The name of the base Docker image to use for the build. + (default: swift-:amazonlinux2) + This parameter cannot be used with --swift-version. + This parameter cannot be used with a value other than Docker provided to --cross-compile. +--disable-docker-image-update Do not update the Docker image before building. +--no-strip Do not strip the binary of debug symbols. +--cross-compile Cross-compile the binary using the specified method. + (default: docker) Accepted values are: docker, swift-static-sdk, custom-sdk +``` + +#### Cross compiling options + +We propose to release an initial version based on the current `archive` plugin implementation, which uses docker. But for the future, we would like to explore the possibility to cross compile with a custom Swift SDK for Amazon Linux. Our [initial tests](https://github.com/swiftlang/swift-sdk-generator/issues/138#issuecomment-2719540021) demonstrated it is possible to build such an SDK using the Swift SDK Generator project. + +For an ideal developer experience, we would imagine the following sequence: + +- developer runs `swift package build --cross-compile custom-sdk` +- the plugin checks if the custom sdk is installed on the machine (`swift sdk list`) [questions : is it possible to call `swift` from a package ? Should we check the file systems instead ? Should this work on multiple OSes, such as macOS and Linux? ] +- if not installed or outdated, the plugin downloads a custom SDK from a safe source and installs it [questions : who should maintain such SDK binaries? Where to host them? We must have a kind of signature to ensure the SDK has not been modified. How to manage Swift version and align with the local toolchain?] +- the plugin build the archive using the custom sdk + +### Deploy (lambda-deploy) + +The `lambda-deploy` plugin will assist developers in deploying their Lambda function to AWS. It will handle the deployment process, including creating the IAM role, the Lambda function itself, and optionally configuring a Lambda function URL. + +The plugin will not depends on nay third-party library. It will interact directly with the AWS REST API, without using the AWS SDK fro Swift or Soto. + +Users will need to provide AWS access key and secret access key credentials. The plugin will attempt to locate these credentials in standard locations. It will first check for the `~/.aws/credentials` file, then the environment variables `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and (optional) `AWS_SESSION_TOKEN`. Finally, it will check the [meta data service v2](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html) in case the plugin runs from a virtual machine (Amazon EC2) or a container (Amazon ECS or AMazon EKS). + +The plugin supports deployment through either the REST and Base64 payload or by uploading the code to a temporary S3 bucket. Refer to [the `Function Code` section](https://docs.aws.amazon.com/lambda/latest/api/API_FunctionCode.html) of the [CreateFunction](https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html) API for more details. + +The plugin will use teh function name as defined in the `executableTarget` in `Package.swift`. This approach is similar to how the `archive` plugin works today. + +The plugin can deploy to multiple regions. Users can specify the desired region as a command-line argument. + +In addition to deploying the Lambda function, the plugin can also create an IAM role for it. Users can specify the IAM role name as a command-line argument. If no role name is provided, the plugin will create a new IAM role with the necessary permissions for the Lambda function. + +The plugin allows developers to update the code for an existing Lambda function. The update command remains the same as for initial deployment. The plugin will detect whether the function already exists and update the code accordingly. + +Finally, the plugin can help developers delete a Lambda function and its associated IAM role. + +An initial version of this plugin might look like this: + +```text +""" +OVERVIEW: A SwiftPM plugin to deploy a Lambda function. + +USAGE: swift package lambda-deploy + [--with-url] + [--region ] + [--iam-role ] + [--delete] + [--help] [--verbose] + +OPTIONS: +--region The AWS region to deploy the Lambda function to. + (default is us-east-1) +--iam-role The name of the IAM role to use for the Lambda function. + when none is provided, a new role will be created. +--input-directory The path of the binary package (zip file) to deploy + (default: .build/plugins/AWSLambdaPackager/outputs/...) +--architecture x64 | arm64 The target architecture of the Lambda function + (default: the architecture of the machine where the plugin runs) +--with-url Add an URL to access the Lambda function +--delete Delete the Lambda function and its associated IAM role +--verbose Produce verbose output for debugging. +--help Show help information. +""" +``` + +In a future version, we might consider adding an `--export` option that would easily migrate the current deployment to an infrastructure as code (IaC) tool, such as AWS SAM, AWS CDK, or Swift Cloud. + +### Dependencies + +One of the design objective of the Swift AWS Lambda Runtime is to minimize its dependencies on other libraries. + +Therefore, minimizing dependencies is a key priority for the new plugins. We aim to avoid including unnecessary dependencies, such as the AWS SDK for Swift or Soto, for the `lambda-deploy` plugin. + +Four essential dependencies have been identified to implement the plugins: + +* an command line argument parser +* an HTTP client +* a library to sign AWS requests +* a library to calculate HMAC-SHA256 (used in the AWS signing process) + +These functionalities can be incorporated by vending source code from other projects. We will consider the following options: + +**Argument Parser:** + +* We propose to leverage the `ArgumentExtractor` from the `swift-package-manager` project ([https://github.com/swiftlang/swift-package-manager/blob/main/Sources/PackagePlugin/ArgumentExtractor.swift](https://github.com/swiftlang/swift-package-manager/blob/main/Sources/PackagePlugin/ArgumentExtractor.swift)). This is a simple argument parser used by the Swift Package Manager. The relevant files will be copied into the plugin. + +**HTTP Client:** + +* We will utilize the `URLSession` provided by `FoundationNetworking`. No additional dependencies will be introduced for the HTTP client. + +**AWS Request Signing:** + +* To interact with the AWS REST API, requests must be signed. We will include the `AWSRequestSigner` from [the `aws-signer-v4` project](https://github.com/adam-fowler/aws-signer-v4). This is a simple library that signs requests using AWS Signature Version 4. The relevant files will be copied into the plugin. + +**HMAC-SHA256 Implementation:** + +* The `AWSRequestSigner` has a dependency on the `swift-crypto` library. We will consider two options: + * Include the HMAC-SHA256 implementation from the popular `CryptoSwift` library ([https://github.com/krzyzanowskim/CryptoSwift](https://github.com/krzyzanowskim/CryptoSwift)), which provides a wide range of cryptographic functions. The relevant files will be copied into the plugin. + * Develop a clean implementation of the HMAC-SHA256 algorithm. This is a relatively simple algorithm used for request signing. + +The dependencies will be vendored within the plugin and will not be listed as dependencies in the `Package.swift` file. + +If we follow that plan, the following files will be copied into the plugin, without modifications from their original projects: + +```text +Sources/AWSLambdaPluginHelper/Vendored +├── crypto +│ ├── Array+Extensions.swift +│ ├── Authenticator.swift +│ ├── BatchedCollections.swift +│ ├── Bit.swift +│ ├── Collections+Extensions.swift +│ ├── Digest.swift +│ ├── DigestType.swift +│ ├── Generics.swift +│ ├── HMAC.swift +│ ├── Int+Extension.swift +│ ├── NoPadding.swift +│ ├── Padding.swift +│ ├── SHA1.swift +│ ├── SHA2.swift +│ ├── SHA3.swift +│ ├── UInt16+Extension.swift +│ ├── UInt32+Extension.swift +│ ├── UInt64+Extension.swift +│ ├── UInt8+Extension.swift +│ ├── Updatable.swift +│ ├── Utils.swift +│ └── ZeroPadding.swift +├── signer +│ ├── AWSCredentials.swift +│ └── AWSSigner.swift +└── spm + └── ArgumentExtractor.swift +``` + +### Implementation + +SwiftPM plugins from a same project can not share code in between sources files or using a shared Library target. The recommended way to share code between plugins is to create an executable target to implement the plugin functionalities and to implement the plugin as a thin wrapper that invokes the executable target. + +We propose to add an executable target and three plugins to the `Package.swift` file of the `swift-aws-lambda-runtime` package. + +```swift +let package = Package( + name: "swift-aws-lambda-runtime", + platforms: [.macOS(.v15)], + products: [ + + // + // The runtime library targets + // + + // ... unchanged ... + + // + // 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"]), + + // + // Testing targets + // + // ... unchanged ... + ], + dependencies: [ // unchanged + .package(url: "https://github.com/apple/swift-nio.git", from: "2.76.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), + ], + targets: [ + + // library target, unchanged + // .... + + // + // The plugins targets + // + .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") + ] + ), + + /// The executable target that implements the three plugins functionality + .executableTarget( + name: "AWSLambdaPluginHelper", + dependencies: [ + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "NIOCore", package: "swift-nio"), + ], + swiftSettings: [.swiftLanguageMode(.v6)] + ), + + // remaining targets, unchanged + ] +) + +``` + +A plugin would be a thin wrapper around the executable target. For example: + +```swift +struct AWSLambdaInitializer: CommandPlugin { + + func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws { + let tool = try context.tool(named: "AWSLambdaPluginHelper") + + let args = ["init", "--dest-dir", context.package.directoryURL.path()] + arguments + + // Invoke the plugin helper on the target directory, passing a configuration + // file from the package directory. + let process = try Process.run(tool.url, arguments: args) + process.waitUntilExit() + + // Check whether the subprocess invocation was successful. + if !(process.terminationReason == .exit && process.terminationStatus == 0) { + let problem = "\(process.terminationReason):\(process.terminationStatus)" + Diagnostics.error("AWSLambdaPluginHelper invocation failed: \(problem)") + } + } +} +``` + +And the executable target would dispatch the invocation to a struct implementing the actual functionality of the plugin: + +```swift + public static func main() async throws { + let args = CommandLine.arguments + let helper = AWSLambdaPluginHelper() + let command = try helper.command(from: args) + switch command { + case .`init`: + try await Initializer().initialize(arguments: args) + case .build: + try await Builder().build(arguments: args) + case .deploy: + try await Deployer().deploy(arguments: args) + } + } +``` + +## Considered Alternatives + +In addition to the proposed solution, we evaluated the following alternatives: + +1. **VSCode Extension for Project Scaffolding:** + +We considered using a VSCode extension, such as the `vscode-aws-lambda-swift-sam` extension ([https://github.com/swift-server-community/vscode-aws-lambda-swift-sam](https://github.com/swift-server-community/vscode-aws-lambda-swift-sam)), to scaffold new Lambda projects. + +This extension creates a new Lambda project from scratch, including the project structure and dependencies. It provides a ready-to-build `main.swift` file with a simple Lambda function and allows users to choose from basic templates, such as "Hello World" or an OpenAPI-based Lambda function. However, the extension relies on the AWS CLI and SAM CLI for deployment. It is only available in the Visual Studio Code Marketplace. + +While the extension offers a user-friendly graphical interface, it does not align well with our goals of simplicity for first-time users and minimal dependencies. Users would need to install and configure VSCode, the extension itself, the AWS CLI, and the SAM CLI before getting started. + +2. **Deployment DSL with AWS SAM:** + +We also considered using a domain-specific language (DSL) to describe deployments, such as the `swift-aws-lambda-sam-dsl` project ([https://github.com/swift-server-community/swift-aws-lambda-sam-dsl](https://github.com/swift-server-community/swift-aws-lambda-sam-dsl)), and leveraging AWS SAM for the actual deployment. + +This plugin allows developers to describe their deployment using Swift code, and the plugin automatically generates the corresponding SAM template. However, the plugin depends on the SAM CLI for deployment. Additionally, new developers would need to learn a new DSL for deployment configuration. + +We believe the `lambda-deploy` plugin is a preferable alternative because it interacts directly with the AWS REST API and avoids introducing additional dependencies for the user. diff --git a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift b/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift similarity index 63% rename from Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift rename to Sources/AWSLambdaRuntime/ControlPlaneRequest.swift index 411dc5ad..9fa933f1 100644 --- a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift +++ b/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift @@ -15,6 +15,7 @@ import NIOCore import NIOHTTP1 +@available(LambdaSwift 2.0, *) enum ControlPlaneRequest: Hashable { case next case invocationResponse(String, ByteBuffer?) @@ -22,39 +23,49 @@ enum ControlPlaneRequest: Hashable { case initializationError(ErrorResponse) } +@available(LambdaSwift 2.0, *) enum ControlPlaneResponse: Hashable { - case next(Invocation, ByteBuffer) + case next(InvocationMetadata, ByteBuffer) case accepted case error(ErrorResponse) } -struct Invocation: Hashable { - let requestID: String - let deadlineInMillisSinceEpoch: Int64 - let invokedFunctionARN: String - let traceID: String - let clientContext: String? - let cognitoIdentity: String? +@usableFromInline +@available(LambdaSwift 2.0, *) +package struct InvocationMetadata: Hashable, Sendable { + @usableFromInline + package let requestID: String + @usableFromInline + package let deadlineInMillisSinceEpoch: Int64 + @usableFromInline + package let invokedFunctionARN: String + @usableFromInline + package let traceID: String + @usableFromInline + package let clientContext: String? + @usableFromInline + package let cognitoIdentity: String? - init(headers: HTTPHeaders) throws { + package init(headers: HTTPHeaders) throws(LambdaRuntimeError) { guard let requestID = headers.first(name: AmazonHeaders.requestID), !requestID.isEmpty else { - throw LambdaRuntimeError.invocationMissingHeader(AmazonHeaders.requestID) + throw LambdaRuntimeError(code: .nextInvocationMissingHeaderRequestID) } guard let deadline = headers.first(name: AmazonHeaders.deadline), - let unixTimeInMilliseconds = Int64(deadline) + let unixTimeInMilliseconds = Int64(deadline) else { - throw LambdaRuntimeError.invocationMissingHeader(AmazonHeaders.deadline) + throw LambdaRuntimeError(code: .nextInvocationMissingHeaderDeadline) } guard let invokedFunctionARN = headers.first(name: AmazonHeaders.invokedFunctionARN) else { - throw LambdaRuntimeError.invocationMissingHeader(AmazonHeaders.invokedFunctionARN) + throw LambdaRuntimeError(code: .nextInvocationMissingHeaderInvokeFuctionARN) } self.requestID = requestID self.deadlineInMillisSinceEpoch = unixTimeInMilliseconds self.invokedFunctionARN = invokedFunctionARN - self.traceID = headers.first(name: AmazonHeaders.traceID) ?? "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=0" + self.traceID = + headers.first(name: AmazonHeaders.traceID) ?? "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=0" self.clientContext = headers["Lambda-Runtime-Client-Context"].first self.cognitoIdentity = headers["Lambda-Runtime-Cognito-Identity"].first } @@ -66,7 +77,7 @@ struct ErrorResponse: Hashable, Codable { } extension ErrorResponse { - internal func toJSONBytes() -> [UInt8] { + func toJSONBytes() -> [UInt8] { var bytes = [UInt8]() bytes.append(UInt8(ascii: "{")) bytes.append(contentsOf: #""errorType":"#.utf8) diff --git a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequestEncoder.swift b/Sources/AWSLambdaRuntime/ControlPlaneRequestEncoder.swift similarity index 89% rename from Sources/AWSLambdaRuntimeCore/ControlPlaneRequestEncoder.swift rename to Sources/AWSLambdaRuntime/ControlPlaneRequestEncoder.swift index a91e1e44..6934b064 100644 --- a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequestEncoder.swift +++ b/Sources/AWSLambdaRuntime/ControlPlaneRequestEncoder.swift @@ -14,6 +14,7 @@ import NIOCore +@available(LambdaSwift 2.0, *) struct ControlPlaneRequestEncoder: _EmittingChannelHandler { typealias OutboundOut = ByteBuffer @@ -24,7 +25,11 @@ struct ControlPlaneRequestEncoder: _EmittingChannelHandler { self.host = host } - mutating func writeRequest(_ request: ControlPlaneRequest, context: ChannelHandlerContext, promise: EventLoopPromise?) { + mutating func writeRequest( + _ request: ControlPlaneRequest, + context: ChannelHandlerContext, + promise: EventLoopPromise? + ) { self.byteBuffer.clear(minimumCapacity: self.byteBuffer.storageCapacity) switch request { @@ -32,7 +37,7 @@ struct ControlPlaneRequestEncoder: _EmittingChannelHandler { self.byteBuffer.writeString(.nextInvocationRequestLine) self.byteBuffer.writeHostHeader(host: self.host) self.byteBuffer.writeString(.userAgentHeader) - self.byteBuffer.writeString(.CRLF) // end of head + self.byteBuffer.writeString(.CRLF) // end of head context.write(self.wrapOutboundOut(self.byteBuffer), promise: promise) context.flush() @@ -42,7 +47,7 @@ struct ControlPlaneRequestEncoder: _EmittingChannelHandler { self.byteBuffer.writeHostHeader(host: self.host) self.byteBuffer.writeString(.userAgentHeader) self.byteBuffer.writeContentLengthHeader(length: contentLength) - self.byteBuffer.writeString(.CRLF) // end of head + self.byteBuffer.writeString(.CRLF) // end of head if let payload = payload, contentLength > 0 { context.write(self.wrapOutboundOut(self.byteBuffer), promise: nil) context.write(self.wrapOutboundOut(payload), promise: promise) @@ -58,7 +63,7 @@ struct ControlPlaneRequestEncoder: _EmittingChannelHandler { self.byteBuffer.writeHostHeader(host: self.host) self.byteBuffer.writeString(.userAgentHeader) self.byteBuffer.writeString(.unhandledErrorHeader) - self.byteBuffer.writeString(.CRLF) // end of head + self.byteBuffer.writeString(.CRLF) // end of head self.byteBuffer.writeBytes(payload) context.write(self.wrapOutboundOut(self.byteBuffer), promise: promise) context.flush() @@ -70,7 +75,7 @@ struct ControlPlaneRequestEncoder: _EmittingChannelHandler { self.byteBuffer.writeHostHeader(host: self.host) self.byteBuffer.writeString(.userAgentHeader) self.byteBuffer.writeString(.unhandledErrorHeader) - self.byteBuffer.writeString(.CRLF) // end of head + self.byteBuffer.writeString(.CRLF) // end of head self.byteBuffer.writeBytes(payload) context.write(self.wrapOutboundOut(self.byteBuffer), promise: promise) context.flush() @@ -89,7 +94,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/\(Version.current)" + 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/AWSLambdaRuntime/Docs.docc/Deployment.md b/Sources/AWSLambdaRuntime/Docs.docc/Deployment.md new file mode 100644 index 00000000..2b49c2ba --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Deployment.md @@ -0,0 +1,639 @@ +# Deploying your Swift Lambda functions + +Learn how to deploy your Swift Lambda functions to AWS. + +### Overview + +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. + +To take full advantage of the cloud, we recommend using Infrastructure as Code (IaC) tools like the [AWS Serverless Application Model (SAM)](https://aws.amazon.com/serverless/sam/) or [AWS Cloud Development Kit (CDK)](https://aws.amazon.com/cdk/). These tools allow you to define your infrastructure and deployment process as code, which can be version-controlled and automated. + +In this section, we show you how to deploy your Swift Lambda functions using different AWS Tools. Alternatively, you might also consider using popular third-party tools like [Serverless Framework](https://www.serverless.com/), [Terraform](https://www.terraform.io/), or [Pulumi](https://www.pulumi.com/) to deploy Lambda functions and create and manage AWS infrastructure. + +Here is the content of this guide: + + * [Prerequisites](#prerequisites) + * [Choosing the AWS Region where to deploy](#choosing-the-aws-region-where-to-deploy) + * [The Lambda execution IAM role](#the-lambda-execution-iam-role) + * [Deploy your Lambda function with the AWS Console](#deploy-your-lambda-function-with-the-aws-console) + * [Deploy your Lambda function with the AWS Command Line Interface (CLI)](#deploy-your-lambda-function-with-the-aws-command-line-interface-cli) + * [Deploy your Lambda function with AWS Serverless Application Model (SAM)](#deploy-your-lambda-function-with-aws-serverless-application-model-sam) + * [Deploy your Lambda function with AWS Cloud Development Kit (CDK)](#deploy-your-lambda-function-with-aws-cloud-development-kit-cdk) + * [Third-party tools](#third-party-tools) + +### Prerequisites + +1. Your AWS Account + + To deploy a Lambda function on AWS, you need an AWS account. If you don't have one yet, you can create a new account at [aws.amazon.com](https://signin.aws.amazon.com/signup?request_type=register). It takes a few minutes to register. A credit card is required. + + We do not recommend using the root credentials you entered at account creation time for day-to-day work. Instead, create an [Identity and Access Manager (IAM) user](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users.html) with the necessary permissions and use its credentials. + + Follow the steps in [Create an IAM User in your AWS account](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html). + + We suggest to attach the `AdministratorAccess` policy to the user for the initial setup. For production workloads, you should follow the principle of least privilege and grant only the permissions required for your users. The ['AdministratorAccess' gives the user permission](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html#aws-managed-policies) to manage all resources on the AWS account. + +2. AWS Security Credentials + + [AWS Security Credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html) are required to access the AWS console, AWS APIs, or to let tools access your AWS account. + + AWS Security Credentials can be **long-term credentials** (for example, an Access Key ID and a Secret Access Key attached to your IAM user) or **temporary credentials** obtained via other AWS API, such as when accessing AWS through single sign-on (SSO) or when assuming an IAM role. + + To follow the steps in this guide, you need to know your AWS Access Key ID and Secret Access Key. If you don't have them, you can create them in the AWS Management Console. Follow the steps in [Creating access keys for an IAM user](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey). + + When you use SSO with your enterprise identity tools (such as Microsoft entra ID –formerly Active Directory–, Okta, and others) or when you write scripts or code assuming an IAM role, you receive temporary credentials. These credentials are valid for a limited time, have a limited scope, and are rotated automatically. You can use them in the same way as long-term credentials. In addition to an AWS Access Key and Secret Access Key, temporary credentials include a session token. + + Here is a typical set of temporary credentials (redacted for security). + + ```json + { + "Credentials": { + "AccessKeyId": "ASIA...FFSD", + "SecretAccessKey": "Xn...NL", + "SessionToken": "IQ...pV", + "Expiration": "2024-11-23T11:32:30+00:00" + } + } + ``` + +3. A Swift Lambda function to deploy. + + You need a Swift Lambda function to deploy. If you don't have one yet, you can use one of the examples in the [Examples](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples) directory. + + Compile and package the function using the following command + + ```sh + swift package archive --allow-network-connections docker + ``` + + This command creates a ZIP file with the compiled Swift code. The ZIP file is located in the `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip` folder. + + The name of the ZIP file depends on the target name you entered in the `Package.swift` file. + + >[!NOTE] + > When building on Linux, your current user must have permission to use docker. On most Linux distributions, you can do so by adding your user to the `docker` group with the following command: `sudo usermod -aG docker $USER`. You must log out and log back in for the changes to take effect. + +### Choosing the AWS Region where to deploy + +[AWS Global infrastructure](https://aws.amazon.com/about-aws/global-infrastructure/) spans over 34 geographic Regions (and continuously expanding). When you create a resource on AWS, such as a Lambda function, you have to select a geographic region where the resource will be created. The two main factors to consider to select a Region are the physical proximity with your users and geographical compliance. + +Physical proximity helps you reduce the network latency between the Lambda function and your customers. For example, when the majority of your users are located in South-East Asia, you might consider deploying in the Singapore, the Malaysia, or Jakarta Region. + +Geographical compliance, also known as data residency compliance, involves following location-specific regulations about how and where data can be stored and processed. + +### The Lambda execution IAM role + +A Lambda execution role is an AWS Identity and Access Management (IAM) role that grants your Lambda function the necessary permissions to interact with other AWS services and resources. Think of it as a security passport that determines what your function is allowed to do within AWS. For example, if your Lambda function needs to read files from Amazon S3, write logs to Amazon CloudWatch, or access an Amazon DynamoDB table, the execution role must include the appropriate permissions for these actions. + +When you create a Lambda function, you must specify an execution role. This role contains two main components: a trust policy that allows the Lambda service itself to assume the role, and permission policies that determine what AWS resources the function can access. By default, Lambda functions get basic permissions to write logs to CloudWatch Logs, but any additional permissions (like accessing S3 buckets or sending messages to SQS queues) must be explicitly added to the role's policies. Following the principle of least privilege, it's recommended to grant only the minimum permissions necessary for your function to operate, helping maintain the security of your serverless applications. + +### Deploy your Lambda function with the AWS Console + +In this section, we deploy the HelloWorld example function using the AWS Console. The HelloWorld function is a simple function that takes a `String` as input and returns a `String`. + +Authenticate on the AWS console using your IAM username and password. On the top right side, select the AWS Region where you want to deploy, then navigate to the Lambda section. + +![Console - Select AWS Region](console-10-regions) + +#### Create the function + +Select **Create a function** to create a function. + +![Console - Lambda dashboard when there is no function](console-20-dashboard) + +Select **Author function from scratch**. Enter a **Function name** (`HelloWorld`) and select `Amazon Linux 2` as **Runtime**. +Select the architecture. When you compile your Swift code on a x84_64 machine, such as an Intel Mac, select `x86_64`. When you compile your Swift code on an Arm machine, such as the Apple Silicon M1 or more recent, select `arm64`. + +Select **Create function** + +![Console - create function](console-30-create-function) + +On the right side, select **Upload from** and select **.zip file**. + +![Console - select zip file](console-40-select-zip-file) + +Select the zip file created with the `swift package archive --allow-network-connections docker` command as described in the [Prerequisites](#prerequisites) section. + +Select **Save** + +![Console - select zip file](console-50-upload-zip) + +You're now ready to test your function. + +#### Invoke the function + +Select the **Test** tab in the console and prepare a payload to send to your Lambda function. In this example, you've deployed the [HelloWorld](Exmaples.HelloWorld/README.md) example function. As explained, the function takes a `String` as input and returns a `String`. we will therefore create a test event with a JSON payload that contains a `String`. + +Select **Create new event**. Enter an **Event name**. Enter `"Swift on Lambda"` as **Event JSON**. Note that the payload must be a valid JSON document, hence we use surrounding double quotes (`"`). + +Select **Test** on the upper right side of the screen. + +![Console - prepare test event](console-60-prepare-test-event) + +The response of the invocation and additional meta data appear in the green section of the page. + +You can see the response from the Swift code: `Hello Swift on Lambda`. + +The function consumed 109.60ms of execution time, out of this 83.72ms where spent to initialize this new runtime. This initialization time is known as Lambda cold start time. + +> Lambda cold start time refers to the initial delay that occurs when a Lambda function is invoked for the first time or after being idle for a while. Cold starts happen because AWS needs to provision and initialize a new container, load your code, and start your runtime environment (in this case, the Swift runtime). This delay is particularly noticeable for the first invocation, but subsequent invocations (known as "warm starts") are typically much faster because the container and runtime are already initialized and ready to process requests. Cold starts are an important consideration when architecting serverless applications, especially for latency-sensitive workloads. Usually, compiled languages, such as Swift, Go, and Rust, have shorter cold start times compared to interpreted languages, such as Python, Java, Ruby, and Node.js. + +```text + +![Console - view invocation result](console-70-view-invocation-response) + +Select **Test** to invoke the function again with the same payload. + +Observe the results. No initialization time is reported because the Lambda execution environment was ready after the first invocation. The runtime duration of the second invocation is 1.12ms. + +```text +REPORT RequestId: f789fbb6-10d9-4ba3-8a84-27aa283369a2 Duration: 1.12 ms Billed Duration: 2 ms Memory Size: 128 MB Max Memory Used: 26 MB +``` + +AWS lambda charges usage per number of invocations and the CPU time, rounded to the next millisecond. AWS Lambda offers a generous free-tier of 1 million invocation each month and 400,000 GB-seconds of compute time per month. See [Lambda pricing](https://aws.amazon.com/lambda/pricing/) for the details. + +#### Delete the function + +When you're finished with testing, you can delete the Lambda function and the IAM execution role that the console created automatically. + +While you are on the `HelloWorld` function page in the AWS console, select **Actions**, then **Delete function** in the menu on the top-right part of the page. + +![Console - delete function](console-80-delete-function) + +Then, navigate to the IAM section of the AWS console. Select **Roles** on the right-side menu and search for `HelloWorld`. The console appended some random characters to role name. The name you see on your console is different that the one on the screenshot. + +Select the `HelloWorld-role-xxxx` role and select **Delete**. Confirm the deletion by entering the role name again, and select **Delete** on the confirmation box. + +![Console - delete IAM role](console-80-delete-role) + +### Deploy your Lambda function with the AWS Command Line Interface (CLI) + +You can deploy your Lambda function using the AWS Command Line Interface (CLI). The CLI is a unified tool to manage your AWS services from the command line and automate your operations through scripts. The CLI is available for Windows, macOS, and Linux. Follow the [installation](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and [configuration](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html) instructions in the AWS CLI User Guide. + +In this example, we're building the HelloWorld example from the [Examples](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples) directory. + +#### Create the function + +To create a function, you must first create the function execution role and define the permission. Then, you create the function with the `create-function` command. + +The command assumes you've already created the ZIP file with the `swift package archive --allow-network-connections docker` command, as described in the [Prerequisites](#prerequisites) section. + +```sh +# enter your AWS Account ID +export AWS_ACCOUNT_ID=123456789012 + +# Allow the Lambda service to assume the execution role +cat < assume-role-policy.json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} +EOF + +# Create the execution role +aws iam create-role \ +--role-name lambda_basic_execution \ +--assume-role-policy-document file://assume-role-policy.json + +# create permissions to associate with the role +cat < permissions.json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": "arn:aws:logs:*:*:*" + } + ] +} +EOF + +# Attach the permissions to the role +aws iam put-role-policy \ +--role-name lambda_basic_execution \ +--policy-name lambda_basic_execution_policy \ +--policy-document file://permissions.json + +# Create the Lambda function +aws lambda create-function \ +--function-name MyLambda \ +--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip \ +--runtime provided.al2 \ +--handler provided \ +--architectures arm64 \ +--role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution +``` + +The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. + +To update the function, use the `update-function-code` command after you've recompiled and archived your code again with the `swift package archive` command. + +```sh +aws lambda update-function-code \ +--function-name MyLambda \ +--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip +``` + +#### Invoke the function + +Use the `invoke-function` command to invoke the function. You can pass a well-formed JSON payload as input to the function. The payload must be encoded in base64. The CLI returns the status code and stores the response in a file. + +```sh +# invoke the function +aws lambda invoke \ +--function-name MyLambda \ +--payload $(echo \"Swift Lambda function\" | base64) \ +out.txt + +# show the response +cat out.txt + +# delete the response file +rm out.txt +``` + +#### Delete the function + +To cleanup, first delete the Lambda funtion, then delete the IAM role. + +```sh +# delete the Lambda function +aws lambda delete-function --function-name MyLambda + +# delete the IAM policy attached to the role +aws iam delete-role-policy --role-name lambda_basic_execution --policy-name lambda_basic_execution_policy + +# delete the IAM role +aws iam delete-role --role-name lambda_basic_execution +``` + +### Deploy your Lambda function with AWS Serverless Application Model (SAM) + +AWS Serverless Application Model (SAM) is an open-source framework for building serverless applications. It provides a simplified way to define the Amazon API Gateway APIs, AWS Lambda functions, and Amazon DynamoDB tables needed by your serverless application. You can define your serverless application in a single file, and SAM will use it to deploy your function and all its dependencies. + +To use SAM, you need to [install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) on your machine. The SAM CLI provides a set of commands to package, deploy, and manage your serverless applications. + +Use SAM when you want to deploy more than a Lambda function. SAM helps you to create additional resources like an API Gateway, an S3 bucket, or a DynamoDB table, and manage the permissions between them. + +#### Create the function + +We assume your Swift function is compiled and packaged, as described in the [Prerequisites](#prerequisites) section. + +When using SAM, you describe the infrastructure you want to deploy in a YAML file. The file contains the definition of the Lambda function, the IAM role, and the permissions needed by the function. The SAM CLI uses this file to package and deploy your function. + +You can create a SAM template to define a REST API implemented by AWS API Gateway and a Lambda function with the following command + +```sh +cat < template.yaml +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for APIGateway Lambda Example + +Resources: + # Lambda function + APIGatewayLambda: + Type: AWS::Serverless::Function + Properties: + # the directory name and ZIP file names depends on the Swift executable target name + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip + Timeout: 60 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 128 + Architectures: + - arm64 + # The events that will trigger this function + Events: + HttpApiEvent: + Type: HttpApi # AWS API Gateway v2 + +Outputs: + # display API Gateway endpoint + APIGatewayEndpoint: + Description: "API Gateway endpoint URI" + Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" +EOF +``` + +In this example, the Lambda function must accept an APIGateway v2 JSON payload as input parameter and return a valid APIGAteway v2 JSON response. See the example code in the [APIGateway example README file](https://github.com/swift-server/swift-aws-lambda-runtime/blob/main/Examples/APIGateway/README.md). + +To deploy the function with SAM, use the `sam deploy` command. The very first time you deploy a function, you should use the `--guided` flag to configure the deployment. The command will ask you a series of questions to configure the deployment. + +Here is the command to deploy the function with SAM: + +```sh +# start the first deployment +sam deploy --guided + +Configuring SAM deploy +====================== + + Looking for config file [samconfig.toml] : Not found + + Setting default arguments for 'sam deploy' + ========================================= + Stack Name [sam-app]: APIGatewayLambda + AWS Region [us-east-1]: + #Shows you resources changes to be deployed and require a 'Y' to initiate deploy + Confirm changes before deploy [y/N]: n + #SAM needs permission to be able to create roles to connect to the resources in your template + Allow SAM CLI IAM role creation [Y/n]: y + #Preserves the state of previously provisioned resources when an operation fails + Disable rollback [y/N]: n + APIGatewayLambda has no authentication. Is this okay? [y/N]: y + Save arguments to configuration file [Y/n]: y + SAM configuration file [samconfig.toml]: + SAM configuration environment [default]: + + Looking for resources needed for deployment: + +(redacted for brevity) + +CloudFormation outputs from deployed stack +-------------------------------------------------------------------------------- +Outputs +-------------------------------------------------------------------------------- +Key APIGatewayEndpoint +Description API Gateway endpoint URI" +Value https://59i4uwbuj2.execute-api.us-east-1.amazonaws.com +-------------------------------------------------------------------------------- + + +Successfully created/updated stack - APIGAtewayLambda in us-east-1 +``` + +To update your function or any other AWS service defined in your YAML file, you can use the `sam deploy` command without the `--guided` flag. + +#### Invoke the function + +SAM allows you to invoke the function locally and remotely. + +Local invocations allows you to test your code before uploading it. It requires docker to run. + +```sh +# First, generate a sample event +sam local generate-event apigateway http-api-proxy > event.json + +# Next, invoke the function locally +sam local invoke -e ./event.json + +START RequestId: 3f5096c6-0fd3-4605-b03e-d46658e6b141 Version: $LATEST +END RequestId: 3134f067-9396-4f4f-bebb-3c63ef745803 +REPORT RequestId: 3134f067-9396-4f4f-bebb-3c63ef745803 Init Duration: 0.04 ms Duration: 38.38 msBilled Duration: 39 ms Memory Size: 512 MB Max Memory Used: 512 MB +{"body": "{\"version\":\"2.0\",\"routeKey\":\"$default\",\"rawPath\":\"\\/path\\/to\\/resource\",... REDACTED FOR BREVITY ...., "statusCode": 200, "headers": {"content-type": "application/json"}} +``` + +> If you've previously authenticated to Amazon ECR Public and your auth token has expired, you may receive an authentication error when attempting to do unauthenticated docker pulls from Amazon ECR Public. To resolve this issue, it may be necessary to run `docker logout public.ecr.aws` to avoid the error. This will result in an unauthenticated pull. For more information, see [Authentication issues](https://docs.aws.amazon.com/AmazonECR/latest/public/public-troubleshooting.html#public-troubleshooting-authentication). + +Remote invocations are done with the `sam remote invoke` command. + +```sh +sam remote invoke \ + --stack-name APIGatewayLambda \ + --event-file ./event.json + +Invoking Lambda Function APIGatewayLambda +START RequestId: ec8082c5-933b-4176-9c63-4c8fb41ca259 Version: $LATEST +END RequestId: ec8082c5-933b-4176-9c63-4c8fb41ca259 +REPORT RequestId: ec8082c5-933b-4176-9c63-4c8fb41ca259 Duration: 6.01 ms Billed Duration: 7 ms Memory Size: 512 MB Max Memory Used: 35 MB +{"body":"{\"stageVariables\":{\"stageVariable1\":\"value1\",\"stageVariable2\":\"value2\"},\"rawPath\":\"\\\/path\\\/to\\\/resource\",\"routeKey\":\"$default\",\"cookies\":[\"cookie1\",\"cookie2\"] ... REDACTED FOR BREVITY ... \"statusCode\":200,"headers":{"content-type":"application/json"}} +``` + +SAM allows you to access the function logs from Amazon Cloudwatch. + +```sh +sam logs --stack-name APIGatewayLambda + +Access logging is disabled for HTTP API ID (g9m53sn7xa) +2024/12/19/[$LATEST]4dd42d66282145a2964ff13dfcd5dc65 2024-12-19T10:16:25.593000 INIT_START Runtime Version: provided:al2.v75 Runtime Version ARN: arn:aws:lambda:us-east-1::runtime:4f3438ed7de2250cc00ea1260c3dc3cd430fad27835d935a02573b6cf07ceed8 +2024/12/19/[$LATEST]4dd42d66282145a2964ff13dfcd5dc65 2024-12-19T10:16:25.715000 START RequestId: d8afa647-8361-4bce-a817-c57b92a060af Version: $LATEST +2024/12/19/[$LATEST]4dd42d66282145a2964ff13dfcd5dc65 2024-12-19T10:16:25.758000 END RequestId: d8afa647-8361-4bce-a817-c57b92a060af +2024/12/19/[$LATEST]4dd42d66282145a2964ff13dfcd5dc65 2024-12-19T10:16:25.758000 REPORT RequestId: d8afa647-8361-4bce-a817-c57b92a060af Duration: 40.74 ms Billed Duration: 162 ms Memory Size: 512 MB Max Memory Used: 34 MB Init Duration: 120.64 ms +2024/12/19/[$LATEST]4dd42d66282145a2964ff13dfcd5dc65 2024-12-19T10:17:10.343000 START RequestId: ec8082c5-933b-4176-9c63-4c8fb41ca259 Version: $LATEST +2024/12/19/[$LATEST]4dd42d66282145a2964ff13dfcd5dc65 2024-12-19T10:17:10.350000 END RequestId: ec8082c5-933b-4176-9c63-4c8fb41ca259 +2024/12/19/[$LATEST]4dd42d66282145a2964ff13dfcd5dc65 2024-12-19T10:17:10.350000 REPORT RequestId: ec8082c5-933b-4176-9c63-4c8fb41ca259 Duration: 6.01 ms Billed Duration: 7 ms Memory Size: 512 MB Max Memory Used: 35 MB +``` + +You can also tail the logs with the `-t, --tail` flag. + +#### Delete the function + +SAM allows you to delete your function and all infrastructure that is defined in the YAML template with just one command. + +```sh +sam delete + +Are you sure you want to delete the stack APIGatewayLambda in the region us-east-1 ? [y/N]: y +Are you sure you want to delete the folder APIGatewayLambda in S3 which contains the artifacts? [y/N]: y +- Deleting S3 object with key APIGatewayLambda/1b5a27c048549382462bd8ea589f7cfe +- Deleting S3 object with key APIGatewayLambda/396d2c434ecc24aaddb670bd5cca5fe8.template +- Deleting Cloudformation stack APIGatewayLambda + +Deleted successfully +``` + +### Deploy your Lambda function with the AWS Cloud Development Kit (CDK) + +The AWS Cloud Development Kit is an open-source software development framework to define cloud infrastructure in code and provision it through AWS CloudFormation. The CDK provides high-level constructs that preconfigure AWS resources with best practices, and you can use familiar programming languages like TypeScript, Javascript, Python, Java, C#, and Go to define your infrastructure. + +To use the CDK, you need to [install the CDK CLI](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) on your machine. The CDK CLI provides a set of commands to manage your CDK projects. + +Use the CDK when you want to define your infrastructure in code and manage the deployment of your Lambda function and other AWS services. + +This example deploys the [APIGateway]((https://github.com/swift-server/swift-aws-lambda-runtime/blob/main/Examples/APIGateway/) example code. It comprises a Lambda function that implements a REST API and an API Gateway to expose the function over HTTPS. + +#### Create a CDK project + +To create a new CDK project, use the `cdk init` command. The command creates a new directory with the project structure and the necessary files to define your infrastructure. + +```sh +# In your Swift Lambda project folder +mkdir infra && cd infra +cdk init app --language typescript +``` + +In this example, the code to create a Swift Lambda function with the CDK is written in TypeScript. The following code creates a new Lambda function with the `swift` runtime. + +It requires the `@aws-cdk/aws-lambda` package to define the Lambda function. You can install the dependency with the following command: + +```sh +npm install aws-cdk-lib constructs +``` + +Then, in the lib folder, create a new file named `swift-lambda-stack.ts` with the following content: + +```typescript +import * as cdk from 'aws-cdk-lib'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; + +export class LambdaApiStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // Create the Lambda function + const lambdaFunction = new lambda.Function(this, 'SwiftLambdaFunction', { + runtime: lambda.Runtime.PROVIDED_AL2, + architecture: lambda.Architecture.ARM_64, + handler: 'bootstrap', + code: lambda.Code.fromAsset('../.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip'), + memorySize: 128, + timeout: cdk.Duration.seconds(30), + environment: { + LOG_LEVEL: 'debug', + }, + }); + } +} +``` +The code assumes you already built and packaged the APIGateway Lambda function with the `swift package archive --allow-network-connections docker` command, as described in the [Prerequisites](#prerequisites) section. + +You can write code to add an API Gateway to invoke your Lambda function. The following code creates an HTTP API Gateway that triggers the Lambda function. + +```typescript +// in the import section at the top +import * as apigateway from 'aws-cdk-lib/aws-apigatewayv2'; +import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations'; + +// in the constructor, after having created the Lambda function +// ... + + // Create the API Gateway + const httpApi = new apigateway.HttpApi(this, 'HttpApi', { + defaultIntegration: new HttpLambdaIntegration({ + handler: lambdaFunction, + }), + }); + + // Output the API Gateway endpoint + new cdk.CfnOutput(this, 'APIGatewayEndpoint', { + value: httpApi.url!, + }); + +// ... +``` + +#### Deploy the infrastructure + +To deploy the infrastructure, type the following commands. + +```sh +# Change to the infra directory +cd infra + +# Install the dependencies (only before the first deployment) +npm install + +# Deploy the infrastructure +cdk deploy + +✨ Synthesis time: 2.88s +... redacted for brevity ... +Do you wish to deploy these changes (y/n)? y +... redacted for brevity ... + ✅ LambdaApiStack + +✨ Deployment time: 42.96s + +Outputs: +LambdaApiStack.ApiUrl = https://tyqnjcawh0.execute-api.eu-central-1.amazonaws.com/ +Stack ARN: +arn:aws:cloudformation:eu-central-1:012345678901:stack/LambdaApiStack/e0054390-be05-11ef-9504-065628de4b89 + +✨ Total time: 45.84s +``` + +#### Invoke your Lambda function + +To invoke the Lambda function, use this `curl` command line. + +```bash +curl https://tyqnjcawh0.execute-api.eu-central-1.amazonaws.com +``` + +Be sure to replace the URL with the API Gateway endpoint returned in the previous step. + +This should print a JSON similar to + +```bash +{"version":"2.0","rawPath":"\/","isBase64Encoded":false,"rawQueryString":"","headers":{"user-agent":"curl\/8.7.1","accept":"*\/*","host":"a5q74es3k2.execute-api.us-east-1.amazonaws.com","content-length":"0","x-amzn-trace-id":"Root=1-66fb0388-691f744d4bd3c99c7436a78d","x-forwarded-port":"443","x-forwarded-for":"81.0.0.43","x-forwarded-proto":"https"},"requestContext":{"requestId":"e719cgNpoAMEcwA=","http":{"sourceIp":"81.0.0.43","path":"\/","protocol":"HTTP\/1.1","userAgent":"curl\/8.7.1","method":"GET"},"stage":"$default","apiId":"a5q74es3k2","time":"30\/Sep\/2024:20:01:12 +0000","timeEpoch":1727726472922,"domainPrefix":"a5q74es3k2","domainName":"a5q74es3k2.execute-api.us-east-1.amazonaws.com","accountId":"012345678901"} +``` + +If you have `jq` installed, you can use it to pretty print the output. + +```bash +curl -s https://tyqnjcawh0.execute-api.eu-central-1.amazonaws.com | jq +{ + "version": "2.0", + "rawPath": "/", + "requestContext": { + "domainPrefix": "a5q74es3k2", + "stage": "$default", + "timeEpoch": 1727726558220, + "http": { + "protocol": "HTTP/1.1", + "method": "GET", + "userAgent": "curl/8.7.1", + "path": "/", + "sourceIp": "81.0.0.43" + }, + "apiId": "a5q74es3k2", + "accountId": "012345678901", + "requestId": "e72KxgsRoAMEMSA=", + "domainName": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", + "time": "30/Sep/2024:20:02:38 +0000" + }, + "rawQueryString": "", + "routeKey": "$default", + "headers": { + "x-forwarded-for": "81.0.0.43", + "user-agent": "curl/8.7.1", + "host": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", + "accept": "*/*", + "x-amzn-trace-id": "Root=1-66fb03de-07533930192eaf5f540db0cb", + "content-length": "0", + "x-forwarded-proto": "https", + "x-forwarded-port": "443" + }, + "isBase64Encoded": false +} +``` + +#### Delete the infrastructure + +When done testing, you can delete the infrastructure with this command. + +```bash +cdk destroy + +Are you sure you want to delete: LambdaApiStack (y/n)? y +LambdaApiStack: destroying... [1/1] +... redacted for brevity ... + ✅ LambdaApiStack: destroyed +``` + +### Third-party tools + +We welcome contributions to this section. If you have experience deploying Swift Lambda functions with third-party tools like Serverless Framework, Terraform, or Pulumi, please share your knowledge with the community. + +## ⚠️ Security and Reliability Notice + +These are example applications for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: + +- Enable access logging on API Gateway ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)) +- Ensure that AWS Lambda function is configured for function-level concurrent execution limit ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +- Check encryption settings for Lambda environment variables ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +- Ensure that AWS Lambda function is configured for a Dead Letter Queue (DLQ) ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +- Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html), [code example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres)) \ No newline at end of file diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Documentation.md b/Sources/AWSLambdaRuntime/Docs.docc/Documentation.md similarity index 97% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Documentation.md rename to Sources/AWSLambdaRuntime/Docs.docc/Documentation.md index 68088df8..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 @@ -18,7 +18,6 @@ Swift AWS Lambda Runtime was designed to make building Lambda functions in Swift ### Essentials -- - - - ``LambdaHandler`` - +- +- diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md b/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md new file mode 100644 index 00000000..05dc8ea4 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md @@ -0,0 +1,905 @@ +# v2 API proposal for swift-aws-lambda-runtime + +`swift-aws-lambda-runtime` is an important library for the Swift on Server ecosystem. The initial API was written before +async/await was introduced to Swift. When async/await was introduced, shims were added to bridge between the underlying +SwiftNIO `EventLoop` interfaces and async/await. However, just like `gRPC-swift` and `postgres-nio`, we now want to +shift to solely using async/await instead of `EventLoop` interfaces. For this, large parts of the current API have to be +reconsidered. + +## Overview + +Versions: + +- v1 (2024-08-07): Initial version +- v1.1: + - Remove the `reportError(_:)` method from `LambdaResponseStreamWriter` and instead make the `handle(...)` method of + `StreamingLambdaHandler` throwing. + - Remove the `addBackgroundTask(_:)` method from `LambdaContext` due to structured concurrency concerns and introduce + the `LambdaWithBackgroundProcessingHandler` protocol as a solution. + - Introduce `LambdaHandlerAdapter`, which adapts handlers conforming to `LambdaHandler` with + `LambdaWithBackgroundProcessingHandler`. + - Update `LambdaCodableAdapter` to now be generic over any handler conforming to + `LambdaWithBackgroundProcessingHandler` instead of `LambdaHandler`. +- v1.2: + - Remove `~Copyable` from `LambdaResponseStreamWriter` and `LambdaResponseWriter`. Instead throw an error when + `finish()` is called multiple times or when `write`/`writeAndFinish` is called after `finish()`. + +### Motivation + +#### Current Limitations + +##### EventLoop interfaces + +The current API extensively uses the `EventLoop` family of interfaces from SwiftNIO in many areas. To use these +interfaces correctly though, it requires developers to exercise great care and understand the various transform methods +that are used to work with `EventLoop`s and `EventLoopFuture`s. This results in a lot of cognitive complexity and makes +the code in the current API hard to reason about and maintain. For these reasons, the overarching trend in the Swift on +Server ecosystem is to shift to newer, more readable, Swift concurrency constructs and de-couple from SwiftNIO's +`EventLoop` interfaces. + +##### No ownership of the main() function + +A Lambda function can currently be implemented through conformance to the various handler protocols defined in +``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 +[set up global properties before the Lambda starts-up](https://github.com/swift-server/swift-aws-lambda-runtime/issues/265). +Setting up global properties is required to customize the Swift Logging, Metric and Tracing backend. + +##### Non-trivial transition from SimpleLambdaHandler to LambdaHandler + +The `SimpleLambdaHandler` protocol provides a quick and easy way to implement a basic Lambda function. It only requires +an implementation of the `handle` function where the business logic of the Lambda function can be written. +`SimpleLambdaHandler` is perfectly sufficient for small use-cases as the user does not need to spend much time looking +into the library. + +However, `SimpleLambdaHandler` cannot be used when services such as a database client need to be initialized before the +Lambda runtime starts and then also gracefully shutdown prior to the runtime terminating. This is because the only way +to register termination logic is through the `LambdaInitializationContext` (containing a field +`terminator: LambdaTerminator`) which is created and used _internally_ within `LambdaRuntime` and never exposed through +`SimpleLambdaHandler`. For such use-cases, other handler protocols like `LambdaHandler` must be used. `LambdaHandler` +exposes a `context` argument of type `LambdaInitializationContext` through its initializer. Within the initializer, +required services can be initialized and their graceful shutdown logic can be registered with the +`context.terminator.register` function. + +Yet, `LambdaHandler` is quite cumbersome to use in such use-cases as users have to deviate from the established norms of +the Swift on Server ecosystem in order to cleanly manage the lifecycle of the services intended to be used. This is +because the convenient `swift-service-lifecycle` v2 library — which is commonly used for cleanly managing the lifecycles +of required services and widely supported by many libraries — cannot be used in a structured concurrency manner. + +##### Does not integrate well with swift-service-lifecycle in a structured concurrency manner + +The Lambda runtime can only be started using the **internal** `Lambda.run()` function. This function is called by the +`main()` function defined by the `LambdaHandler` protocol, preventing users from injecting initialized services into the +runtime _prior_ to it starting. As shown below, this forces users to use an **unstructured concurrency** approach and +manually initialize services, leading to the issue of the user then perhaps forgetting to gracefully shutdown the +initialized services: + +```swift +struct MyLambda: LambdaHandler { + let pgClient: PostgresClient + + init(context: AWSLambdaRuntime.LambdaInitializationContext) async throws { + /// Instantiate service + let client = PostgresClient(configuration: ...) + + /// Unstructured concurrency to initialize the service + let pgTask = Task { + await client.run() + } + + /// Store the client in `self` so that it can be used in `handle(...)` + self.pgClient = client + + /// !!! Must remember to explicitly register termination logic for PostgresClient !!! + context.terminator.register( + name: "PostgreSQL Client", + handler: { eventLoop in + pgTask.cancel() + return eventLoop.makeFutureWithTask { + await pgTask.value + } + } + ) + } + + func handle(_ event: Event, context: LambdaContext) async throws -> Output { + /// Use the initialized service stored in `self.pgClient` + try await self.pgClient.query(...) + } +} +``` + +##### Verbose Codable support + +In the current API, there are extensions and Codable wrapper classes for decoding events and encoding computed responses +for _each_ different handler protocol and for both `String` and `JSON` formats. This has resulted in a lot of +boilerplate code which can very easily be made generic and simplified in v2. + +#### New features + +##### Support response streaming + +In April 2023 +[AWS introduced support for response streaming](https://aws.amazon.com/blogs/compute/introducing-aws-lambda-response-streaming/) +in Lambda. The current API does not support streaming. For v2 we want to change this. + +##### Scheduling background work + +In May +[AWS described in a blog post that you can run background tasks in Lambda](https://aws.amazon.com/blogs/compute/running-code-after-returning-a-response-from-an-aws-lambda-function/) +until the runtime asks for more work from the control plane. We want to support this by adding new API that allows +background processing, even after the response has been returned. + +### Proposed Solution + +#### async/await-first API + +Large parts of `Lambda`, `LambdaHandler`, and `LambdaRuntime` will be re-written to use async/await constructs in place +of the `EventLoop` family of interfaces. + +#### Providing ownership of main() and support for swift-service-lifecycle + +- Instead of conforming to a handler protocol, users can now create a `LambdaRuntime` by passing in a handler closure. +- `LambdaRuntime` conforms to `ServiceLifecycle.Service` by implementing a `run()` method that contains initialization + and graceful shutdown logic. +- This allows the lifecycle of the `LambdaRuntime` to be managed with `swift-service-lifecycle` _alongside_ and in the + same way the lifecycles of the required services are managed, e.g. + `try await ServiceGroup(services: [postgresClient, ..., lambdaRuntime], ...).run()`. +- Dependencies can now be injected into `LambdaRuntime`. With `swift-service-lifecycle`, services will be initialized + together with `LambdaRuntime`. +- The required services can then be used within the handler in a structured concurrency manner. + `swift-service-lifecycle` takes care of listening for termination signals and terminating the services as well as the + `LambdaRuntime` in correct order. +- `LambdaTerminator` can now be eliminated because its role is replaced with `swift-service-lifecycle`. The termination + logic of the Lambda function will be implemented in the conforming `run()` function of `LambdaRuntime`. + +With this, the earlier code snippet can be replaced with something much easier to read, maintain, and debug: + +```swift +/// Instantiate services +let postgresClient = PostgresClient() + +/// Instantiate LambdaRuntime with a closure handler implementing the business logic of the Lambda function +let runtime = LambdaRuntime { (event: Input, context: LambdaContext) in + /// Use initialized service within the handler + try await postgresClient.query(...) +} + +/// Use ServiceLifecycle to manage the initialization and termination +/// of the services as well as the LambdaRuntime +let serviceGroup = ServiceGroup( + services: [postgresClient, runtime], + configuration: .init(gracefulShutdownSignals: [.sigterm]), + logger: logger +) +try await serviceGroup.run() +``` + +#### Simplifying Codable support + +A detailed explanation is provided in the **Codable Support** section. In short, much of the boilerplate code defined +for each handler protocol in `Lambda+Codable` and `Lambda+String` will be replaced with a single `LambdaCodableAdapter` +struct. + +This adapter struct is generic over (1) any handler conforming to a new handler protocol +`LambdaWithBackgroundProcessingHandler`, (2) the user-specified input and output types, and (3) any decoder and encoder +conforming to protocols `LambdaEventDecoder` and `LambdaOutputDecoder`. The adapter will wrap the underlying handler +with encoding/decoding logic. + +### Detailed Solution + +Below are explanations for all types that we want to use in AWS Lambda Runtime v2. + +#### LambdaResponseStreamWriter + +We will introduce a new `LambdaResponseStreamWriter` protocol. It is used in the new `StreamingLambdaHandler` (defined +below), which is the new base protocol for the `LambdaRuntime` (defined below as well). + +```swift +/// A writer object to write the Lambda response stream into +public protocol LambdaResponseStreamWriter { + /// Write a response part into the stream. The HTTP response is started lazily before the first call to `write(_:)`. + /// Bytes written to the writer are streamed continually. + func write(_ buffer: ByteBuffer) async throws + /// End the response stream and the underlying HTTP response. + func finish() async throws + /// Write a response part into the stream and end the response stream as well as the underlying HTTP response. + func writeAndFinish(_ buffer: ByteBuffer) async throws +} +``` + +If the user does not call `finish()`, the library will automatically finish the stream after the last `write`. +Appropriate errors will be thrown if `finish()` is called multiple times, or if `write`/`writeAndFinish` is called after +`finish()`. + +#### LambdaContext + +`LambdaContext` will be largely unchanged, but the `eventLoop` property will be removed. The `allocator` property of +type `ByteBufferAllocator` will also be removed because (1), we generally want to reduce the number of SwiftNIO types +exposed in the API, and (2), `ByteBufferAllocator` does not optimize the allocation strategies. The common pattern +observed across many libraries is to re-use existing `ByteBuffer`s as much as possible. This is also what we do for the +`LambdaCodableAdapter` (explained in the **Codable Support** section) implementation. + +```swift +/// A context object passed as part of an invocation in LambdaHandler handle functions. +public struct LambdaContext: Sendable { + /// The request ID, which identifies the request that triggered the function invocation. + public var requestID: String { get } + + /// The AWS X-Ray tracing header. + public var traceID: String { get } + + /// The ARN of the Lambda function, version, or alias that's specified in the invocation. + public var invokedFunctionARN: String { get } + + /// The timestamp that the function times out. + public var deadline: DispatchWallTime { get } + + /// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider. + public var cognitoIdentity: String? { get } + + /// For invocations from the AWS Mobile SDK, data about the client application and device. + public var clientContext: String? { get } + + /// `Logger` to log with. + /// + /// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable. + public var logger: Logger { get } +} +``` + +#### Handlers + +We introduce three handler protocols: `StreamingLambdaHandler`, `LambdaHandler`, and +`LambdaWithBackgroundProcessingHandler`. + +##### StreamingLambdaHandler + +The new `StreamingLambdaHandler` protocol is the base protocol to implement a Lambda function. Most users will not use +this protocol and instead use the `LambdaHandler` protocol defined below. + +```swift +/// The base StreamingLambdaHandler protocol +public protocol StreamingLambdaHandler { + /// The business logic of the Lambda function + /// - Parameters: + /// - event: The invocation's input data + /// - responseWriter: A ``LambdaResponseStreamWriter`` to write the invocation's response to. + /// If no response or error is written to the `responseWriter` it will + /// report an error to the invoker. + /// - context: The LambdaContext containing the invocation's metadata + /// - Throws: + /// How the thrown error will be handled by the runtime: + /// - An invocation error will be reported if the error is thrown before the first call to + /// ``LambdaResponseStreamWriter.write(_:)``. + /// - If the error is thrown after call(s) to ``LambdaResponseStreamWriter.write(_:)`` but before + /// a call to ``LambdaResponseStreamWriter.finish()``, the response stream will be closed and trailing + /// headers will be sent. + /// - If ``LambdaResponseStreamWriter.finish()`` has already been called before the error is thrown, the + /// error will be logged. + mutating func handle(_ event: ByteBuffer, responseWriter: some LambdaResponseStreamWriter, context: LambdaContext) async throws +} +``` + +Using this protocol requires the `handle` method to receive the incoming event as a `ByteBuffer` and return the output +as a `ByteBuffer` too. + +Through the `LambdaResponseStreamWriter`, which is passed as an argument in the `handle` function, the **response can be +streamed** by calling the `write(_:)` function of the `LambdaResponseStreamWriter` with partial data repeatedly before +finally closing the response stream by calling `finish()`. Users can also choose to return the entire output and not +stream the response by calling `writeAndFinish(_:)`. + +This protocol also allows for background tasks to be run after a result has been reported to the AWS Lambda control +plane, since the `handle(...)` function is free to implement any background work after the call to +`responseWriter.finish()`. + +The protocol is defined in a way that supports a broad range of use-cases. The handle method is marked as `mutating` to +allow handlers to be implemented with a `struct`. + +An implementation that sends the number 1 to 10 every 500ms could look like this: + +```swift +struct SendNumbersWithPause: StreamingLambdaHandler { + func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + for i in 1...10 { + // Send partial data + responseWriter.write(ByteBuffer(string: #"\#(i)\n\r"#)) + // Perform some long asynchronous work + try await Task.sleep(for: .milliseconds(500)) + } + // All data has been sent. Close off the response stream. + responseWriter.finish() + } +} +``` + +##### LambdaHandler: + +This handler protocol will be the go-to choice for most use-cases because it is completely agnostic to any +encoding/decoding logic -- conforming objects simply have to implement the `handle` function where the input and return +types are Swift objects. + +Note that the `handle` function does not receive a `LambdaResponseStreamWriter` as an argument. Response streaming is +not viable for `LambdaHandler` because the output has to be encoded prior to it being sent, e.g. it is not possible to +encode a partial/incomplete JSON string. + +```swift +public protocol LambdaHandler { + /// Generic input type + /// The body of the request sent to Lambda will be decoded into this type for the handler to consume + associatedtype Event + /// Generic output type + /// This is the return type of the handle() function. + associatedtype Output + + /// The business logic of the Lambda function. Receives a generic input type and returns a generic output type. + /// Agnostic to encoding/decoding + mutating func handle(_ event: Event, context: LambdaContext) async throws -> Output +} +``` + +##### LambdaWithBackgroundProcessingHandler: + +This protocol is exactly like `LambdaHandler`, with the only difference being the added support for executing background +work after the result has been sent to the AWS Lambda control plane. + +This is achieved by not having a return type in the `handle` function. The output is instead written into a +`LambdaResponseWriter` that is passed in as an argument, meaning that the `handle` function is then free to implement +any background work after the result has been sent to the AWS Lambda control plane. + +`LambdaResponseWriter` has different semantics to the `LambdaResponseStreamWriter`. Where the `write(_:)` function of +`LambdaResponseStreamWriter` means writing into a response stream, the `write(_:)` function of `LambdaResponseWriter` +simply serves as a mechanism to return the output without explicitly returning from the `handle` function. + +```swift +public protocol LambdaResponseWriter { + associatedtype Output + + /// Sends the generic Output object (representing the computed result of the handler) + /// to the AWS Lambda response endpoint. + /// An error will be thrown if this function is called more than once. + func write(_: Output) async throws +} + +public protocol LambdaWithBackgroundProcessingHandler { + /// Generic input type + /// The body of the request sent to Lambda will be decoded into this type for the handler to consume + associatedtype Event + /// Generic output type + /// This is the type that the handle() function will send through the ``LambdaResponseWriter``. + associatedtype Output + + /// The business logic of the Lambda function. Receives a generic input type and returns a generic output type. + /// Agnostic to JSON encoding/decoding + func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws +} +``` + +###### Example Usage: + +```swift +struct BackgroundProcessingHandler: LambdaWithBackgroundProcessingHandler { + struct Input: Decodable { + let message: String + } + + struct Greeting: Encodable { + let echoedMessage: String + } + + typealias Event = Input + typealias Output = Greeting + + func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + // Return result to the Lambda control plane + try await outputWriter.write(result: Greeting(echoedMessage: event.messageToEcho)) + + // Perform some background work, e.g: + try await Task.sleep(for: .seconds(10)) + + // Exit the function. All asynchronous work has been executed before exiting the scope of this function. + // Follows structured concurrency principles. + return + } +} +``` + +##### Handler Adapters + +Since the `StreamingLambdaHandler` protocol is the base protocol the `LambdaRuntime` works with, there are adapters to +make both `LambdaHandler` and `LambdaWithBackgroundProcessingHandler` compatible with `StreamingLambdaHandler`. + +1. `LambdaHandlerAdapter` accepts a `LambdaHandler` and conforms it to `LambdaWithBackgroundProcessingHandler`. This is + achieved by taking the generic `Output` object returned from the `handle` function of `LambdaHandler` and passing it + to the `write(_:)` function of the `LambdaResponseWriter`. + +2. `LambdaCodableAdapter` accepts a `LambdaWithBackgroundProcessingHandler` and conforms it to `StreamingLambdaHandler`. + This is achieved by wrapping the `LambdaResponseWriter` with the `LambdaResponseStreamWriter` provided by + `StreamingLambdaHandler`. A call to the `write(_:)` function of `LambdaResponseWriter` is translated into a call to + the `writeAndFinish(_:)` function of `LambdaResponseStreamWriter`. + +Both `LambdaHandlerAdapter` and `LambdaCodableAdapter` are described in greater detail in the **Codable Support** +section. + +To summarize, `LambdaHandler` can be used with the `LambdaRuntime` by first going through `LambdaHandlerAdapter` and +then through `LambdaCodableAdapter`. `LambdaWithBackgroundHandler` just requires `LambdaCodableAdapter`. + +For the common JSON-in and JSON-out use-case, there is an extension on `LambdaRuntime` that abstracts away this wrapping +from the user. + +#### LambdaRuntime + +`LambdaRuntime` is the class that communicates with the Lambda control plane as defined in +[Building a custom runtime for AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) and +forward the invocations to the provided `StreamingLambdaHandler`. It will conform to `ServiceLifecycle.Service` to +provide support for `swift-service-lifecycle`. + +```swift +/// The LambdaRuntime object. This object communicates with the Lambda control plane +/// to fetch work and report errors. +public final class LambdaRuntime: ServiceLifecycle.Service, Sendable + where Handler: StreamingLambdaHandler +{ + + /// Create a LambdaRuntime by passing a handler, an eventLoop and a logger. + /// - Parameter handler: A ``StreamingLambdaHandler`` that will be invoked + /// - Parameter eventLoop: An ``EventLoop`` on which the LambdaRuntime will be + /// executed. Defaults to an EventLoop from + /// ``NIOSingletons.posixEventLoopGroup``. + /// - Parameter logger: A logger + public init( + handler: sending Handler, + eventLoop: EventLoop = Lambda.defaultEventLoop, + logger: Logger = Logger(label: "Lambda") + ) + + /// Create a LambdaRuntime by passing a ``StreamingLambdaHandler``. + public convenience init(handler: sending Handler) + + /// Starts the LambdaRuntime by connecting to the Lambda control plane to ask + /// for events to process. If the environment variable AWS_LAMBDA_RUNTIME_API is + /// set, the LambdaRuntime will connect to the Lambda control plane. Otherwise + /// it will start a mock server that can be used for testing at port 8080 + /// locally. + /// Cancel the task that runs this function to close the communication with + /// the Lambda control plane or close the local mock server. This function + /// only returns once cancelled. + public func run() async throws +} +``` + +The current API allows for a Lambda function to be tested locally through a mock server by requiring an environment +variable named `LOCAL_LAMBDA_SERVER_ENABLED` to be set to `true`. If this environment variable is not set, the program +immediately crashes as the user will not have the `AWS_LAMBDA_RUNTIME_API` environment variable on their local machine +(set automatically when deployed to AWS Lambda). However, making the user set the `LOCAL_LAMBDA_SERVER_ENABLED` +environment variable is an unnecessary step that can be avoided. In the v2 API, the `run()` function will automatically +start the mock server when the `AWS_LAMBDA_RUNTIME_API` environment variable cannot be found. + +#### Lambda + +We also add an enum to store a static function and a property on. We put this on the static `Lambda` because +`LambdaRuntime` is generic and thus has bad ergonomics for static properties and functions. + +```swift +enum Lambda { + /// This returns the default EventLoop that a LambdaRuntime is scheduled on. + /// It uses `NIOSingletons.posixEventLoopGroup.next()` under the hood. + public static var defaultEventLoop: any EventLoop { get } + + /// Report a startup error to the Lambda Control Plane API + public static func reportStartupError(any Error) async +} +``` + +Since the library now provides ownership of the `main()` function and allows users to initialize services before the +`LambdaRuntime` is initialized, the library cannot implicitly report +[errors that occur during initialization to the dedicated endpoint AWS exposes](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-initerror) +like it currently does through the `initialize()` function of `LambdaRunner` which wraps the handler's `init(...)` and +handles any errors thrown by reporting it to the dedicated AWS endpoint. + +To retain support for initialization error reporting, the `Lambda.reportStartupError(any Error)` function gives users +the option to manually report initialization errors in their closure handler. Although this should ideally happen +implicitly like it currently does in v1, we believe this is a small compromise in comparison to the benefits gained in +now being able to cleanly manage the lifecycles of required services in a structured concurrency manner. + +> Use-case: +> +> Assume we want to load a secret for the Lambda function from a secret vault first. If this fails, we want to report +> the error to the control plane: +> +> ```swift +> let secretVault = SecretVault() +> +> do { +> /// !!! Error thrown: secret "foo" does not exist !!! +> let secret = try await secretVault.getSecret("foo") +> +> let runtime = LambdaRuntime { (event: Input, context: LambdaContext) in +> /// Lambda business logic +> } +> +> let serviceGroup = ServiceGroup( +> services: [postgresClient, runtime], +> configuration: .init(gracefulShutdownSignals: [.sigterm]), +> logger: logger +> ) +> try await serviceGroup.run() +> } catch { +> /// Report startup error straight away to the dedicated initialization error endpoint +> try await Lambda.reportStartupError(error) +> } +> ``` + +#### Codable support + +The `LambdaHandler` and `LambdaWithBackgroundProcessingHandler` protocols abstract away encoding/decoding logic from the +conformers as they are generic over custom `Event` and `Output` types. We introduce two adapters `LambdaHandlerAdapter` +and `CodableLambdaAdapter` that implement the encoding/decoding logic and in turn allow the respective handlers to +conform to `StreamingLambdaHandler`. + +##### LambdaHandlerAdapter + +Any handler conforming to `LambdaHandler` can be conformed to `LambdaWithBackgroundProcessingHandler` through +`LambdaHandlerAdapter`. + +```swift +/// Wraps an underlying handler conforming to ``LambdaHandler`` +/// with ``LambdaWithBackgroundProcessingHandler``. +public struct LambdaHandlerAdapter< + Event: Decodable, + Output, + Handler: LambdaHandler +>: LambdaWithBackgroundProcessingHandler where Handler.Event == Event, Handler.Output == Output { + let handler: Handler + + /// Register the concrete handler. + public init(handler: Handler) + + /// 1. Call the `self.handler.handle(...)` with `event` and `context`. + /// 2. Pass the generic `Output` object returned from `self.handler.handle(...)` to `outputWriter.write(_:)` + public func handle(_ event: Event, outputWriter: some LambdaResponseWriter, context: LambdaContext) async throws +} +``` + +##### LambdaCodableAdapter + +`LambdaCodableAdapter` accepts any generic underlying handler conforming to `LambdaWithBackgroundProcessingHandler`. It +also accepts _any_ encoder and decoder object conforming to the `LambdaEventDecoder` and `LambdaOutputEncoder` +protocols: + +###### LambdaEventDecoder and LambdaOutputEncoder protocols + +```swift +public protocol LambdaEventDecoder { + /// Decode the ByteBuffer representing the received event into the generic type Event + /// the handler will receive + func decode(_ type: Event.Type, from buffer: ByteBuffer) throws -> Event +} + +public protocol LambdaOutputEncoder { + /// Encode the generic type Output the handler has produced into a ByteBuffer + func encode(_ value: Output, into buffer: inout ByteBuffer) throws +} +``` + +We provide conformances for Foundation's `JSONDecoder` to `LambdaEventDecoder` and `JSONEncoder` to +`LambdaOutputEncoder`. + +`LambdaCodableAdapter` implements its `handle()` method by: + +1. Decoding the `ByteBuffer` event into the generic `Event` type. +2. Wrapping the `LambdaResponseStreamWriter` with a concrete `LambdaResponseWriter` such that calls to + `LambdaResponseWriter`s `write(_:)` are mapped to `LambdaResponseStreamWriter`s `writeAndFinish(_:)`. + - Note that the argument to `LambdaResponseWriter`s `write(_:)` is a generic `Output` object whereas + `LambdaResponseStreamWriter`s `writeAndFinish(_:)` requires a `ByteBuffer`. + - Therefore, the concrete implementation of `LambdaResponseWriter` also accepts an encoder. Its `write(_:)` function + first encodes the generic `Output` object and then passes it to the underlying `LambdaResponseStreamWriter`. +3. Passing the generic `Event` instance, the concrete `LambdaResponseWriter`, as well as the `LambdaContext` to the + underlying handler's `handle()` method. + +`LambdaCodableAdapter` can implement encoding/decoding for _any_ handler conforming to +`LambdaWithBackgroundProcessingHandler` if `Event` is `Decodable` and the `Output` is `Encodable` or `Void`, meaning +that the encoding/decoding stubs do not need to be implemented by the user. + +```swift +/// Wraps an underlying handler conforming to `LambdaWithBackgroundProcessingHandler` +/// with encoding/decoding logic +public struct LambdaCodableAdapter< + Handler: LambdaWithBackgroundProcessingHandler, + Event: Decodable, + Output, + Decoder: LambdaEventDecoder, + Encoder: LambdaOutputEncoder +>: StreamingLambdaHandler where Handler.Output == Output, Handler.Event == Event { + + /// Register the concrete handler, encoder, and decoder. + public init( + handler: Handler, + encoder: Encoder, + decoder: Decoder + ) where Output: Encodable + + /// For handler with a void output -- the user doesn't specify an encoder. + public init( + handler: Handler, + decoder: Decoder + ) where Output == Void, Encoder == VoidEncoder + + /// 1. Decode the invocation event using `self.decoder` + /// 2. Create a concrete `LambdaResponseWriter` that maps calls to `write(_:)` with the `responseWriter`s `writeAndFinish(_:)` + /// 2. Call the underlying `self.handler.handle()` method with the decoded event data, the concrete `LambdaResponseWriter`, + /// and the `LambdaContext`. + public mutating func handle( + _ request: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws +} +``` + +#### Handler as a Closure + +To create a Lambda function using the current API, a user first has to create an object and conform it to one of the +handler protocols by implementing the initializer and the `handle(...)` function. Now that `LambdaRuntime` is public, +this verbosity can very easily be simplified. + +##### ClosureHandler + +This handler is generic over any `Event` type conforming to `Decodable` and any `Output` type conforming to `Encodable` +or `Void`. + +```swift +public struct ClosureHandler: LambdaHandler { + /// Initialize with a closure handler over generic Input and Output types + public init(body: @escaping (Event, LambdaContext) async throws -> Output) where Output: Encodable + /// Initialize with a closure handler over a generic Input type (Void Output). + public init(body: @escaping (Event, LambdaContext) async throws -> Void) where Output == Void + /// The business logic of the Lambda function. + public func handle(_ event: Event, context: LambdaContext) async throws -> Output +} +``` + +Given that `ClosureHandler` conforms to `LambdaHandler`: + +1. We can extend the `LambdaRuntime` initializer such that it accepts a closure as an argument. +2. Within the initializer, the closure handler is wrapped with `LambdaCodableAdapter`. + +```swift +extension LambdaRuntime { + /// Initialize a LambdaRuntime with a closure handler over generic Event and Output types. + /// This initializer bolts on encoding/decoding logic by wrapping the closure handler with + /// LambdaCodableAdapter. + public init( + body: @escaping (Event, LambdaContext) async throws -> Output + ) where Handler == LambdaCodableAdapter, Event, Output, JSONDecoder, JSONEncoder> + + /// Same as above but for handlers with a void output + public init( + body: @escaping (Event, LambdaContext) async throws -> Void + ) where Handler == LambdaCodableAdapter, Event, Void, JSONDecoder, VoidEncoder> +} +``` + +We can now significantly reduce the verbosity and leverage Swift's trailing closure syntax to cleanly create and run a +Lambda function, abstracting away the decoding and encoding logic from the user: + +```swift +/// The type the handler will use as input +struct Input: Decodable { + var message: String +} + +/// The type the handler will output +struct Greeting: Encodable { + var echoedMessage: String +} + +/// A simple Lambda function that echoes the input +let runtime = LambdaRuntime { (event: Input, context: LambdaContext) in + Greeting(echoedMessage: event.message) +} + +try await runtime.run() +``` + +We also add a `StreamingClosureHandler` conforming to `StreamingLambdaHandler` for use-cases where the user wants to +handle encoding/decoding themselves: + +```swift +public struct StreamingClosureHandler: StreamingLambdaHandler { + + public init( + body: @escaping sending (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> () + ) + + public func handle( + _ request: ByteBuffer, + responseWriter: LambdaResponseStreamWriter, + context: LambdaContext + ) async throws +} + +extension LambdaRuntime { + public init( + body: @escaping sending (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> () + ) +} +``` + +### Alternatives considered + +#### [UInt8] instead of ByteBuffer + +We considered using `[UInt8]` instead of `ByteBuffer` in the base `LambdaHandler` API. We decided to use `ByteBuffer` +for two reasons. + +1. 99% of use-cases will use the JSON codable API and will not directly get in touch with ByteBuffer anyway. For those + users it does not matter if the base API uses `ByteBuffer` or `[UInt8]`. +2. The incoming and outgoing data must be in the `ByteBuffer` format anyway, as Lambda uses SwiftNIO under the hood and + SwiftNIO uses `ByteBuffer` in its APIs. By using `ByteBuffer` we can save a copies to and from `[UInt8]`. This will + reduce the invocation time for all users. +3. The base `LambdaHandler` API is most likely mainly being used by developers that want to integrate their web + framework with Lambda (examples: Vapor, Hummingbird, ...). Those developers will most likely prefer to get the data + in the `ByteBuffer` format anyway, as their lower level networking stack also depends on SwiftNIO. + +#### Users create a LambdaResponse, that supports streaming instead of being passed a LambdaResponseStreamWriter + +Instead of passing the `LambdaResponseStreamWriter` in the invocation we considered a new type `LambdaResponse`, that +users must return in the `StreamingLambdaHandler`. + +Its API would look like this: + +```swift +/// A response returned from a ``LambdaHandler``. +/// The response can be empty, a single ByteBuffer or a response stream. +public struct LambdaResponse { + /// A writer to be used when creating a streamed response. + public struct Writer { + /// Writes data to the response stream + public func write(_ byteBuffer: ByteBuffer) async throws + /// Closes off the response stream + public func finish() async throws + /// Writes the `byteBuffer` to the response stream and subsequently closes the stream + public func writeAndFinish(_ byteBuffer: ByteBuffer) async throws + } + + /// Creates an empty lambda response + public init() + + /// Creates a LambdaResponse with a fixed ByteBuffer. + public init(_ byteBuffer: ByteBuffer) + + /// Creates a streamed lambda response. Use the ``Writer`` to send + /// response chunks on the stream. + public init(_ stream: @escaping sending (Writer) async throws -> ()) +} +``` + +The `StreamingLambdaHandler` would look like this: + +```swift +/// The base LambdaHandler protocol +public protocol StreamingLambdaHandler { + /// The business logic of the Lambda function + /// - Parameters: + /// - event: The invocation's input data + /// - context: The LambdaContext containing the invocation's metadata + /// - Returns: A LambdaResponse, that can be streamed + mutating func handle( + _ event: ByteBuffer, + context: LambdaContext + ) async throws -> LambdaResponse +} +``` + +There are pros and cons for the API that returns the `LambdaResponses` and there are pros and cons for the API that +receives a `LambdaResponseStreamWriter` as a parameter. + +Concerning following structured concurrency principles the approach that receives a `LambdaResponseStreamWriter` as a +parameter has benefits as the lifetime of the handle function is tied to the invocation runtime. The approach that +returns a `LambdaResponse` splits the invocation into two separate function calls. First the handle method is invoked, +second the `LambdaResponse` writer closure is invoked. This means that it is impossible to use Swift APIs that use +`with` style lifecycle management patterns from before creating the response until sending the full response stream off. +For example, users instrumenting their lambdas with Swift tracing likely can not use the `withSpan` API for the full +lifetime of the request, if they return a streamed response. + +However, if it comes to consistency with the larger Swift on server ecosystem, the API that returns a `LambdaResponse` +is likely the better choice. Hummingbird v2, OpenAPI and the new Swift gRPC v2 implementation all use this approach. +This might be due to the fact that writing middleware becomes easier, if a Response is explicitly returned. + +We decided to implement the approach in which a `LambdaResponseStreamWriter` is passed to the function, since the +approach in which a `LambdaResponse` is returned can trivially be built on top of it. This is not true vice versa. + +We welcome the discussion on this topic and are open to change our minds and API here. + +#### Adding a function `addBackgroundTask(_ body: sending @escaping () async -> ())` in `LambdaContext` + +Initially we proposed an explicit `addBackgroundTask(_:)` function in `LambdaContext` that users could call from their +handler object to schedule a background task to be run after the result is reported to AWS. We received feedback that +this approach for supporting background tasks does not exhibit structured concurrency, as code could still be in +execution after leaving the scope of the `handle(...)` function. + +For handlers conforming to the `StreamingLambdaHandler`, `addBackgroundTask(_:)` was anyways unnecessary as background +work could be executed in a structured concurrency manner within the `handle(...)` function after the call to +`LambdaResponseStreamWriter.finish()`. + +For handlers conforming to the `LambdaHandler` protocol, we considered extending `LambdaHandler` with a +`performPostHandleWork(...)` function that will be called after the `handle` function by the library. Users wishing to +add background work can override this function in their `LambdaHandler` conforming object. + +```swift +public protocol LambdaHandler { + associatedtype Event + associatedtype Output + + func handle(_ event: Event, context: LambdaContext) async throws -> Output + + func performPostHandleWork(...) async throws -> Void +} + +extension LambdaHandler { + // User's can override this function if they wish to perform background work + // after returning a response from ``handle``. + func performPostHandleWork(...) async throws -> Void { + // nothing to do + } +} +``` + +Yet this poses difficulties when the user wishes to use any state created in the `handle(...)` function as part of the +background work. + +In general, the most common use-case for this library will be to implement simple Lambda functions that do not have +requirements for response streaming, nor to perform any background work after returning the output. To keep things easy +for the common use-case, and with Swift's principle of progressive disclosure of complexity in mind, we settled on three +handler protocols: + +1. `LambdaHandler`: Most common use-case. JSON-in, JSON-out. Does not support background work execution. An intuitive + `handle(event: Event, context: LambdaContext) -> Output` API that is simple to understand, i.e. users are not exposed + to the concept of sending their response through a writer. `LambdaHandler` can be very cleanly implemented and used + with `LambdaRuntime`, especially with `ClosureHandler`. +2. `LambdaWithBackgroundProcessingHandler`: If users wish to augment their `LambdaHandler` with the ability to run + background tasks, they can easily migrate. A user simply has to: + 1. Change the conformance to `LambdaWithBackgroundProcessingHandler`. + 2. Add an additional `outputWriter: some LambdaResponseWriter` argument to the `handle` function. + 3. Replace the `return ...` with `outputWriter.write(...)`. + 4. Implement any background work after `outputWriter.write(...)`. +3. `StreamingLambdaHandler`: This is the base handler protocol which is intended to be used directly only for advanced + use-cases. Users are provided the invocation event as a `ByteBuffer` and a `LambdaResponseStreamWriter` where the + computed result (as `ByteBuffer`) can either be streamed (with repeated calls to `write(_:)`) or sent all at once + (with a single call to `writeAndFinish(_:)`). After closing the `LambdaResponseStreamWriter`, any background work can + be implemented. + +#### Making LambdaResponseStreamWriter and LambdaResponseWriter ~Copyable + +We initially proposed to make the `LambdaResponseStreamWriter` and `LambdaResponseWriter` protocols `~Copyable`, with +the functions that close the response having the `consuming` ownership keyword. This was so that the compiler could +enforce the restriction of not being able to interact with the writer after the response stream has closed. + +However, non-copyable types do not compose nicely and add complexity for users. Further, for the compiler to actually +enforce the `consuming` restrictions, user's have to explicitly mark the writer argument as `consuming` in the `handle` +function. + +Therefore, throwing appropriate errors to prevent abnormal interaction with the writers seems to be the simplest +approach. + +### A word about versioning + +We are aware that AWS Lambda Runtime has not reached a proper 1.0. We intend to keep the current implementation around +at 1.0-alpha. We don't want to change the current API without releasing a new major. We think there are lots of adopters +out there that depend on the API in v1. Because of this we intend to release the proposed API here as AWS Lambda Runtime +v2. diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/.shellcheckrc b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/.shellcheckrc new file mode 100644 index 00000000..95ac895f --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/.shellcheckrc @@ -0,0 +1 @@ +disable=all \ No newline at end of file diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-01-package-init.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-01-package-init.sh new file mode 100644 index 00000000..600d8880 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-01-package-init.sh @@ -0,0 +1,2 @@ +# Create a project directory +mkdir Palindrome && cd Palindrome diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-02-package-init.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-02-package-init.sh new file mode 100644 index 00000000..dfa52a08 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-02-package-init.sh @@ -0,0 +1,5 @@ +# Create a project directory +mkdir Palindrome && cd Palindrome + +# create a skeleton project +swift package init --type executable 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 75% 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 index aa6e4926..54b1c96b 100644 --- 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 @@ -1,6 +1,8 @@ # Create a project directory -mkdir SquareNumber && cd SquareNumber +mkdir Palindrome && cd Palindrome + # create a skeleton project swift package init --type executable + # open Xcode in the current directory -xed . \ No newline at end of file +xed . 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 82% 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 index fc946011..a8fcbf5d 100644 --- 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 @@ -1,8 +1,11 @@ # Create a project directory -mkdir SquareNumber && cd SquareNumber +mkdir Palindrome && cd Palindrome + # create a skeleton project swift package init --type executable + # open Xcode in the current directory xed . + # alternatively, you may open VSCode code . \ No newline at end of file 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 71% 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 index dada42b4..155bda8d 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-01-package.swift +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-01-package.swift @@ -1,8 +1,8 @@ -// swift-tools-version:5.8 +// swift-tools-version:6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( - name: "SquareNumberLambda", + name: "Palindrome" ) 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 65% 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 index 40b9f784..45be7f2c 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-02-package.swift +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-02-package.swift @@ -1,11 +1,11 @@ -// swift-tools-version:5.8 +// swift-tools-version:6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( - name: "SquareNumberLambda", + name: "Palindrome", platforms: [ - .macOS(.v12), - ], + .macOS(.v15) + ] ) 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 66% 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 index 88258ed8..7fceaf97 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-03-package.swift +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-03-package.swift @@ -1,14 +1,14 @@ -// swift-tools-version:5.8 +// swift-tools-version:6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( - name: "SquareNumberLambda", + name: "Palindrome", platforms: [ - .macOS(.v12), + .macOS(.v15) ], dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), - ], + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "2.0.0") + ] ) 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 56% 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 index b1d69ed5..889a4e40 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-04-package.swift +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-04-package.swift @@ -1,17 +1,17 @@ -// swift-tools-version:5.8 +// swift-tools-version:6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( - name: "SquareNumberLambda", + name: "Palindrome", platforms: [ - .macOS(.v12), + .macOS(.v15) ], products: [ - .executable(name: "SquareNumberLambda", targets: ["SquareNumberLambda"]), + .executable(name: "PalindromeLambda", targets: ["PalindromeLambda"]) ], dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), - ], + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "2.0.0") + ] ) 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 59% 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 index bd9f7290..34c7f3a1 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-05-package.swift +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-05-package.swift @@ -1,26 +1,26 @@ -// swift-tools-version:5.8 +// swift-tools-version:6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( - name: "SquareNumberLambda", + name: "Palindrome", platforms: [ - .macOS(.v12), + .macOS(.v15) ], products: [ - .executable(name: "SquareNumberLambda", targets: ["SquareNumberLambda"]), + .executable(name: "PalindromeLambda", targets: ["PalindromeLambda"]) ], dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "2.0.0") ], targets: [ .executableTarget( - name: "SquareNumberLambda", + name: "PalindromeLambda", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") ], - path: "." - ), + path: "Sources" + ) ] ) diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-01-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-01-main.swift new file mode 100644 index 00000000..72751f48 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-01-main.swift @@ -0,0 +1,4 @@ +// the data structure to represent the input parameter +struct Request: Decodable { + let text: String +} diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-02-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-02-main.swift new file mode 100644 index 00000000..5b0d8e2b --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-02-main.swift @@ -0,0 +1,11 @@ +// the data structure to represent the input parameter +struct Request: Decodable { + let text: String +} + +// the data structure to represent the response parameter +struct Response: Encodable { + let text: String + let isPalindrome: Bool + let message: String +} diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-03-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-03-main.swift new file mode 100644 index 00000000..da6de3de --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-03-main.swift @@ -0,0 +1,17 @@ +// the data structure to represent the input parameter +struct Request: Decodable { + let text: String +} + +// the data structure to represent the response parameter +struct Response: Encodable { + let text: String + let isPalindrome: Bool + let message: String +} + +// the business function +func isPalindrome(_ text: String) -> Bool { + let cleanedText = text.lowercased().filter { $0.isLetter } + return cleanedText == String(cleanedText.reversed()) +} diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-04-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-04-main.swift new file mode 100644 index 00000000..4b14c43b --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-04-main.swift @@ -0,0 +1,19 @@ +import AWSLambdaRuntime + +// the data structure to represent the input parameter +struct Request: Decodable { + let text: String +} + +// the data structure to represent the response parameter +struct Response: Encodable { + let text: String + let isPalindrome: Bool + let message: String +} + +// the business function +func isPalindrome(_ text: String) -> Bool { + let cleanedText = text.lowercased().filter { $0.isLetter } + return cleanedText == String(cleanedText.reversed()) +} diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-05-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-05-main.swift new file mode 100644 index 00000000..f308e04b --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-05-main.swift @@ -0,0 +1,25 @@ +import AWSLambdaRuntime + +// the data structure to represent the input parameter +struct Request: Decodable { + let text: String +} + +// the data structure to represent the response parameter +struct Response: Encodable { + let text: String + let isPalindrome: Bool + let message: String +} + +// the business function +func isPalindrome(_ text: String) -> Bool { + let cleanedText = text.lowercased().filter { $0.isLetter } + return cleanedText == String(cleanedText.reversed()) +} + +// the lambda handler function +let runtime = LambdaRuntime { + (event: Request, context: LambdaContext) -> Response in + +} diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-06-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-06-main.swift new file mode 100644 index 00000000..0a382bc1 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-06-main.swift @@ -0,0 +1,32 @@ +import AWSLambdaRuntime + +// the data structure to represent the input parameter +struct Request: Decodable { + let text: String +} + +// the data structure to represent the response parameter +struct Response: Encodable { + let text: String + let isPalindrome: Bool + let message: String +} + +// the business function +func isPalindrome(_ text: String) -> Bool { + let cleanedText = text.lowercased().filter { $0.isLetter } + return cleanedText == String(cleanedText.reversed()) +} + +// the lambda handler function +let runtime = LambdaRuntime { + (event: Request, context: LambdaContext) -> Response in + + // call the business function and return a response + let result = isPalindrome(event.text) + return Response( + text: event.text, + isPalindrome: result, + message: "Your text is \(result ? "a" : "not a") palindrome" + ) +} diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-07-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-07-main.swift new file mode 100644 index 00000000..c777c23b --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-07-main.swift @@ -0,0 +1,35 @@ +import AWSLambdaRuntime + +// the data structure to represent the input parameter +struct Request: Decodable { + let text: String +} + +// the data structure to represent the response parameter +struct Response: Encodable { + let text: String + let isPalindrome: Bool + let message: String +} + +// the business function +func isPalindrome(_ text: String) -> Bool { + let cleanedText = text.lowercased().filter { $0.isLetter } + return cleanedText == String(cleanedText.reversed()) +} + +// the lambda handler function +let runtime = LambdaRuntime { + (event: Request, context: LambdaContext) -> Response in + + // call the business function and return a response + let result = isPalindrome(event.text) + return Response( + text: event.text, + isPalindrome: result, + message: "Your text is \(result ? "a" : "not a") palindrome" + ) +} + +// start the runtime +try await runtime.run() diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-02-console-output.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-02-console-output.sh new file mode 100644 index 00000000..11c59fb9 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-02-console-output.sh @@ -0,0 +1,2 @@ +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/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-03-curl.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-03-curl.sh new file mode 100644 index 00000000..c8f991a2 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-03-curl.sh @@ -0,0 +1,5 @@ +curl --header "Content-Type: application/json" \ + --request POST \ + --data '{"text": "Was it a car or a cat I saw?"}' \ + http://127.0.0.1:7000/invoke + diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-04-curl.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-04-curl.sh new file mode 100644 index 00000000..7928ac36 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-04-curl.sh @@ -0,0 +1,7 @@ +curl --header "Content-Type: application/json" \ + --request POST \ + --data '{"text": "Was it a car or a cat I saw?"}' \ + http://127.0.0.1:7000/invoke + +{"message":"Your text is a palindrome","isPalindrome":true,"text":"Was it a car or a cat I saw?"} + diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-06-terminal.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-06-terminal.sh new file mode 100644 index 00000000..59fc671e --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-06-terminal.sh @@ -0,0 +1 @@ +swift run diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-07-terminal.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-07-terminal.sh new file mode 100644 index 00000000..1348bddc --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-07-terminal.sh @@ -0,0 +1,6 @@ +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 : [AWSLambdaRuntime] LocalLambdaServer started and listening on 127.0.0.1:7000, receiving events on /invoke diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-02-plugin-archive.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-02-plugin-archive.sh new file mode 100644 index 00000000..1f5ca9d8 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-02-plugin-archive.sh @@ -0,0 +1 @@ +swift package archive --allow-network-connections docker diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-03-plugin-archive.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-03-plugin-archive.sh new file mode 100644 index 00000000..c760c981 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-03-plugin-archive.sh @@ -0,0 +1,20 @@ +swift package archive --allow-network-connections docker + +------------------------------------------------------------------------- +building "palindrome" in docker +------------------------------------------------------------------------- +updating "swift:amazonlinux2" docker image + amazonlinux2: Pulling from library/swift + Digest: sha256:df06a50f70e2e87f237bd904d2fc48195742ebda9f40b4a821c4d39766434009 +Status: Image is up to date for swift:amazonlinux2 + docker.io/library/swift:amazonlinux2 +building "PalindromeLambda" + [0/1] Planning build + Building for production... + [0/2] Write swift-version-24593BA9C3E375BF.txt + Build of product 'PalindromeLambda' complete! (1.91s) +------------------------------------------------------------------------- +archiving "PalindromeLambda" +------------------------------------------------------------------------- +1 archive created + * PalindromeLambda at /Users/sst/Palindrome/.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/PalindromeLambda/PalindromeLambda.zip diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-04-plugin-archive.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-04-plugin-archive.sh new file mode 100644 index 00000000..c347694e --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-04-plugin-archive.sh @@ -0,0 +1,22 @@ +swift package archive --allow-network-connections docker + +------------------------------------------------------------------------- +building "palindrome" in docker +------------------------------------------------------------------------- +updating "swift:amazonlinux2" docker image + amazonlinux2: Pulling from library/swift + Digest: sha256:df06a50f70e2e87f237bd904d2fc48195742ebda9f40b4a821c4d39766434009 +Status: Image is up to date for swift:amazonlinux2 + docker.io/library/swift:amazonlinux2 +building "PalindromeLambda" + [0/1] Planning build + Building for production... + [0/2] Write swift-version-24593BA9C3E375BF.txt + Build of product 'PalindromeLambda' complete! (1.91s) +------------------------------------------------------------------------- +archiving "PalindromeLambda" +------------------------------------------------------------------------- +1 archive created + * PalindromeLambda at /Users/sst/Palindrome/.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/PalindromeLambda/PalindromeLambda.zip + +cp .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/PalindromeLambda/PalindromeLambda.zip ~/Desktop 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 99% 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 index 9e66559e..6c9513cd 100644 --- 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 @@ -1,7 +1,5 @@ -# # --region the AWS Region to send the command # --function-name the name of your function # --cli-binary-format tells the cli to use raw data as input (default is base64) # --payload the payload to pass to your function code # result.json the name of the file to store the response from the function - 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 81% 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 index 52c3c549..00c96fcb 100644 --- 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 @@ -1,4 +1,3 @@ -# # --region the AWS Region to send the command # --function-name the name of your function # --cli-binary-format tells the cli to use raw data as input (default is base64) @@ -7,8 +6,8 @@ aws lambda invoke \ --region us-west-2 \ - --function-name SquaredNumberLambda \ + --function-name PalindromeLambda \ --cli-binary-format raw-in-base64-out \ - --payload '{"number":3}' \ + --payload '{"text": "Was it a car or a cat I saw?"}' \ result.json 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 82% 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 index 569e75ea..032c722d 100644 --- 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 @@ -1,4 +1,3 @@ -# # --region the AWS Region to send the command # --function-name the name of your function # --cli-binary-format tells the cli to use raw data as input (default is base64) @@ -7,9 +6,9 @@ aws lambda invoke \ --region us-west-2 \ - --function-name SquaredNumberLambda \ + --function-name PalindromeLambda \ --cli-binary-format raw-in-base64-out \ - --payload '{"number":3}' \ + --payload '{"text": "Was it a car or a cat I saw?"}' \ result.json { 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 83% 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 index c31e7e4b..52b17573 100644 --- 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 @@ -1,4 +1,3 @@ -# # --region the AWS Region to send the command # --function-name the name of your function # --cli-binary-format tells the cli to use raw data as input (default is base64) @@ -7,9 +6,9 @@ aws lambda invoke \ --region us-west-2 \ - --function-name SquaredNumberLambda \ + --function-name PalindromeLambda \ --cli-binary-format raw-in-base64-out \ - --payload '{"number":3}' \ + --payload '{"text": "Was it a car or a cat I saw?"}' \ result.json { 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 73% 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 index 54a198bd..bb4889b6 100644 --- 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 @@ -1,4 +1,3 @@ -# # --region the AWS Region to send the command # --function-name the name of your function # --cli-binary-format tells the cli to use raw data as input (default is base64) @@ -7,9 +6,9 @@ aws lambda invoke \ --region us-west-2 \ - --function-name SquaredNumberLambda \ + --function-name PalindromeLambda \ --cli-binary-format raw-in-base64-out \ - --payload '{"number":3}' \ + --payload '{"text": "Was it a car or a cat I saw?"}' \ result.json { @@ -18,5 +17,5 @@ aws lambda invoke \ } cat result.json -{"result":9} +{"text":"Was it a car or a cat I saw?","isPalindrome":true,"message":"Your text is a palindrome"} diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-10-regions.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-10-regions.png new file mode 100644 index 00000000..afe97b53 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-10-regions.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-20-dashboard.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-20-dashboard.png new file mode 100644 index 00000000..b48ea591 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-20-dashboard.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-30-create-function.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-30-create-function.png new file mode 100644 index 00000000..e3bd131f Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-30-create-function.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-40-select-zip-file.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-40-select-zip-file.png new file mode 100644 index 00000000..da4ff924 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-40-select-zip-file.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-50-upload-zip.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-50-upload-zip.png new file mode 100644 index 00000000..89eedab9 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-50-upload-zip.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-60-prepare-test-event.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-60-prepare-test-event.png new file mode 100644 index 00000000..2f7b15ed Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-60-prepare-test-event.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-70-view-invocation-response.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-70-view-invocation-response.png new file mode 100644 index 00000000..f4f712bc Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-70-view-invocation-response.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-function.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-function.png new file mode 100644 index 00000000..f205f47b Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-function.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-role.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-role.png new file mode 100644 index 00000000..2a335d5d Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-role.png differ 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/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-terminal-package-init.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-terminal-package-init.png new file mode 100644 index 00000000..d12e5f9d Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-terminal-package-init.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode@2x.png new file mode 100644 index 00000000..96347ebf Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode~dark@2x.png new file mode 100644 index 00000000..bdb286e6 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode~dark@2x.png differ 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/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-01-compile-run@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-01-compile-run@2x.png new file mode 100644 index 00000000..7dfc7f64 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-01-compile-run@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.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 new file mode 100644 index 00000000..3315a90c Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-01-compile-run~dark@2x.png differ 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/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-03-select-region@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-03-select-region@2x.png new file mode 100644 index 00000000..8460945b Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-03-select-region@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-04-select-lambda@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-04-select-lambda@2x.png new file mode 100644 index 00000000..c79c495f Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-04-select-lambda@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.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 new file mode 100644 index 00000000..2abd5056 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-04-select-lambda~dark@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-05-create-function@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-05-create-function@2x.png new file mode 100644 index 00000000..1c123d5a Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-05-create-function@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.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 new file mode 100644 index 00000000..c23355a9 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-05-create-function~dark@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-06-create-function@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-06-create-function@2x.png new file mode 100644 index 00000000..b8048d19 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-06-create-function@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.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 new file mode 100644 index 00000000..2fbdf277 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-06-create-function~dark@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-07-upload-zip@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-07-upload-zip@2x.png new file mode 100644 index 00000000..0e4ba22c Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-07-upload-zip@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.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 new file mode 100644 index 00000000..cf73d7f9 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-07-upload-zip~dark@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-08-upload-zip@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-08-upload-zip@2x.png new file mode 100644 index 00000000..ef3d8160 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-08-upload-zip@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.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 new file mode 100644 index 00000000..4273f810 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-08-upload-zip~dark@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-09-test-lambda@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-09-test-lambda@2x.png new file mode 100644 index 00000000..e9e7a309 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-09-test-lambda@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.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 new file mode 100644 index 00000000..61cf6147 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-09-test-lambda~dark@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.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 new file mode 100644 index 00000000..e962e7c2 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-10-test-lambda-result@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.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 new file mode 100644 index 00000000..54ca5198 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-10-test-lambda-result~dark@2x.png differ 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 0470c73b..00000000 --- a/Sources/AWSLambdaRuntime/Docs.docc/index.md +++ /dev/null @@ -1,370 +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://fabianfett.de/getting-started-with-swift-aws-lambda-runtime) 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 hang, 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. - -### Using EventLoopLambdaHandler - - Performance sensitive Lambda functions may choose to use a more complex API which allows user code to run on the same thread as the networking handlers. Swift AWS Lambda Runtime uses [SwiftNIO](https://github.com/apple/swift-nio) as its underlying networking engine which means the APIs are based on [SwiftNIO](https://github.com/apple/swift-nio) concurrency primitives like the `EventLoop` and `EventLoopFuture`. For example: - - ```swift - // Import the modules - import AWSLambdaRuntime - import AWSLambdaEvents - import NIO - - // Our Lambda handler, conforms to EventLoopLambdaHandler - struct Handler: EventLoopLambdaHandler { - typealias In = SNS.Message // Request type - typealias Out = Void // Response type - - // In this example we are receiving an SNS Message, with no response (Void). - func handle(context: Lambda.Context, event: In) -> EventLoopFuture { - ... - context.eventLoop.makeSucceededFuture(Void()) - } - } - - Lambda.run(Handler()) - ``` - - Beyond the small cognitive complexity of using the `EventLoopFuture` based APIs, note these APIs should be used with extra care. An [`EventLoopLambdaHandler`][ellh] will execute the user code on the same `EventLoop` (thread) as the library, making processing faster but requiring the user code to never call blocking APIs as it might prevent the underlying process from functioning. - -## 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 60% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/quick-setup.md rename to Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md index 151fb8c3..426a024d 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/quick-setup.md +++ b/Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md @@ -10,7 +10,7 @@ For a detailed step-by-step instruction, follow the tutorial instead. For the impatient, keep reading. -## High-level instructions +### High-level instructions Follow these 6 steps to write, test, and deploy a Lambda function in Swift. @@ -23,7 +23,7 @@ swift package init --type executable 2. Add dependencies on `AWSLambdaRuntime` library ```swift -// swift-tools-version:5.8 +// swift-tools-version:6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -31,21 +31,21 @@ import PackageDescription let package = Package( name: "YourProjetName", platforms: [ - .macOS(.v12), + .macOS(.v15), ], products: [ - .executable(name: "YourFunctionName", targets: ["YourFunctionName"]), + .executable(name: "MyFirstLambdaFunction", targets: ["MyFirstLambdaFunction"]), ], dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "2.0.0"), ], targets: [ .executableTarget( - name: "YourFunctionName", + name: "MyFirstLambdaFunction", dependencies: [ .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), ], - path: "." + path: "Sources" ), ] ) @@ -53,50 +53,50 @@ let package = Package( 3. Write your function code. -> Be sure to rename the `main.swift` file to something else. +Create an instance of `LambdaRuntime` and pass a function as a closure. The function has this signature: `(_: Event, context: LambdaContext) async throws -> Output` (as defined in the `LambdaHandler` protocol). `Event` must be `Decodable`. `Output` must be `Encodable`. -Extends the `SimpleLambdaHandler` protocol and implement `handle(_:context)`. +If your Lambda function is invoked by another AWS service, use the `AWSLambdaEvent` library at [https://github.com/swift-server/swift-aws-lambda-events](https://github.com/swift-server/swift-aws-lambda-events) to represent the input event. - -If your Lambda function is invoked by another AWS service, use the `AWSLambdaEvent` library at [https://github.com/swift-server/swift-aws-lambda-events](https://github.com/swift-server/swift-aws-lambda-events) +Finally, call `runtime.run()` to start the event loop. ```swift -import AWSLambdaRuntime - -struct Input: Codable { - let number: Double +// the data structure to represent the input parameter +struct HelloRequest: Decodable { + let name: String + let age: Int } -struct Number: Codable { - let result: Double +// the data structure to represent the output response +struct HelloResponse: Encodable { + let greetings: String } -@main -struct SquareNumberHandler: SimpleLambdaHandler { - typealias Event = Input - typealias Output = Number - - func handle(_ event: Input, context: LambdaContext) async throws -> Number { - Number(result: event.number * event.number) - } +// the Lambda runtime +let runtime = LambdaRuntime { + (event: HelloRequest, context: LambdaContext) in + + HelloResponse( + greetings: "Hello \(event.name). You look \(event.age > 30 ? "younger" : "older") than your age." + ) } + +// start the loop +try await runtime.run() ``` 4. Test your code locally ```sh -export LOCAL_LAMBDA_SERVER_ENABLED=true - -swift run +swift run # this starts a local server on port 7000 # Switch to another Terminal tab curl --header "Content-Type: application/json" \ --request POST \ - --data '{"number": 3}' \ + --data '{"name": "Seb", "age": 50}' \ http://localhost:7000/invoke -{"result":9} +{"greetings":"Hello Seb. You look younger than your age."} ``` 5. Build and package your code for AWS Lambda @@ -106,27 +106,27 @@ AWS Lambda runtime runs on Amazon Linux. You must compile your code for Amazon L > Be sure to have [Docker](https://docs.docker.com/desktop/install/mac-install/) installed for this step. ```sh -swift package --disable-sandbox plugin archive +swift package --allow-network-connections docker archive ------------------------------------------------------------------------- -building "squarenumberlambda" in docker +building "MyFirstLambdaFunction" in docker ------------------------------------------------------------------------- updating "swift:amazonlinux2" docker image amazonlinux2: Pulling from library/swift Digest: sha256:5b0cbe56e35210fa90365ba3a4db9cd2b284a5b74d959fc1ee56a13e9c35b378 Status: Image is up to date for swift:amazonlinux2 docker.io/library/swift:amazonlinux2 -building "SquareNumberLambda" +building "MyFirstLambdaFunction" Building for production... ... ------------------------------------------------------------------------- -archiving "SquareNumberLambda" +archiving "MyFirstLambdaFunction" ------------------------------------------------------------------------- 1 archive created - * SquareNumberLambda at /Users/YourUserName/SquareNumberLambda/.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/SquareNumberLambda/SquareNumberLambda.zip + * MyFirstLambdaFunction at /Users/YourUserName/MyFirstLambdaFunction/.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyFirstLambdaFunction/MyFirstLambdaFunction.zip -cp .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/SquareNumberLambda/SquareNumberLambda.zip ~/Desktop +cp .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyFirstLambdaFunction/MyFirstLambdaFunction.zip ~/Desktop ``` 6. Deploy on AWS Lambda @@ -139,9 +139,9 @@ cp .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/SquareNumberLambda - Select **Provide your own bootstrap on Amazon Linux 2** as **Runtime** - Select an **Architecture** that matches the one of the machine where you build the code. Select **x86_64** when you build on Intel-based Macs or **arm64** for Apple Silicon-based Macs. - Upload the ZIP create during step 5 -- Select the **Test** tab, enter a test event such as `{"number":3}` and select **Test** +- Select the **Test** tab, enter a test event such as `{"name": "Seb", "age": 50}` and select **Test** -If the test succeeds, you will see the result: '{"result":9}' +If the test succeeds, you will see the result: `{"greetings":"Hello Seb. You look younger than your age."}`. Congratulations 🎉! You just wrote, test, build, and deployed a Lambda function written in Swift. diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/01-overview.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/01-overview.tutorial similarity index 83% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/01-overview.tutorial rename to Sources/AWSLambdaRuntime/Docs.docc/tutorials/01-overview.tutorial index f4a922b1..5ca896db 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/01-overview.tutorial +++ b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/01-overview.tutorial @@ -14,5 +14,5 @@ It's a beginners' tutorial. The business logic of the function is very simple, i If you have any questions or recommendations, please [leave your feedback on GitHub](https://github.com/swift-server/swift-aws-lambda-runtime/issues) so that you can get your question answered and this tutorial can be improved. -*The following instructions were recorded on April 15, 2023 and the AWS Management Console may have changed since then. Feel free to raise an issue if you spot differences with our screenshots* +*The following instructions were recorded on January 2025 and the AWS Management Console may have changed since then. Feel free to raise an issue if you spot differences with our screenshots* } \ No newline at end of file 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 57% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/03-write-function.tutorial rename to Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-write-function.tutorial index 428e3c71..d062e224 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/03-write-function.tutorial +++ b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-write-function.tutorial @@ -1,6 +1,10 @@ @Tutorial(time: 15) { @Intro(title: "Write your first Lambda function") { - Learn how to create your project, add dependencies, and create and test your first Lambda function in Swift. + Learn how to create your project, add dependencies, and create and test your first Lambda function in Swift. + + In this example, we will create a Lambda function that receives a text and checks if this text is a palindrome or not. + + A palindrome is a word or phrase that reads the same forward and backward. } @Section(title: "Initialize a new project") { @@ -51,7 +55,7 @@ @Step { In the Xcode editor, replace the content of `Package.swift` with the file on the right side of the screen. - It defines a package for a project named `SquareNumberLambda`. The package name only matters when you build a library that is used by other Swift packages. + It defines a package for a project named `Palindrome`. The package name only matters when you build a library that is used by other Swift packages. > Comments are important here, do not skip them. They define the minimum version of Swift to use. @Code(name: "Package.swift", file: 03-02-01-package.swift) @@ -72,7 +76,7 @@ @Step { Add the `target` section. - In the `targets` section you specify your own targets. They are pretty comparable to targets you specify within an Xcode project (that's probably why they share the name 😎). In our example we only want to create an executable that is called `SquareNumberLambda`. An executable must have an entrypoint. This can be either a `main.swift` or an object that is marked with `@main`. For Lambda we will use the `@main` approach. + In the `targets` section you specify your own targets. They are pretty comparable to targets you specify within an Xcode project (that's probably why they share the name 😎). In our example we only want to create an executable that is called `PalindromeLambda`. An executable must have an entrypoint. This can be either a `main.swift` or an object that is marked with `@main`. For Lambda we will use the `@main` approach. @Code(name: "Package.swift", file: 03-02-04-package.swift) } @@ -94,58 +98,65 @@ } @Steps { + @Step { - Rename the file `main.swift` to something else. I typically use `Lambda.swift`. - - The `AWSLambdaRuntime` use the [`@main`](https://github.com/apple/swift-evolution/blob/main/proposals/0281-main-attribute.md) directive to designate the entry point in your code. - - >A `main.swift` file is always considered to be an entry point, even if it has no top-level code. Because of this, placing the @main-designated type in a `main.swift` file is an error. - - @Image(source: 03-03-01-rename-file, alt: "Rename the file in Xcode IDE") + Open the `main.swift` file, remove the code generated and write the code to represent the request sent to your Lambda function. + + Input parameters must conform to the `Decodable` protocol. This ensures that your Lambda function accepts any JSON input. + + > When your function is triggered by another AWS service, we modeled most of the input and output data format for you. You can add the dependency on [https://github.com/swift-server/swift-aws-lambda-events](https://github.com/swift-server/swift-aws-lambda-events) and import `AWSLambdaEvents` in your code. + + @Code(name: "main.swift", file: 03-03-01-main.swift) + } + + @Step { + Write the code to represent the response returned by your Lambda function. + + Output parameters must conform to the `Encodable` protocol. This ensures that your Lambda function returns a valid JSON output. Your function might also return `Void` if it does not return any value. + + > You can also write function that stream a response back to the caller. This is useful when you have a large amount of data to return. See the [Lambda Streaming example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/Streaming) for more information. + + @Code(name: "main.swift", file: 03-03-02-main.swift) } - @Step { - Remove the code generated and create a `@main struct` that implements the protocol `SimpleLambdaHandler` + Write your business logic. - @Code(name: "Lambda.swift", file: 03-03-01-main.swift) + In real life project, this will be the most complex part of your code. It will live in spearate files or libraries. For this example, we will keep it simple and just return `true` if a `String` is a palindrome. + + @Code(name: "main.swift", file: 03-03-03-main.swift) } - + @Step { - Add an import statement to import the `AWSLambdaRuntime` library. - @Code(name: "Lambda.swift", file: 03-03-02-main.swift) + Add an `import` statement to import the `AWSLambdaRuntime` library. + + @Code(name: "main.swift", file: 03-03-04-main.swift) } - + @Step { - Write the `handle(_:context:) async throws -> Output` function as defined in `SimpleLambdaHandler` protocol. - - The `handle(_:context:)` function is the entry point of the Lambda function. - @Code(name: "Lambda.swift", file: 03-03-03-main.swift) + Create a `LambdaRuntime` struct and add a handler function that will be called by the Lambda runtime. + + This function is passed as a closure to the initializer of the `LambdaRuntime` struct. It accepts two parameters: the input event and the context. The input event is the JSON payload sent to your Lambda function. The context provides information about the function, such as the function name, memory limit, and log group name. The function returns the output event, which is the JSON payload returned by your Lambda function or Void if your function does not return any value. + + @Code(name: "main.swift", file: 03-03-05-main.swift) } @Step { - Add the definition of the input and output parameters. - - Input and Output parameters must conform to the `Codable` protocol. This ensures that your Lambda function accepts a JSON input and creates a JSON output. - Your function can use any `Codable`. When your function is triggered by another AWS service, we modeled most of the input and output data format for you. You can add the dependency on https://github.com/swift-server/swift-aws-lambda-events and import `AWSLambdaEvents` in your code. + Add the business logic to the handler function and return the response. + + In this example, we call the `isPalindrome(_:)` function to check if the input string is a palindrome. Then, we create a response with the result of the check. - @Code(name: "Lambda.swift", file: 03-03-04-main.swift) + @Code(name: "main.swift", file: 03-03-06-main.swift) } @Step { - Modify the `struct` and the `handle(_:context:)` function to use your input and output parameter types. - - @Code(name: "Lambda.swift", file: 03-03-05-main.swift) - } + Start the runtime by calling the `run()` function. - @Step { - Add your function-specific business logic. - - As mentioned earlier, this example is very simple, it just squares the number received as input. Your actual function can do whatever you want: call APIs, access a database, or any other task your business requires. + This function starts the Lambda runtime and listens for incoming requests. When a request is received, it calls the handler function with the input event and context. The handler function processes the request and returns the output event. The runtime sends the output event back to the caller. This function might `throw` an error if the runtime fails to process an event or if the handler function throws an error. This function is asynchronous and does not return until the runtime is stopped. - @Code(name: "Lambda.swift", file: 03-03-06-main.swift) + @Code(name: "main.swift", file: 03-03-07-main.swift) } - + } } @@ -160,61 +171,51 @@ @Steps { - The embedded web server starts only when an environment variable is defined. You will edit the Run step of the target scheme to include the environment variable. This will allow you to run your code from Xcode. + The embedded web server starts only when compiling in `DEBUG` mode and when the code is not run inside a Lambda function environment. You will start the test server directly from Xcode. @Step { - Select `Edit Scheme` under `SquareNumberLambda` target. + Compile and run your project. Click on the `Run` button (▶️) in Xcode. - @Image(source: 03-04-01-edit-scheme.png, alt: "Menu entry to edit schemes") + @Image(source: 03-04-01-compile-run.png, alt: "Compile and run the project") } - + @Step { - Add the `LOCAL_LAMBDA_SERVER_ENABLED` environment variable, with a value of `true` under `Run` settings. + Verify the server is correctlys started. You should see the following output in the console. - @Image(source: 03-04-02-add-variable.png, alt: "Add environment variable under Run settings") + @Code(name: "Console output", file: 03-04-02-console-output.sh) } @Step { - Compile and Run your project. You should see the following output in the console. + Now that the local server started, open a Terminal and use `curl` or any other HTTP client to POST your input payload to `127.0.0.1:7000`. - @Code(name: "Console output", file: 03-04-03-console-output.sh) + @Code(name: "curl command in a terminal", file: 03-04-03-curl.sh) } @Step { - Now that the local server started, open a Terminal and use `curl` or any other HTTP client to POST your input payload to `localhost:7000`. + When you pass `'{"text": "Was it a car or a cat I saw?"}'`, you should receive the response `{"message":"Your text is a palindrome","isPalindrome":true,"text":"Was it a car or a cat I saw?"}` + > Do not forget to stop the running scheme in Xcode (⏹️) when you're done. + @Code(name: "curl command in a terminal", file: 03-04-04-curl.sh) } - - @Step { - When you pass `{"number":3}`, you should receive the response `{"result":9}` - - > Do not forget to stop the running scheme when you're done. - @Code(name: "curl command in a terminal", file: 03-04-05-curl.sh) - } Alternatively, you can use the command line from the Terminal. - @Step { - From a Terminal, set the `LOCAL_LAMBDA_SERVER_ENABLED` environment variable to `true` - @Code(name: "curl command in a terminal", file: 03-04-06-terminal.sh) - } - @Step { Use the command `swift run` to start the local embedded web server. - @Code(name: "curl command in a terminal", file: 03-04-07-terminal.sh) + @Code(name: "curl command in a terminal", file: 03-04-06-terminal.sh) } @Step { You should see the following output in the console. - @Code(name: "curl command in a terminal", file: 03-04-08-terminal.sh) + @Code(name: "curl command in a terminal", file: 03-04-07-terminal.sh) } @Step { - Now that the local server started, open a second tab in the Terminal and use `curl` or any other HTTP client to POST your input payload to `localhost:7000`. + Now that the local server started, open a second tab in the Terminal and use `curl` or any other HTTP client to POST your input payload to `127.0.0.1:7000`. > Do not forget to stop the local server with `CTRL-C` when you're done. - @Code(name: "curl command in a terminal", file: 03-04-04-curl.sh) + @Code(name: "curl command in a terminal", file: 03-04-03-curl.sh) } } diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/04-deploy-function.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/04-deploy-function.tutorial similarity index 88% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/04-deploy-function.tutorial rename to Sources/AWSLambdaRuntime/Docs.docc/tutorials/04-deploy-function.tutorial index c2bc7552..70213532 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/04-deploy-function.tutorial +++ b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/04-deploy-function.tutorial @@ -65,7 +65,9 @@ } @Step { - On the top right side of the console, select the AWS Region where you want to deploy your Lambda function. You typically choose a Region close to your customers to minimize the network latency. For this demo, I selected **Oregon (us-west-2)** + On the top right side of the console, select the AWS Region where you want to deploy your Lambda function. + + You typically choose a Region close to your customers to minimize the network latency. For this demo, I selected **Oregon (us-west-2)** > AWS has multiple Regions across all continents. You can learn more about [AWS Global Infrastructure](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/) here. @@ -79,17 +81,16 @@ } @Step { - On the top right side of the console, select **Create function**. + On the top right side of the Lambda page, select **Create function**. @Image(source: 04-02-05-create-function.png, alt: "Create function") } @Step { - Enter a **Function name**. I choose `SquaredNumberLambda`. Select `Provide your own bootstrap on Amazon Linux 2` as **Runtime**. And select `arm64` as **Architecture** when you build on a Mac with Apple Silicon. Leave all other parameter as default, and select **Create function** on the bottom right part. + Enter a **Function name**. I choose `PalindromeLambda`. Select `Provide your own bootstrap on Amazon Linux 2` as **Runtime**. And select `arm64` as **Architecture** when you build on a Mac with Apple Silicon. Leave all other parameter as default, and select **Create function** on the bottom right part. > The runtime architecture for Lambda (`arm64` or `x86_64`) must match the one of the machine where you compiled the code. When you compiled on an Intel-based Mac, use `x86_64`. When compiling on an Apple Silicon-based Mac select `arm64`. - @Image(source: 04-02-06-create-function.png, alt: "Create function details") } @@ -106,13 +107,13 @@ } @Step { - To verify everything works well, create a test event and invoke the function from the **Test** tab in the console. Enter `MyTestEvent` as **Event name**. Enter `{"number":3}` as **Event JSON**. Then, select **Test**. + To verify everything works well, create a test event and invoke the function from the **Test** tab in the console. Enter `MyTestEvent` as **Event name**. Enter `{"text": "Was it a car or a cat I saw?"}` as **Event JSON**. Then, select **Test**. @Image(source: 04-02-09-test-lambda.png, alt: "Create function") } @Step { - When the invocation succeeds, you can see the execution details and the result: `{ "result" : 9 }`. + When the invocation succeeds, you can see the execution details and the result: `{ "message": "Your text is a palindrome","isPalindrome": true, "text": "Was it a car or a cat I saw?"}`. > The execution result also shares the execution duration, the actual memory consumed and the logs generated by the function. These are important data to help you to fine-tune your function. Providing the function with more memory will also give it more compute power, resulting in lower execution time. @@ -164,7 +165,9 @@ } @Step { - When everything goes well, you will see `{ "result" : 9}`. Congratulation 🎉 ! + When everything goes well, you will see `{"text":"Was it a car or a cat I saw?","isPalindrome":true,"message":"Your text is a palindrome"}`. + + Congratulation 🎉 ! @Code(name: "Command to type in the Terminal", file: 04-03-05-lambda-invoke.sh) 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/FoundationSupport/Context+Foundation.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift new file mode 100644 index 00000000..1dd6584c --- /dev/null +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift @@ -0,0 +1,32 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-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 FoundationJSONSupport +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import struct Foundation.Date +#endif + +@available(LambdaSwift 2.0, *) +extension LambdaContext { + /// Returns the deadline as a Date for the Lambda function execution. + /// I'm not sure how usefull it is to have this as a Date, with only seconds precision, + /// but I leave it here for compatibility with the FoundationJSONSupport trait. + var deadlineDate: Date { + // Date(timeIntervalSince1970:) expects seconds, so we convert milliseconds to seconds. + Date(timeIntervalSince1970: Double(self.deadline.millisecondsSinceEpoch()) / 1000) + } +} +#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..c8ca65ef --- /dev/null +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift @@ -0,0 +1,164 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +import Logging + +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) + } +} + +@available(LambdaSwift 2.0, *) +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 + ) + } +} +@available(LambdaSwift 2.0, *) +extension LambdaResponseStreamWriter { + /// Writes the HTTP status code and headers to the response stream. + /// + /// This method serializes the status and headers as JSON and writes them to the stream, + /// followed by eight null bytes as a separator before the response body. + /// + /// - Parameters: + /// - response: The status and headers response to write + /// - encoder: The encoder to use for serializing the response, use JSONEncoder by default + /// - Throws: An error if JSON serialization or writing fails + public func writeStatusAndHeaders( + _ response: StreamingLambdaStatusAndHeadersResponse, + encoder: JSONEncoder = JSONEncoder() + ) async throws { + encoder.outputFormatting = .withoutEscapingSlashes + try await self.writeStatusAndHeaders(response, encoder: LambdaJSONOutputEncoder(encoder)) + } +} +@available(LambdaSwift 2.0, *) +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. + /// - logger: The logger to use for the runtime. Defaults to a logger with label "LambdaRuntime". + /// - body: The handler in the form of a closure. + public convenience init( + decoder: JSONDecoder = JSONDecoder(), + encoder: JSONEncoder = JSONEncoder(), + logger: Logger = Logger(label: "LambdaRuntime"), + 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, logger: logger) + } + + /// 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. + /// - Parameter logger: The logger to use for the runtime. Defaults to a logger with label "LambdaRuntime". + public convenience init( + decoder: JSONDecoder = JSONDecoder(), + logger: Logger = Logger(label: "LambdaRuntime"), + 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, logger: logger) + } +} +#endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/ByteBuffer-foundation.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/ByteBuffer-foundation.swift new file mode 100644 index 00000000..482e020f --- /dev/null +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/ByteBuffer-foundation.swift @@ -0,0 +1,108 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if FoundationJSONSupport +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// This is NIO's `NIOFoundationCompat` module which at the moment only adds `ByteBuffer` utility methods +// for Foundation's `Data` type. +// +// The reason that it's not in the `NIO` module is that we don't want to have any direct Foundation dependencies +// in `NIO` as Foundation is problematic for a few reasons: +// +// - its implementation is different on Linux and on macOS which means our macOS tests might be inaccurate +// - on macOS Foundation is mostly written in ObjC which means the autorelease pool might get populated +// - `swift-corelibs-foundation` (the OSS Foundation used on Linux) links the world which will prevent anyone from +// having static binaries. It can also cause problems in the choice of an SSL library as Foundation already brings +// the platforms OpenSSL in which might cause problems. + +extension ByteBuffer { + /// Controls how bytes are transferred between `ByteBuffer` and other storage types. + @usableFromInline + enum ByteTransferStrategy: Sendable { + /// Force a copy of the bytes. + case copy + + /// Do not copy the bytes if at all possible. + case noCopy + + /// Use a heuristic to decide whether to copy the bytes or not. + case automatic + } + + // MARK: - Data APIs + + /// Return `length` bytes starting at `index` and return the result as `Data`. This will not change the reader index. + /// The selected bytes must be readable or else `nil` will be returned. + /// + /// - parameters: + /// - index: The starting index of the bytes of interest into the `ByteBuffer` + /// - length: The number of bytes of interest + /// - byteTransferStrategy: Controls how to transfer the bytes. See `ByteTransferStrategy` for an explanation + /// of the options. + /// - returns: A `Data` value containing the bytes of interest or `nil` if the selected bytes are not readable. + @usableFromInline + func getData(at index0: Int, length: Int, byteTransferStrategy: ByteTransferStrategy) -> Data? { + let index = index0 - self.readerIndex + guard index >= 0 && length >= 0 && index <= self.readableBytes - length else { + return nil + } + let doCopy: Bool + switch byteTransferStrategy { + case .copy: + doCopy = true + case .noCopy: + doCopy = false + case .automatic: + doCopy = length <= 256 * 1024 + } + + return self.withUnsafeReadableBytesWithStorageManagement { ptr, storageRef in + if doCopy { + return Data( + bytes: UnsafeMutableRawPointer(mutating: ptr.baseAddress!.advanced(by: index)), + count: Int(length) + ) + } else { + _ = storageRef.retain() + return Data( + bytesNoCopy: UnsafeMutableRawPointer(mutating: ptr.baseAddress!.advanced(by: index)), + count: Int(length), + deallocator: .custom { _, _ in storageRef.release() } + ) + } + } + } +} +#endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/JSON+ByteBuffer.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/JSON+ByteBuffer.swift new file mode 100644 index 00000000..89ce9b87 --- /dev/null +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/JSON+ByteBuffer.swift @@ -0,0 +1,151 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2019-2021 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if FoundationJSONSupport +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +extension ByteBuffer { + /// Attempts to decode the `length` bytes from `index` using the `JSONDecoder` `decoder` as `T`. + /// + /// - parameters: + /// - type: The type type that is attempted to be decoded. + /// - decoder: The `JSONDecoder` that is used for the decoding. + /// - index: The index of the first byte to decode. + /// - length: The number of bytes to decode. + /// - returns: The decoded value if successful or `nil` if there are not enough readable bytes available. + @inlinable + func getJSONDecodable( + _ type: T.Type, + decoder: JSONDecoder = JSONDecoder(), + at index: Int, + length: Int + ) throws -> T? { + guard let data = self.getData(at: index, length: length, byteTransferStrategy: .noCopy) else { + return nil + } + return try decoder.decode(T.self, from: data) + } + + /// Encodes `value` using the `JSONEncoder` `encoder` and set the resulting bytes into this `ByteBuffer` at the + /// given `index`. + /// + /// - note: The `writerIndex` remains unchanged. + /// + /// - parameters: + /// - value: An `Encodable` value to encode. + /// - encoder: The `JSONEncoder` to encode `value` with. + /// - returns: The number of bytes written. + @inlinable + @discardableResult + mutating func setJSONEncodable( + _ value: T, + encoder: JSONEncoder = JSONEncoder(), + at index: Int + ) throws -> Int { + let data = try encoder.encode(value) + return self.setBytes(data, at: index) + } + + /// Encodes `value` using the `JSONEncoder` `encoder` and writes the resulting bytes into this `ByteBuffer`. + /// + /// If successful, this will move the writer index forward by the number of bytes written. + /// + /// - parameters: + /// - value: An `Encodable` value to encode. + /// - encoder: The `JSONEncoder` to encode `value` with. + /// - returns: The number of bytes written. + @inlinable + @discardableResult + mutating func writeJSONEncodable( + _ value: T, + encoder: JSONEncoder = JSONEncoder() + ) throws -> Int { + let result = try self.setJSONEncodable(value, encoder: encoder, at: self.writerIndex) + self.moveWriterIndex(forwardBy: result) + return result + } +} + +extension JSONDecoder { + /// Returns a value of the type you specify, decoded from a JSON object inside the readable bytes of a `ByteBuffer`. + /// + /// If the `ByteBuffer` does not contain valid JSON, this method throws the + /// `DecodingError.dataCorrupted(_:)` error. If a value within the JSON + /// fails to decode, this method throws the corresponding error. + /// + /// - note: The provided `ByteBuffer` remains unchanged, neither the `readerIndex` nor the `writerIndex` will move. + /// If you would like the `readerIndex` to move, consider using `ByteBuffer.readJSONDecodable(_:length:)`. + /// + /// - parameters: + /// - type: The type of the value to decode from the supplied JSON object. + /// - buffer: The `ByteBuffer` that contains JSON object to decode. + /// - returns: The decoded object. + func decode(_ type: T.Type, from buffer: ByteBuffer) throws -> T { + try buffer.getJSONDecodable( + T.self, + decoder: self, + at: buffer.readerIndex, + length: buffer.readableBytes + )! // must work, enough readable bytes// must work, enough readable bytes + } +} + +extension JSONEncoder { + /// Writes a JSON-encoded representation of the value you supply into the supplied `ByteBuffer`. + /// + /// - parameters: + /// - value: The value to encode as JSON. + /// - buffer: The `ByteBuffer` to encode into. + @inlinable + func encode( + _ value: T, + into buffer: inout ByteBuffer + ) throws { + try buffer.writeJSONEncodable(value, encoder: self) + } + + /// Writes a JSON-encoded representation of the value you supply into a `ByteBuffer` that is freshly allocated. + /// + /// - parameters: + /// - value: The value to encode as JSON. + /// - allocator: The `ByteBufferAllocator` which is used to allocate the `ByteBuffer` to be returned. + /// - returns: The `ByteBuffer` containing the encoded JSON. + func encodeAsByteBuffer(_ value: T, allocator: ByteBufferAllocator) throws -> ByteBuffer { + let data = try self.encode(value) + var buffer = allocator.buffer(capacity: data.count) + buffer.writeBytes(data) + return buffer + } +} +#endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/Lambda+Codable.swift b/Sources/AWSLambdaRuntime/Lambda+Codable.swift index f925754b..bf88b67c 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,128 +12,153 @@ // //===----------------------------------------------------------------------===// -@_exported import AWSLambdaRuntimeCore -import struct Foundation.Data -import class Foundation.JSONDecoder -import class Foundation.JSONEncoder import NIOCore -import NIOFoundationCompat -// MARK: - SimpleLambdaHandler Codable support - -/// Implementation of `ByteBuffer` to `Event` decoding. -extension SimpleLambdaHandler where Event: Decodable { - @inlinable - public func decode(buffer: ByteBuffer) throws -> Event { - try self.decoder.decode(Event.self, from: buffer) - } +/// 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 } -/// Implementation of `Output` to `ByteBuffer` encoding. -extension SimpleLambdaHandler where Output: Encodable { - @inlinable - public func encode(value: Output, into buffer: inout ByteBuffer) throws { - try self.encoder.encode(value, into: &buffer) - } -} +/// 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 -/// Default `ByteBuffer` to `Event` decoder using Foundation's `JSONDecoder`. -/// Advanced users who want to inject their own codec can do it by overriding these functions. -extension SimpleLambdaHandler where Event: Decodable { - public var decoder: LambdaCodableDecoder { - Lambda.defaultJSONDecoder - } + /// 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 } -/// Default `Output` to `ByteBuffer` encoder using Foundation's `JSONEncoder`. -/// Advanced users who want to inject their own codec can do it by overriding these functions. -extension SimpleLambdaHandler where Output: Encodable { - public var encoder: LambdaCodableEncoder { - Lambda.defaultJSONEncoder - } -} +public struct VoidEncoder: LambdaOutputEncoder { + public typealias Output = Void -// MARK: - LambdaHandler Codable support + public init() {} -/// Implementation of `ByteBuffer` to `Event` decoding. -extension LambdaHandler where Event: Decodable { @inlinable - public func decode(buffer: ByteBuffer) throws -> Event { - try self.decoder.decode(Event.self, from: buffer) - } + public func encode(_ value: Void, into buffer: inout NIOCore.ByteBuffer) throws {} } -/// Implementation of `Output` to `ByteBuffer` encoding. -extension LambdaHandler where Output: Encodable { +/// Adapts a ``LambdaHandler`` conforming handler to conform to ``LambdaWithBackgroundProcessingHandler``. +@available(LambdaSwift 2.0, *) +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 func encode(value: Output, into buffer: inout ByteBuffer) throws { - try self.encoder.encode(value, into: &buffer) + public init(handler: sending Handler) { + self.handler = handler } -} -/// Default `ByteBuffer` to `Event` decoder using Foundation's `JSONDecoder`. -/// Advanced users who want to inject their own codec can do it by overriding these functions. -extension LambdaHandler where Event: Decodable { - public var decoder: LambdaCodableDecoder { - Lambda.defaultJSONDecoder + /// 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) } } -/// Default `Output` to `ByteBuffer` encoder using Foundation's `JSONEncoder`. -/// Advanced users who want to inject their own codec can do it by overriding these functions. -extension LambdaHandler where Output: Encodable { - public var encoder: LambdaCodableEncoder { - Lambda.defaultJSONEncoder +/// Adapts a ``LambdaWithBackgroundProcessingHandler`` conforming handler to conform to ``StreamingLambdaHandler``. +@available(LambdaSwift 2.0, *) +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 } -} -// MARK: - EventLoopLambdaHandler Codable support - -/// Implementation of `ByteBuffer` to `Event` decoding. -extension EventLoopLambdaHandler where Event: Decodable { + /// 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 func decode(buffer: ByteBuffer) throws -> Event { - try self.decoder.decode(Event.self, from: buffer) + public init(decoder: sending Decoder, handler: Handler) where Output == Void, Encoder == VoidEncoder { + self.encoder = VoidEncoder() + self.decoder = decoder + self.handler = handler } -} -/// Implementation of `Output` to `ByteBuffer` encoding. -extension EventLoopLambdaHandler where Output: Encodable { + /// A ``StreamingLambdaHandler/handle(_:responseWriter:context:)`` wrapper. + /// - Parameters: + /// - request: The received event. + /// - responseWriter: The writer to write the computed response to. + /// - context: The ``LambdaContext`` containing the invocation's metadata. @inlinable - public func encode(value: Output, into buffer: inout ByteBuffer) throws { - try self.encoder.encode(value, into: &buffer) + 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) } } -/// Default `ByteBuffer` to `Event` decoder using Foundation's `JSONDecoder`. -/// Advanced users that want to inject their own codec can do it by overriding these functions. -extension EventLoopLambdaHandler where Event: Decodable { - public var decoder: LambdaCodableDecoder { - Lambda.defaultJSONDecoder +/// 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 } -} -/// Default `Output` to `ByteBuffer` encoder using Foundation's `JSONEncoder`. -/// Advanced users that want to inject their own codec can do it by overriding these functions. -extension EventLoopLambdaHandler where Output: Encodable { - public var encoder: LambdaCodableEncoder { - Lambda.defaultJSONEncoder + @inlinable + public func write(_ output: Output) async throws { + var outputBuffer = ByteBuffer() + try self.encoder.encode(output, into: &outputBuffer) + try await self.underlyingStreamWriter.writeAndFinish(outputBuffer) } } - -public protocol LambdaCodableDecoder { - func decode(_ type: T.Type, from buffer: ByteBuffer) throws -> T -} - -public protocol LambdaCodableEncoder { - func encode(_ value: T, into buffer: inout ByteBuffer) throws -} - -extension Lambda { - fileprivate static let defaultJSONDecoder = JSONDecoder() - fileprivate static let defaultJSONEncoder = JSONEncoder() -} - -extension JSONDecoder: LambdaCodableDecoder {} - -extension JSONEncoder: LambdaCodableEncoder {} diff --git a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift new file mode 100644 index 00000000..c3fa2e5d --- /dev/null +++ b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift @@ -0,0 +1,685 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#if LocalServerSupport +import DequeModule +import Dispatch +import Logging +import NIOCore +import NIOHTTP1 +import NIOPosix +import Synchronization + +// This functionality is designed for local testing when the LocalServerSupport trait is enabled. + +// 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 +// ) +// } +// } +@available(LambdaSwift 2.0, *) +extension Lambda { + /// Execute code in the context of a mock Lambda server. + /// + /// - parameters: + /// - host: the hostname or IP address to listen on + /// - port: the TCP port to listen to + /// - 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 when the LocalServerSupport trait is enabled. + @usableFromInline + static func withLocalServer( + host: String, + port: Int, + invocationEndpoint: String? = nil, + logger: Logger, + _ body: sending @escaping () async throws -> Void + ) async throws { + do { + try await LambdaHTTPServer.withLocalServer( + host: host, + port: port, + invocationEndpoint: invocationEndpoint, + logger: logger + ) { + try await body() + } + } catch let error as ChannelError { + // when this server is part of a ServiceLifeCycle group + // and user presses CTRL-C, this error is thrown + // The error description is "I/O on closed channel" + // TODO: investigate and solve the root cause + // because this server is used only for local tests + // and the error happens when we shutdown the server, I decided to ignore it at the moment. + logger.trace("Ignoring ChannelError during local server shutdown: \(error)") + } + } +} + +// 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. +@available(LambdaSwift 2.0, *) +internal 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) + } + + fileprivate struct UnsafeTransferBox: @unchecked Sendable { + let value: Value + + init(value: sending Value) { + self.value = value + } + } + + static func withLocalServer( + host: String, + port: Int, + invocationEndpoint: String?, + 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 + ) + ) + } + } + + // it's ok to keep this at `info` level because it is only used for local testing and unit tests + 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 withTaskCancellationHandler { + 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") + } + } + } + } + } onCancel: { + channel.channel.close(promise: nil) + } + 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): + + if result.maybeError is CancellationError { + logger.trace("Server's task cancelled") + } else { + 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") + if case .failure(let error) = result { + logger.error("Error during server shutdown: \(error)") + } + return try result.get() + } + + /// This method handles individual TCP connections + private func handleConnection( + channel: NIOAsyncChannel, + logger: Logger + ) async { + + var requestHead: HTTPRequestHead! + var requestBody: ByteBuffer? + var requestId: String? + + // 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. + await withTaskCancellationHandler { + do { + try await channel.executeThenClose { inbound, outbound in + for try await inboundData in inbound { + switch inboundData { + case .head(let head): + requestHead = head + requestId = getRequestId(from: requestHead) + + // for streaming requests, push a partial head response + if self.isStreamingResponse(requestHead) { + await self.responsePool.push( + LocalServerResponse( + id: requestId, + status: .ok + ) + ) + } + + case .body(let body): + precondition(requestHead != nil, "Received .body without .head") + + // if this is a request from a Streaming Lambda Handler, + // stream the response instead of buffering it + if self.isStreamingResponse(requestHead) { + await self.responsePool.push( + LocalServerResponse(id: requestId, body: body) + ) + } else { + requestBody.setOrWriteImmutableBuffer(body) + } + + case .end: + precondition(requestHead != nil, "Received .end without .head") + + if self.isStreamingResponse(requestHead) { + // for streaming response, send the final response + await self.responsePool.push( + LocalServerResponse(id: requestId, final: true) + ) + } else { + // process the buffered response for non streaming requests + try await self.processRequestAndSendResponse( + head: requestHead, + body: requestBody, + outbound: outbound, + logger: logger + ) + } + + // reset the request state for next request + requestHead = nil + requestBody = nil + requestId = nil + } + } + } + } catch let error as CancellationError { + logger.trace("The task was cancelled", metadata: ["error": "\(error)"]) + } catch { + logger.error("Hit error: \(error)") + } + + } onCancel: { + channel.channel.close(promise: nil) + } + } + + /// This function checks if the request is a streaming response request + /// verb = POST, uri = :requestId/response, HTTP Header contains "Transfer-Encoding: chunked" + private func isStreamingResponse(_ requestHead: HTTPRequestHead) -> Bool { + requestHead.method == .POST && requestHead.uri.hasSuffix(Consts.postResponseURLSuffix) + && requestHead.headers.contains(name: "Transfer-Encoding") + && (requestHead.headers["Transfer-Encoding"].contains("chunked") + || requestHead.headers["Transfer-Encoding"].contains("Chunked")) + } + + /// This function parses and returns the requestId or nil if the request doesn't contain a requestId + private func getRequestId(from head: HTTPRequestHead) -> String? { + let parts = head.uri.split(separator: "/") + return parts.count > 2 ? String(parts[parts.count - 2]) : nil + } + /// 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 processRequestAndSendResponse( + head: HTTPRequestHead, + body: ByteBuffer?, + outbound: NIOAsyncChannelOutboundWriter, + logger: Logger + ) async throws { + + var logger = logger + logger[metadataKey: "URI"] = "\(head.method) \(head.uri)" + if let body { + logger.trace( + "Processing request", + metadata: ["Body": "\(String(buffer: body))"] + ) + } else { + logger.trace("Processing request") + } + + switch (head.method, head.uri) { + + // + // client invocations + // + // client POST /invoke + case (.POST, let url) where url.hasSuffix(self.invocationEndpoint): + guard let body else { + return try await sendResponse( + .init(status: .badRequest, final: true), + outbound: outbound, + logger: logger + ) + } + // we always accept the /invoke request and push them to the pool + let requestId = "\(DispatchTime.now().uptimeNanoseconds)" + logger[metadataKey: "requestId"] = "\(requestId)" + logger.trace("/invoke received invocation, pushing it to the pool and wait for a lambda response") + 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[metadataKey: "response requestId"] = "\(response.requestId ?? "nil")" + logger.trace("Received response to return to client") + if response.requestId == requestId { + logger.trace("/invoke requestId is valid, sending the response") + // send the response to the client + // if the response is final, we can send it and return + // if the response is not final, we can send it and wait for the next response + try await self.sendResponse(response, outbound: outbound, logger: logger) + if response.final == true { + logger.trace("/invoke returning") + return // if the response is final, we can return and close the connection + } + } else { + logger.error( + "Received response for a different request id", + metadata: ["response requestId": "\(response.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 try await sendResponse( + .init(status: .methodNotAllowed, final: true), + outbound: outbound, + logger: logger + ) + + // + // 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[metadataKey: "requestId"] = "\(invocation.requestId)" + logger.trace("/next retrieved invocation") + // tell the lambda function we accepted the invocation + return try await sendResponse(invocation.acceptedResponse(), outbound: outbound, logger: logger) + } + // 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): + guard let requestId = getRequestId(from: head) else { + // the request is malformed, since we were expecting a requestId in the path + return try await sendResponse( + .init(status: .badRequest, final: true), + outbound: outbound, + logger: logger + ) + } + // 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: .accepted, + // the local server has no mecanism to collect headers set by the lambda function + headers: HTTPHeaders(), + body: body, + final: true + ) + ) + + // tell the Lambda function we accepted the response + return try await sendResponse( + .init(id: requestId, status: .accepted, final: true), + outbound: outbound, + logger: logger + ) + + // :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): + guard let requestId = getRequestId(from: head) else { + // the request is malformed, since we were expecting a requestId in the path + return try await sendResponse( + .init(status: .badRequest, final: true), + outbound: outbound, + logger: logger + ) + } + // 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: HTTPHeaders([("Content-Type", "application/json")]), + body: body, + final: true + ) + ) + + return try await sendResponse(.init(status: .accepted, final: true), outbound: outbound, logger: logger) + + // unknown call + default: + return try await sendResponse(.init(status: .notFound, final: true), outbound: outbound, logger: logger) + } + } + + private func sendResponse( + _ response: LocalServerResponse, + outbound: NIOAsyncChannelOutboundWriter, + logger: Logger + ) async throws { + var logger = logger + logger[metadataKey: "requestId"] = "\(response.requestId ?? "nil")" + logger.trace("Writing response for \(response.status?.code ?? 0)") + + var headers = response.headers ?? HTTPHeaders() + if let body = response.body { + headers.add(name: "Content-Length", value: "\(body.readableBytes)") + } + + if let status = response.status { + logger.trace("Sending status and headers") + try await outbound.write( + HTTPServerResponsePart.head( + HTTPResponseHead( + version: .init(major: 1, minor: 1), + status: status, + headers: headers + ) + ) + ) + } + + if let body = response.body { + logger.trace("Sending body") + try await outbound.write(HTTPServerResponsePart.body(.byteBuffer(body))) + } + + if response.final { + logger.trace("Sending end") + 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). + internal 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 withTaskCancellationHandler { + 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) + } + } onCancel: { + self.lock.withLock { state in + switch consume state { + case .buffer(let buffer): + state = .buffer(buffer) + case .continuation(let continuation): + continuation?.resume(throwing: CancellationError()) + state = .buffer([]) + } + } + } + } + + func makeAsyncIterator() -> Pool { + self + } + } + + private struct LocalServerResponse: Sendable { + let requestId: String? + let status: HTTPResponseStatus? + let headers: HTTPHeaders? + let body: ByteBuffer? + let final: Bool + init( + id: String? = nil, + status: HTTPResponseStatus? = nil, + headers: HTTPHeaders? = nil, + body: ByteBuffer? = nil, + final: Bool = false + ) { + self.requestId = id + self.status = status + self.headers = headers + self.body = body + self.final = final + } + } + + private struct LocalServerInvocation: Sendable { + let requestId: String + let request: ByteBuffer + + func acceptedResponse() -> LocalServerResponse { + + // required headers + let headers = HTTPHeaders([ + (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, "\(LambdaClock.maxLambdaDeadline)"), + ]) + + return LocalServerResponse( + id: self.requestId, + status: .accepted, + headers: headers, + body: self.request, + final: true + ) + } + } +} + +extension Result { + var maybeError: Failure? { + switch self { + case .success: + return nil + case .failure(let error): + return error + } + } +} +#endif diff --git a/Sources/AWSLambdaRuntime/Lambda.swift b/Sources/AWSLambdaRuntime/Lambda.swift new file mode 100644 index 00000000..d53efade --- /dev/null +++ b/Sources/AWSLambdaRuntime/Lambda.swift @@ -0,0 +1,111 @@ +//===----------------------------------------------------------------------===// +// +// 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 Logging +import NIOCore +import NIOPosix + +#if os(macOS) +import Darwin.C +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif os(Windows) +import ucrt +#else +#error("Unsupported platform") +#endif + +@available(LambdaSwift 2.0, *) +public enum Lambda { + @inlinable + package static func runLoop( + runtimeClient: RuntimeClient, + handler: Handler, + logger: Logger + ) async throws where Handler: StreamingLambdaHandler { + var handler = handler + + var logger = logger + do { + while !Task.isCancelled { + + logger.trace("Waiting for next invocation") + let (invocation, writer) = try await runtimeClient.nextInvocation() + logger[metadataKey: "aws-request-id"] = "\(invocation.metadata.requestID)" + + // when log level is trace or lower, print the first Kb of the payload + let bytes = invocation.event + let maxPayloadPreviewSize = 1024 + var metadata: Logger.Metadata? = nil + if logger.logLevel <= .trace, + let buffer = bytes.getSlice(at: 0, length: min(bytes.readableBytes, maxPayloadPreviewSize)) + { + metadata = [ + "Event's first bytes": .string( + String(buffer: buffer) + (bytes.readableBytes > maxPayloadPreviewSize ? "..." : "") + ) + ] + } + logger.trace( + "Sending invocation event to lambda handler", + metadata: metadata + ) + + do { + try await handler.handle( + invocation.event, + responseWriter: writer, + context: LambdaContext( + requestID: invocation.metadata.requestID, + traceID: invocation.metadata.traceID, + invokedFunctionARN: invocation.metadata.invokedFunctionARN, + deadline: LambdaClock.Instant( + millisecondsSinceEpoch: invocation.metadata.deadlineInMillisSinceEpoch + ), + logger: logger + ) + ) + logger.trace("Handler finished processing invocation") + } catch { + logger.trace("Handler failed processing invocation", metadata: ["Handler error": "\(error)"]) + try await writer.reportError(error) + continue + } + logger.handler.metadata.removeValue(forKey: "aws-request-id") + } + } catch is CancellationError { + // don't allow cancellation error to propagate further + } + + } + + /// The default EventLoop the Lambda is scheduled on. + public static let defaultEventLoop: any EventLoop = NIOSingletons.posixEventLoopGroup.next() +} + +// MARK: - Public API + +@available(LambdaSwift 2.0, *) +extension Lambda { + /// Utility to access/read environment variables + public static func env(_ name: String) -> String? { + guard let value = getenv(name) else { + return nil + } + return String(cString: value) + } +} diff --git a/Sources/AWSLambdaRuntime/LambdaClock.swift b/Sources/AWSLambdaRuntime/LambdaClock.swift new file mode 100644 index 00000000..5fe65e75 --- /dev/null +++ b/Sources/AWSLambdaRuntime/LambdaClock.swift @@ -0,0 +1,190 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#if os(macOS) +import Darwin.C +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif os(Windows) +import ucrt +#else +#error("Unsupported platform") +#endif + +/// A clock implementation based on Unix epoch time for AWS Lambda runtime operations. +/// +/// `LambdaClock` provides millisecond-precision timing based on the Unix epoch +/// (January 1, 1970, 00:00:00 UTC). This clock is designed for Lambda runtime +/// operations where precise wall-clock time is required. +/// +/// ## Usage +/// +/// ```swift +/// let clock = LambdaClock() +/// let now = clock.now +/// let deadline = now.advanced(by: .seconds(30)) +/// +/// // Sleep until deadline +/// try await clock.sleep(until: deadline) +/// ``` +/// +/// ## Performance +/// +/// This clock uses `clock_gettime(CLOCK_REALTIME)` on Unix systems for +/// high-precision wall-clock time measurement with millisecond resolution. +/// +/// ## TimeZone Handling +/// +/// The Lambda execution environment uses UTC as a timezone, +/// `LambdaClock` operates in UTC and does not account for time zones. +/// see: TZ in https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html +@available(LambdaSwift 2.0, *) +public struct LambdaClock: Clock { + public typealias Duration = Swift.Duration + + /// A moment in time represented as milliseconds since the Unix epoch. + /// + /// `Instant` represents a specific point in time as the number of milliseconds + /// that have elapsed since January 1, 1970, 00:00:00 UTC (Unix epoch). + /// + /// ## Thread Safety + /// + /// `Instant` is a value type and is inherently thread-safe. + public struct Instant: InstantProtocol, CustomStringConvertible { + /// The number of milliseconds since the Unix epoch. + let instant: Int64 + + public typealias Duration = Swift.Duration + + /// Creates a new instant by adding a duration to this instant. + /// + /// - Parameter duration: The duration to add to this instant. + /// - Returns: A new instant advanced by the specified duration. + /// + /// ## Example + /// + /// ```swift + /// let now = LambdaClock().now + /// let future = now.advanced(by: .seconds(30)) + /// ``` + public func advanced(by duration: Duration) -> Instant { + .init(millisecondsSinceEpoch: Int64(instant + Int64(duration / .milliseconds(1)))) + } + + /// Calculates the duration between this instant and another instant. + /// + /// - Parameter other: The target instant to calculate duration to. + /// - Returns: The duration from this instant to the other instant. + /// Positive if `other` is in the future, negative if in the past. + /// + /// ## Example + /// + /// ```swift + /// let start = LambdaClock().now + /// // ... some work ... + /// let end = LambdaClock().now + /// let elapsed = start.duration(to: end) + /// ``` + public func duration(to other: Instant) -> Duration { + .milliseconds(other.instant - self.instant) + } + + /// Compares two instants for ordering. + /// + /// - Parameters: + /// - lhs: The left-hand side instant. + /// - rhs: The right-hand side instant. + /// - Returns: `true` if `lhs` represents an earlier time than `rhs`. + public static func < (lhs: Instant, rhs: Instant) -> Bool { + lhs.instant < rhs.instant + } + + /// Returns this instant as the number of milliseconds since the Unix epoch. + /// - Returns: The number of milliseconds since the Unix epoch. + public func millisecondsSinceEpoch() -> Int64 { + self.instant + } + + /// Creates an instant from milliseconds since the Unix epoch. + /// - Parameter milliseconds: The number of milliseconds since the Unix epoch. + public init(millisecondsSinceEpoch milliseconds: Int64) { + self.instant = milliseconds + } + + /// Renders an Instant as an EPOCH value + public var description: String { + "\(self.instant)" + } + } + + /// The current instant according to this clock. + /// + /// This property returns the current wall-clock time as milliseconds + /// since the Unix epoch. + /// This method uses `clock_gettime(CLOCK_REALTIME)` to obtain high-precision + /// wall-clock time. + /// + /// - Returns: An `Instant` representing the current time. + public var now: Instant { + var ts = timespec() + clock_gettime(CLOCK_REALTIME, &ts) + return .init(millisecondsSinceEpoch: Int64(ts.tv_sec) * 1000 + Int64(ts.tv_nsec) / 1_000_000) + } + + /// The minimum resolution of this clock. + /// + /// `LambdaClock` provides millisecond resolution. + public var minimumResolution: Duration { + .milliseconds(1) + } + + /// Suspends the current task until the specified deadline. + /// + /// - Parameters: + /// - deadline: The instant until which to sleep. + /// - tolerance: The allowed tolerance for the sleep duration. Currently unused. + /// + /// - Throws: `CancellationError` if the task is cancelled during sleep. + /// + /// ## Example + /// + /// ```swift + /// let clock = LambdaClock() + /// let deadline = clock.now.advanced(by: .seconds(5)) + /// try await clock.sleep(until: deadline) + /// ``` + public func sleep(until deadline: Instant, tolerance: Instant.Duration?) async throws { + let now = self.now + let sleepDuration = now.duration(to: deadline) + if sleepDuration > .zero { + try await ContinuousClock().sleep(for: sleepDuration) + } + } + + /// Hardcoded maximum execution time for a Lambda function. + public static var maxLambdaExecutionTime: Duration { + // 15 minutes in milliseconds + // see https://docs.aws.amazon.com/lambda/latest/dg/configuration-timeout.html + .milliseconds(15 * 60 * 1000) + } + + /// Returns the maximum deadline for a Lambda function execution. + /// This is the current time plus the maximum execution time. + /// This function is only used by the local server for testing purposes. + public static var maxLambdaDeadline: Instant { + LambdaClock().now.advanced(by: maxLambdaExecutionTime) + } +} diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift new file mode 100644 index 00000000..df0166d2 --- /dev/null +++ b/Sources/AWSLambdaRuntime/LambdaContext.swift @@ -0,0 +1,201 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import Logging +import NIOCore + +// MARK: - Client Context + +/// AWS Mobile SDK client fields. +public struct ClientApplication: Codable, Sendable { + /// The mobile app installation id + public let installationID: String? + /// The app title for the mobile app as registered with AWS' mobile services. + public let appTitle: String? + /// The version name of the application as registered with AWS' mobile services. + public let appVersionName: String? + /// The app version code. + public let appVersionCode: String? + /// The package name for the mobile application invoking the function + public let appPackageName: String? + + private enum CodingKeys: String, CodingKey { + case installationID = "installation_id" + case appTitle = "app_title" + case appVersionName = "app_version_name" + case appVersionCode = "app_version_code" + case appPackageName = "app_package_name" + } + + public init( + installationID: String? = nil, + appTitle: String? = nil, + appVersionName: String? = nil, + appVersionCode: String? = nil, + appPackageName: String? = nil + ) { + self.installationID = installationID + self.appTitle = appTitle + self.appVersionName = appVersionName + self.appVersionCode = appVersionCode + self.appPackageName = appPackageName + } +} + +/// For invocations from the AWS Mobile SDK, data about the client application and device. +public struct ClientContext: Codable, Sendable { + /// Information about the mobile application invoking the function. + public let client: ClientApplication? + /// Custom properties attached to the mobile event context. + public let custom: [String: String]? + /// Environment settings from the mobile client. + public let environment: [String: String]? + + private enum CodingKeys: String, CodingKey { + case client + case custom + case environment = "env" + } + + public init( + client: ClientApplication? = nil, + custom: [String: String]? = nil, + environment: [String: String]? = nil + ) { + self.client = client + self.custom = custom + self.environment = environment + } +} + +// MARK: - Context + +/// Lambda runtime context. +/// The Lambda runtime generates and passes the `LambdaContext` to the Lambda handler as an argument. +@available(LambdaSwift 2.0, *) +public struct LambdaContext: CustomDebugStringConvertible, Sendable { + final class _Storage: Sendable { + let requestID: String + let traceID: String + let invokedFunctionARN: String + let deadline: LambdaClock.Instant + let cognitoIdentity: String? + let clientContext: ClientContext? + let logger: Logger + + init( + requestID: String, + traceID: String, + invokedFunctionARN: String, + deadline: LambdaClock.Instant, + cognitoIdentity: String?, + clientContext: ClientContext?, + logger: Logger + ) { + self.requestID = requestID + self.traceID = traceID + self.invokedFunctionARN = invokedFunctionARN + self.deadline = deadline + self.cognitoIdentity = cognitoIdentity + self.clientContext = clientContext + self.logger = logger + } + } + + private var storage: _Storage + + /// The request ID, which identifies the request that triggered the function invocation. + public var requestID: String { + self.storage.requestID + } + + /// The AWS X-Ray tracing header. + public var traceID: String { + self.storage.traceID + } + + /// The ARN of the Lambda function, version, or alias that's specified in the invocation. + public var invokedFunctionARN: String { + self.storage.invokedFunctionARN + } + + /// The timestamp that the function times out. + public var deadline: LambdaClock.Instant { + self.storage.deadline + } + + /// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider. + public var cognitoIdentity: String? { + self.storage.cognitoIdentity + } + + /// For invocations from the AWS Mobile SDK, data about the client application and device. + public var clientContext: ClientContext? { + self.storage.clientContext + } + + /// `Logger` to log with. + /// + /// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable. + public var logger: Logger { + self.storage.logger + } + + public init( + requestID: String, + traceID: String, + invokedFunctionARN: String, + deadline: LambdaClock.Instant, + cognitoIdentity: String? = nil, + clientContext: ClientContext? = nil, + logger: Logger + ) { + self.storage = _Storage( + requestID: requestID, + traceID: traceID, + invokedFunctionARN: invokedFunctionARN, + deadline: deadline, + cognitoIdentity: cognitoIdentity, + clientContext: clientContext, + logger: logger + ) + } + + public func getRemainingTime() -> Duration { + let deadline = self.deadline + return LambdaClock().now.duration(to: deadline) + } + + public var debugDescription: String { + "\(Self.self)(requestID: \(self.requestID), traceID: \(self.traceID), invokedFunctionARN: \(self.invokedFunctionARN), cognitoIdentity: \(self.cognitoIdentity ?? "nil"), clientContext: \(String(describing: self.clientContext)), deadline: \(self.deadline))" + } + + /// This interface is not part of the public API and must not be used by adopters. This API is not part of semver versioning. + /// The timeout is expressed relative to now + package static func __forTestsOnly( + requestID: String, + traceID: String, + invokedFunctionARN: String, + timeout: Duration, + logger: Logger + ) -> LambdaContext { + LambdaContext( + requestID: requestID, + traceID: traceID, + invokedFunctionARN: invokedFunctionARN, + deadline: LambdaClock().now.advanced(by: timeout), + logger: logger + ) + } +} diff --git a/Sources/AWSLambdaRuntime/LambdaHandlers.swift b/Sources/AWSLambdaRuntime/LambdaHandlers.swift new file mode 100644 index 00000000..4b42d0d7 --- /dev/null +++ b/Sources/AWSLambdaRuntime/LambdaHandlers.swift @@ -0,0 +1,260 @@ +//===----------------------------------------------------------------------===// +// +// 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 Logging +import NIOCore + +/// The base handler protocol that receives a `ByteBuffer` representing the incoming event and returns the response as a `ByteBuffer` too. +/// This handler protocol supports response streaming. Bytes can be streamed outwards through the ``LambdaResponseStreamWriter`` +/// passed as an argument in the ``handle(_:responseWriter:context:)`` function. +/// Background work can also be executed after returning the response. After closing the response stream by calling +/// ``LambdaResponseStreamWriter/finish()`` or ``LambdaResponseStreamWriter/writeAndFinish(_:)``, +/// the ``handle(_:responseWriter:context:)`` function is free to execute any background work. +@available(LambdaSwift 2.0, *) +public protocol StreamingLambdaHandler: _Lambda_SendableMetatype { + /// The handler function -- implement the business logic of the Lambda function here. + /// - Parameters: + /// - event: The invocation's input data. + /// - responseWriter: A ``LambdaResponseStreamWriter`` to write the invocation's response to. + /// If no response or error is written to `responseWriter` an error will be reported to the invoker. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + /// - Throws: + /// How the thrown error will be handled by the runtime: + /// - An invocation error will be reported if the error is thrown before the first call to + /// ``LambdaResponseStreamWriter/write(_:)``. + /// - If the error is thrown after call(s) to ``LambdaResponseStreamWriter/write(_:)`` but before + /// a call to ``LambdaResponseStreamWriter/finish()``, the response stream will be closed and trailing + /// headers will be sent. + /// - If ``LambdaResponseStreamWriter/finish()`` has already been called before the error is thrown, the + /// error will be logged. + mutating func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws +} + +/// A writer object to write the Lambda response stream into. The HTTP response is started lazily. +/// before the first call to ``write(_:)`` or ``writeAndFinish(_:)``. +public protocol LambdaResponseStreamWriter { + /// Write a response part into the stream. Bytes written are streamed continually. + /// - Parameter buffer: The buffer to write. + /// - Parameter hasCustomHeaders: If `true`, the response will be sent with custom HTTP status code and headers. + func write(_ buffer: ByteBuffer, hasCustomHeaders: Bool) async throws + + /// End the response stream and the underlying HTTP response. + func finish() async throws + + /// Write a response part into the stream and then end the stream as well as the underlying HTTP response. + /// - Parameter buffer: The buffer to write. + func writeAndFinish(_ buffer: ByteBuffer) async throws +} + +/// This handler protocol is intended to serve the most common use-cases. +/// This protocol is completely agnostic to any encoding/decoding -- decoding the received event invocation into an ``Event`` object and encoding the returned ``Output`` object is handled by the library. +/// The``handle(_:context:)`` function simply receives the generic ``Event`` object as input and returns the generic ``Output`` object. +/// +/// - note: This handler protocol does not support response streaming because the output has to be encoded prior to it being sent, e.g. it is not possible to encode a partial/incomplete JSON string. +/// This protocol also does not support the execution of background work after the response has been returned -- the ``LambdaWithBackgroundProcessingHandler`` protocol caters for such use-cases. +@available(LambdaSwift 2.0, *) +public protocol LambdaHandler { + /// Generic input type. + /// The body of the request sent to Lambda will be decoded into this type for the handler to consume. + associatedtype Event + /// Generic output type. + /// This is the return type of the ``LambdaHandler/handle(_:context:)`` function. + associatedtype Output + + /// Implement the business logic of the Lambda function here. + /// - Parameters: + /// - event: The generic ``LambdaHandler/Event`` object representing the invocation's input data. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + /// - Returns: A generic ``Output`` object representing the computed result. + func handle(_ event: Event, context: LambdaContext) async throws -> Output +} + +/// This protocol is exactly like ``LambdaHandler``, with the only difference being the added support for executing background +/// work after the result has been sent to the AWS Lambda control plane. +/// This is achieved by not having a return type in the `handle` function. The output is instead written into a +/// ``LambdaResponseWriter``that is passed in as an argument, meaning that the +/// ``LambdaWithBackgroundProcessingHandler/handle(_:outputWriter:context:)`` function is then +/// free to implement any background work after the result has been sent to the AWS Lambda control plane. +@available(LambdaSwift 2.0, *) +public protocol LambdaWithBackgroundProcessingHandler { + /// Generic input type. + /// The body of the request sent to Lambda will be decoded into this type for the handler to consume. + associatedtype Event + /// Generic output type. + /// This is the type that the `handle` function will send through the ``LambdaResponseWriter``. + associatedtype Output + + /// Implement the business logic of the Lambda function here. + /// - Parameters: + /// - event: The generic ``LambdaWithBackgroundProcessingHandler/Event`` object representing the invocation's input data. + /// - outputWriter: The writer to send the computed response to. A call to `outputWriter.write(_:)` will return the response to the AWS Lambda response endpoint. + /// Any background work can then be executed before returning. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws +} + +/// Used with ``LambdaWithBackgroundProcessingHandler``. +/// A mechanism to "return" an output from ``LambdaWithBackgroundProcessingHandler/handle(_:outputWriter:context:)`` without the function needing to +/// have a return type and exit at that point. This allows for background work to be executed _after_ a response has been sent to the AWS Lambda response endpoint. +public protocol LambdaResponseWriter { + associatedtype Output + /// Sends the generic ``LambdaResponseWriter/Output`` object (representing the computed result of the handler) + /// to the AWS Lambda response endpoint. + /// This function simply serves as a mechanism to return the computed result from a handler function + /// without an explicit `return`. + func write(_ output: Output) async throws +} + +/// A ``StreamingLambdaHandler`` conforming handler object that can be constructed with a closure. +/// Allows for a handler to be defined in a clean manner, leveraging Swift's trailing closure syntax. +@available(LambdaSwift 2.0, *) +public struct StreamingClosureHandler: StreamingLambdaHandler { + let body: @Sendable (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> Void + + /// Initialize an instance from a handler function in the form of a closure. + /// - Parameter body: The handler function written as a closure. + public init( + body: @Sendable @escaping (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> Void + ) { + self.body = body + } + + /// Calls the provided `self.body` closure with the `ByteBuffer` invocation event, the ``LambdaResponseStreamWriter``, and the ``LambdaContext`` + /// - Parameters: + /// - event: The invocation's input data. + /// - responseWriter: A ``LambdaResponseStreamWriter`` to write the invocation's response to. + /// If no response or error is written to `responseWriter` an error will be reported to the invoker. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + public func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + try await self.body(event, responseWriter, context) + } +} + +/// A ``LambdaHandler`` conforming handler object that can be constructed with a closure. +/// Allows for a handler to be defined in a clean manner, leveraging Swift's trailing closure syntax. +@available(LambdaSwift 2.0, *) +public struct ClosureHandler: LambdaHandler { + let body: (Event, LambdaContext) async throws -> Output + + /// Initialize with a closure handler over generic `Input` and `Output` types. + /// - Parameter body: The handler function written as a closure. + public init(body: sending @escaping (Event, LambdaContext) async throws -> Output) where Output: Encodable { + self.body = body + } + + /// Initialize with a closure handler over a generic `Input` type, and a `Void` `Output`. + /// - Parameter body: The handler function written as a closure. + public init(body: @escaping (Event, LambdaContext) async throws -> Void) where Output == Void { + self.body = body + } + + /// Calls the provided `self.body` closure with the generic `Event` object representing the incoming event, and the ``LambdaContext`` + /// - Parameters: + /// - event: The generic `Event` object representing the invocation's input data. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + public func handle(_ event: Event, context: LambdaContext) async throws -> Output { + try await self.body(event, context) + } +} + +@available(LambdaSwift 2.0, *) +extension LambdaRuntime { + /// Initialize an instance with a ``StreamingLambdaHandler`` in the form of a closure. + /// - Parameter + /// - logger: The logger to use for the runtime. Defaults to a logger with label "LambdaRuntime". + /// - body: The handler in the form of a closure. + public convenience init( + logger: Logger = Logger(label: "LambdaRuntime"), + body: @Sendable @escaping (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> Void + + ) where Handler == StreamingClosureHandler { + self.init(handler: StreamingClosureHandler(body: body), logger: logger) + } + + /// Initialize an instance with a ``LambdaHandler`` defined in the form of a closure **with a non-`Void` return type**, an encoder, and a decoder. + /// - Parameters: + /// - encoder: The encoder object that will be used to encode the generic `Output` into a `ByteBuffer`. + /// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. + /// - logger: The logger to use for the runtime. Defaults to a logger with label "LambdaRuntime". + /// - body: The handler in the form of a closure. + public convenience init< + Event: Decodable, + Output: Encodable, + Encoder: LambdaOutputEncoder, + Decoder: LambdaEventDecoder + >( + encoder: sending Encoder, + decoder: sending Decoder, + logger: Logger = Logger(label: "LambdaRuntime"), + body: sending @escaping (Event, LambdaContext) async throws -> Output + ) + where + Handler == LambdaCodableAdapter< + LambdaHandlerAdapter>, + Event, + Output, + Decoder, + Encoder + > + { + let closureHandler = ClosureHandler(body: body) + let streamingAdapter = LambdaHandlerAdapter(handler: closureHandler) + let codableWrapper = LambdaCodableAdapter( + encoder: encoder, + decoder: decoder, + handler: streamingAdapter + ) + + self.init(handler: codableWrapper, logger: logger) + } + + /// Initialize an instance with a ``LambdaHandler`` defined in the form of a closure **with a `Void` return type**, an encoder, and a decoder. + /// - Parameters: + /// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. + /// - logger: The logger to use for the runtime. Defaults to a logger with label "LambdaRuntime". + /// - body: The handler in the form of a closure. + public convenience init( + decoder: sending Decoder, + logger: Logger = Logger(label: "LambdaRuntime"), + body: sending @escaping (Event, LambdaContext) async throws -> Void + ) + where + Handler == LambdaCodableAdapter< + LambdaHandlerAdapter>, + Event, + Void, + Decoder, + VoidEncoder + > + { + let handler = LambdaCodableAdapter( + decoder: decoder, + handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) + ) + + self.init(handler: handler, logger: logger) + } +} diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRequestID.swift b/Sources/AWSLambdaRuntime/LambdaRequestID.swift similarity index 79% rename from Sources/AWSLambdaRuntimeCore/LambdaRequestID.swift rename to Sources/AWSLambdaRuntime/LambdaRequestID.swift index 86178ff4..df576947 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRequestID.swift +++ b/Sources/AWSLambdaRuntime/LambdaRequestID.swift @@ -18,7 +18,9 @@ import NIOCore // https://github.com/swift-extras/swift-extras-uuid struct LambdaRequestID { - typealias uuid_t = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) + typealias uuid_t = ( + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8 + ) var uuid: uuid_t { self._uuid @@ -86,15 +88,16 @@ struct LambdaRequestID { } /// thread safe secure random number generator. - private static var generator = SystemRandomNumberGenerator() private static func generateRandom() -> Self { + var generator = SystemRandomNumberGenerator() + var _uuid: uuid_t = LambdaRequestID.null // https://tools.ietf.org/html/rfc4122#page-14 // o Set all the other bits to randomly (or pseudo-randomly) chosen // values. withUnsafeMutableBytes(of: &_uuid) { ptr in - ptr.storeBytes(of: Self.generator.next(), toByteOffset: 0, as: UInt64.self) - ptr.storeBytes(of: Self.generator.next(), toByteOffset: 8, as: UInt64.self) + ptr.storeBytes(of: generator.next(), toByteOffset: 0, as: UInt64.self) + ptr.storeBytes(of: generator.next(), toByteOffset: 8, as: UInt64.self) } // o Set the four most significant bits (bits 12 through 15) of the @@ -114,22 +117,12 @@ struct LambdaRequestID { extension LambdaRequestID: Equatable { // sadly no auto conformance from the compiler static func == (lhs: Self, rhs: Self) -> Bool { - lhs._uuid.0 == rhs._uuid.0 && - lhs._uuid.1 == rhs._uuid.1 && - lhs._uuid.2 == rhs._uuid.2 && - lhs._uuid.3 == rhs._uuid.3 && - lhs._uuid.4 == rhs._uuid.4 && - lhs._uuid.5 == rhs._uuid.5 && - lhs._uuid.6 == rhs._uuid.6 && - lhs._uuid.7 == rhs._uuid.7 && - lhs._uuid.8 == rhs._uuid.8 && - lhs._uuid.9 == rhs._uuid.9 && - lhs._uuid.10 == rhs._uuid.10 && - lhs._uuid.11 == rhs._uuid.11 && - lhs._uuid.12 == rhs._uuid.12 && - lhs._uuid.13 == rhs._uuid.13 && - lhs._uuid.14 == rhs._uuid.14 && - lhs._uuid.15 == rhs._uuid.15 + lhs._uuid.0 == rhs._uuid.0 && lhs._uuid.1 == rhs._uuid.1 && lhs._uuid.2 == rhs._uuid.2 + && lhs._uuid.3 == rhs._uuid.3 && lhs._uuid.4 == rhs._uuid.4 && lhs._uuid.5 == rhs._uuid.5 + && lhs._uuid.6 == rhs._uuid.6 && lhs._uuid.7 == rhs._uuid.7 && lhs._uuid.8 == rhs._uuid.8 + && lhs._uuid.9 == rhs._uuid.9 && lhs._uuid.10 == rhs._uuid.10 && lhs._uuid.11 == rhs._uuid.11 + && lhs._uuid.12 == rhs._uuid.12 && lhs._uuid.13 == rhs._uuid.13 && lhs._uuid.14 == rhs._uuid.14 + && lhs._uuid.15 == rhs._uuid.15 } } @@ -160,7 +153,10 @@ extension LambdaRequestID: Decodable { let uuidString = try container.decode(String.self) guard let uuid = LambdaRequestID.fromString(uuidString) else { - throw DecodingError.dataCorruptedError(in: container, debugDescription: "Attempted to decode UUID from invalid UUID string.") + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Attempted to decode UUID from invalid UUID string." + ) } self = uuid @@ -217,38 +213,38 @@ extension LambdaRequestID { // are safe and we don't need Swifts safety guards. characters.withUnsafeBufferPointer { lookup in - string.0 = lookup[Int(uuid.0 >> 4)] - string.1 = lookup[Int(uuid.0 & 0x0F)] - string.2 = lookup[Int(uuid.1 >> 4)] - string.3 = lookup[Int(uuid.1 & 0x0F)] - string.4 = lookup[Int(uuid.2 >> 4)] - string.5 = lookup[Int(uuid.2 & 0x0F)] - string.6 = lookup[Int(uuid.3 >> 4)] - string.7 = lookup[Int(uuid.3 & 0x0F)] - string.9 = lookup[Int(uuid.4 >> 4)] - string.10 = lookup[Int(uuid.4 & 0x0F)] - string.11 = lookup[Int(uuid.5 >> 4)] - string.12 = lookup[Int(uuid.5 & 0x0F)] - string.14 = lookup[Int(uuid.6 >> 4)] - string.15 = lookup[Int(uuid.6 & 0x0F)] - string.16 = lookup[Int(uuid.7 >> 4)] - string.17 = lookup[Int(uuid.7 & 0x0F)] - string.19 = lookup[Int(uuid.8 >> 4)] - string.20 = lookup[Int(uuid.8 & 0x0F)] - string.21 = lookup[Int(uuid.9 >> 4)] - string.22 = lookup[Int(uuid.9 & 0x0F)] - string.24 = lookup[Int(uuid.10 >> 4)] - string.25 = lookup[Int(uuid.10 & 0x0F)] - string.26 = lookup[Int(uuid.11 >> 4)] - string.27 = lookup[Int(uuid.11 & 0x0F)] - string.28 = lookup[Int(uuid.12 >> 4)] - string.29 = lookup[Int(uuid.12 & 0x0F)] - string.30 = lookup[Int(uuid.13 >> 4)] - string.31 = lookup[Int(uuid.13 & 0x0F)] - string.32 = lookup[Int(uuid.14 >> 4)] - string.33 = lookup[Int(uuid.14 & 0x0F)] - string.34 = lookup[Int(uuid.15 >> 4)] - string.35 = lookup[Int(uuid.15 & 0x0F)] + string.0 = lookup[Int(self.uuid.0 >> 4)] + string.1 = lookup[Int(self.uuid.0 & 0x0F)] + string.2 = lookup[Int(self.uuid.1 >> 4)] + string.3 = lookup[Int(self.uuid.1 & 0x0F)] + string.4 = lookup[Int(self.uuid.2 >> 4)] + string.5 = lookup[Int(self.uuid.2 & 0x0F)] + string.6 = lookup[Int(self.uuid.3 >> 4)] + string.7 = lookup[Int(self.uuid.3 & 0x0F)] + string.9 = lookup[Int(self.uuid.4 >> 4)] + string.10 = lookup[Int(self.uuid.4 & 0x0F)] + string.11 = lookup[Int(self.uuid.5 >> 4)] + string.12 = lookup[Int(self.uuid.5 & 0x0F)] + string.14 = lookup[Int(self.uuid.6 >> 4)] + string.15 = lookup[Int(self.uuid.6 & 0x0F)] + string.16 = lookup[Int(self.uuid.7 >> 4)] + string.17 = lookup[Int(self.uuid.7 & 0x0F)] + string.19 = lookup[Int(self.uuid.8 >> 4)] + string.20 = lookup[Int(self.uuid.8 & 0x0F)] + string.21 = lookup[Int(self.uuid.9 >> 4)] + string.22 = lookup[Int(self.uuid.9 & 0x0F)] + string.24 = lookup[Int(self.uuid.10 >> 4)] + string.25 = lookup[Int(self.uuid.10 & 0x0F)] + string.26 = lookup[Int(self.uuid.11 >> 4)] + string.27 = lookup[Int(self.uuid.11 & 0x0F)] + string.28 = lookup[Int(self.uuid.12 >> 4)] + string.29 = lookup[Int(self.uuid.12 & 0x0F)] + string.30 = lookup[Int(self.uuid.13 >> 4)] + string.31 = lookup[Int(self.uuid.13 & 0x0F)] + string.32 = lookup[Int(self.uuid.14 >> 4)] + string.33 = lookup[Int(self.uuid.14 & 0x0F)] + string.34 = lookup[Int(self.uuid.15 >> 4)] + string.35 = lookup[Int(self.uuid.15 & 0x0F)] } return string @@ -270,11 +266,11 @@ extension LambdaRequestID { static func fromPointer(_ ptr: UnsafeRawBufferPointer) -> LambdaRequestID? { func uint4Value(from value: UInt8, valid: inout Bool) -> UInt8 { switch value { - case UInt8(ascii: "0") ... UInt8(ascii: "9"): + case UInt8(ascii: "0")...UInt8(ascii: "9"): return value &- UInt8(ascii: "0") - case UInt8(ascii: "a") ... UInt8(ascii: "f"): + case UInt8(ascii: "a")...UInt8(ascii: "f"): return value &- UInt8(ascii: "a") &+ 10 - case UInt8(ascii: "A") ... UInt8(ascii: "F"): + case UInt8(ascii: "A")...UInt8(ascii: "F"): return value &- UInt8(ascii: "A") &+ 10 default: valid = false @@ -365,7 +361,7 @@ extension ByteBuffer { return nil } - let upperBound = indexFromReaderIndex &+ length // safe, can't overflow, we checked it above. + let upperBound = indexFromReaderIndex &+ length // safe, can't overflow, we checked it above. // uncheckedBounds is safe because `length` is >= 0, so the lower bound will always be lower/equal to upper return Range(uncheckedBounds: (lower: indexFromReaderIndex, upper: upperBound)) diff --git a/Sources/AWSLambdaRuntime/LambdaResponseStreamWriter+Headers.swift b/Sources/AWSLambdaRuntime/LambdaResponseStreamWriter+Headers.swift new file mode 100644 index 00000000..0aeb84e5 --- /dev/null +++ b/Sources/AWSLambdaRuntime/LambdaResponseStreamWriter+Headers.swift @@ -0,0 +1,88 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-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 + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +/// A response structure specifically designed for streaming Lambda responses that contains +/// HTTP status code and headers without body content. +/// +/// This structure is used with `LambdaResponseStreamWriter.writeStatusAndHeaders(_:)` to send +/// HTTP response metadata before streaming the response body. +public struct StreamingLambdaStatusAndHeadersResponse: Codable, Sendable { + /// The HTTP status code for the response (e.g., 200, 404, 500) + public let statusCode: Int + + /// Dictionary of single-value HTTP headers + public let headers: [String: String]? + + /// Dictionary of multi-value HTTP headers (e.g., Set-Cookie headers) + public let multiValueHeaders: [String: [String]]? + + /// Creates a new streaming Lambda response with status code and optional headers + /// + /// - Parameters: + /// - statusCode: The HTTP status code for the response + /// - headers: Optional dictionary of single-value HTTP headers + /// - multiValueHeaders: Optional dictionary of multi-value HTTP headers + public init( + statusCode: Int, + headers: [String: String]? = nil, + multiValueHeaders: [String: [String]]? = nil + ) { + self.statusCode = statusCode + self.headers = headers + self.multiValueHeaders = multiValueHeaders + } +} + +extension LambdaResponseStreamWriter { + /// Writes the HTTP status code and headers to the response stream. + /// + /// This method serializes the status and headers as JSON and writes them to the stream, + /// followed by eight null bytes as a separator before the response body. + /// + /// - Parameters: + /// - response: The status and headers response to write + /// - encoder: The encoder to use for serializing the response, + /// - Throws: An error if JSON serialization or writing fails + public func writeStatusAndHeaders( + _ response: StreamingLambdaStatusAndHeadersResponse, + encoder: Encoder + ) async throws where Encoder.Output == StreamingLambdaStatusAndHeadersResponse { + + // Convert JSON headers to an array of bytes in a ByteBuffer + var buffer = ByteBuffer() + try encoder.encode(response, into: &buffer) + + // Write eight null bytes as separator + buffer.writeBytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + + // Write the JSON data and the separator + try await self.write(buffer, hasCustomHeaders: true) + } + + /// Write a response part into the stream. Bytes written are streamed continually. + /// This implementation avoids having to modify all the tests and other part of the code that use this function signature + /// - Parameter buffer: The buffer to write. + public func write(_ buffer: ByteBuffer) async throws { + // Write the buffer to the response stream + try await self.write(buffer, hasCustomHeaders: false) + } +} diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime+ServiceLifecycle.swift b/Sources/AWSLambdaRuntime/LambdaRuntime+ServiceLifecycle.swift new file mode 100644 index 00000000..7489e8a3 --- /dev/null +++ b/Sources/AWSLambdaRuntime/LambdaRuntime+ServiceLifecycle.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#if ServiceLifecycleSupport +import ServiceLifecycle + +@available(LambdaSwift 2.0, *) +extension LambdaRuntime: Service { + public func run() async throws { + try await cancelWhenGracefulShutdown { + try await self._run() + } + } +} +#endif diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift new file mode 100644 index 00000000..33bd46fd --- /dev/null +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -0,0 +1,158 @@ +//===----------------------------------------------------------------------===// +// +// 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 Logging +import NIOCore +import Synchronization + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// This is our guardian to ensure only one LambdaRuntime is running at the time +// We use an Atomic here to ensure thread safety +@available(LambdaSwift 2.0, *) +private let _isRunning = Atomic(false) + +@available(LambdaSwift 2.0, *) +public final class LambdaRuntime: Sendable where Handler: StreamingLambdaHandler { + @usableFromInline + /// we protect the handler behind a Mutex to ensure that we only ever have one copy of it + let handlerStorage: SendingStorage + @usableFromInline + let logger: Logger + @usableFromInline + let eventLoop: EventLoop + + public init( + handler: sending Handler, + eventLoop: EventLoop = Lambda.defaultEventLoop, + logger: Logger = Logger(label: "LambdaRuntime") + ) { + self.handlerStorage = SendingStorage(handler) + self.eventLoop = eventLoop + + // by setting the log level here, we understand it can not be changed dynamically at runtime + // developers have to wait for AWS Lambda to dispose and recreate a runtime environment to pickup a change + // this approach is less flexible but more performant than reading the value of the environment variable at each invocation + var log = logger + + // use the LOG_LEVEL environment variable to set the log level. + // if the environment variable is not set, use the default log level from the logger provided + log.logLevel = Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? logger.logLevel + + self.logger = log + self.logger.debug("LambdaRuntime initialized") + } + + #if !ServiceLifecycleSupport + public func run() async throws { + try await _run() + } + #endif + + /// Make sure only one run() is called at a time + internal func _run() async throws { + + // we use an atomic global variable to ensure only one LambdaRuntime is running at the time + let (_, original) = _isRunning.compareExchange(expected: false, desired: true, ordering: .acquiringAndReleasing) + + // if the original value was already true, run() is already running + if original { + throw LambdaRuntimeError(code: .runtimeCanOnlyBeStartedOnce) + } + + defer { + _isRunning.store(false, ordering: .releasing) + } + + // The handler can be non-sendable, we want to ensure we only ever have one copy of it + let handler = try? self.handlerStorage.get() + guard let handler else { + throw LambdaRuntimeError(code: .handlerCanOnlyBeGetOnce) + } + + // are we running inside an AWS Lambda runtime environment ? + // AWS_LAMBDA_RUNTIME_API is set when running on Lambda + // https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html + if let runtimeEndpoint = Lambda.env("AWS_LAMBDA_RUNTIME_API") { + + let ipAndPort = runtimeEndpoint.split(separator: ":", maxSplits: 1) + let ip = String(ipAndPort[0]) + guard let port = Int(ipAndPort[1]) else { throw LambdaRuntimeError(code: .invalidPort) } + + do { + try await LambdaRuntimeClient.withRuntimeClient( + configuration: .init(ip: ip, port: port), + eventLoop: self.eventLoop, + logger: self.logger + ) { runtimeClient in + try await Lambda.runLoop( + runtimeClient: runtimeClient, + handler: handler, + logger: self.logger + ) + } + } catch { + // catch top level errors that have not been handled until now + // this avoids the runtime to crash and generate a backtrace + self.logger.error("LambdaRuntime.run() failed with error", metadata: ["error": "\(error)"]) + if let error = error as? LambdaRuntimeError, + error.code != .connectionToControlPlaneLost + { + // if the error is a LambdaRuntimeError but not a connection error, + // we rethrow it to preserve existing behaviour + throw error + } + } + + } else { + + #if LocalServerSupport + + // we're not running on Lambda and we're compiled in DEBUG mode, + // let's start a local server for testing + + let host = Lambda.env("LOCAL_LAMBDA_HOST") ?? "127.0.0.1" + let port = Lambda.env("LOCAL_LAMBDA_PORT").flatMap(Int.init) ?? 7000 + let endpoint = Lambda.env("LOCAL_LAMBDA_INVOCATION_ENDPOINT") + + try await Lambda.withLocalServer( + host: host, + port: port, + invocationEndpoint: endpoint, + logger: self.logger + ) { + + try await LambdaRuntimeClient.withRuntimeClient( + configuration: .init(ip: host, port: port), + eventLoop: self.eventLoop, + logger: self.logger + ) { runtimeClient in + try await Lambda.runLoop( + runtimeClient: runtimeClient, + handler: handler, + logger: self.logger + ) + } + } + #else + // When the LocalServerSupport trait is disabled, we can't start a local server because the local server code is not compiled. + throw LambdaRuntimeError(code: .missingLambdaRuntimeAPIEnvironmentVariable) + #endif + } + } +} diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeClient+ChannelHandler.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeClient+ChannelHandler.swift new file mode 100644 index 00000000..2b66ee87 --- /dev/null +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeClient+ChannelHandler.swift @@ -0,0 +1,488 @@ +//===----------------------------------------------------------------------===// +// +// 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 NIOCore +import NIOHTTP1 +import NIOPosix + +internal protocol LambdaChannelHandlerDelegate { + func connectionWillClose(channel: any Channel) + func connectionErrorHappened(_ error: any Error, channel: any Channel) +} + +@available(LambdaSwift 2.0, *) +internal final class LambdaChannelHandler { + let nextInvocationPath = Consts.invocationURLPrefix + Consts.getNextInvocationURLSuffix + + enum State { + case disconnected + case connected(ChannelHandlerContext, LambdaState) + case closing + + enum LambdaState { + /// this is the "normal" state. Transitions to `waitingForNextInvocation` + case idle + /// this is the state while we wait for an invocation. A next call is running. + /// Transitions to `waitingForResponse` + case waitingForNextInvocation(CheckedContinuation) + /// The invocation was forwarded to the handler and we wait for a response. + /// Transitions to `sendingResponse` or `sentResponse`. + case waitingForResponse + case sendingResponse + case sentResponse(CheckedContinuation) + } + } + + private var state: State = .disconnected + private var lastError: Error? + private var reusableErrorBuffer: ByteBuffer? + private let logger: Logger + private let delegate: Delegate + private let configuration: LambdaRuntimeClient.Configuration + + /// These are the default headers that must be sent along an invocation + let defaultHeaders: HTTPHeaders + /// These headers must be sent along an invocation or initialization error report + let errorHeaders: HTTPHeaders + /// These headers must be sent when streaming a large response + let largeResponseHeaders: HTTPHeaders + /// These headers must be sent when the handler streams its response + let streamingHeaders: HTTPHeaders + + init( + delegate: Delegate, + logger: Logger, + configuration: LambdaRuntimeClient.Configuration + ) { + self.delegate = delegate + self.logger = logger + self.configuration = configuration + self.defaultHeaders = [ + "host": "\(self.configuration.ip):\(self.configuration.port)", + "user-agent": .userAgent, + ] + self.errorHeaders = [ + "host": "\(self.configuration.ip):\(self.configuration.port)", + "user-agent": .userAgent, + "lambda-runtime-function-error-type": "Unhandled", + ] + self.largeResponseHeaders = [ + "host": "\(self.configuration.ip):\(self.configuration.port)", + "user-agent": .userAgent, + "transfer-encoding": "chunked", + ] + // https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html#runtimes-custom-response-streaming + // These are the headers returned by the Runtime to the Lambda Data plane. + // These are not the headers the Lambda Data plane sends to the caller of the Lambda function + // The developer of the function can set the caller's headers in the handler code. + self.streamingHeaders = [ + "host": "\(self.configuration.ip):\(self.configuration.port)", + "user-agent": .userAgent, + "Lambda-Runtime-Function-Response-Mode": "streaming", + // these are not used by this runtime client at the moment + // FIXME: the eror handling should inject these headers in the streamed response to report mid-stream errors + "Trailer": "Lambda-Runtime-Function-Error-Type, Lambda-Runtime-Function-Error-Body", + ] + } + + func nextInvocation(isolation: isolated (any Actor)? = #isolation) async throws -> Invocation { + switch self.state { + case .connected(let context, .idle): + return try await withCheckedThrowingContinuation { + (continuation: CheckedContinuation) in + self.state = .connected(context, .waitingForNextInvocation(continuation)) + self.sendNextRequest(context: context) + } + + case .connected(_, .sendingResponse), + .connected(_, .sentResponse), + .connected(_, .waitingForNextInvocation), + .connected(_, .waitingForResponse), + .closing: + fatalError("Invalid state: \(self.state)") + + case .disconnected: + throw LambdaRuntimeError(code: .connectionToControlPlaneLost) + } + } + + func reportError( + isolation: isolated (any Actor)? = #isolation, + _ error: any Error, + requestID: String + ) async throws { + switch self.state { + case .connected(_, .waitingForNextInvocation): + fatalError("Invalid state: \(self.state)") + + case .connected(let context, .waitingForResponse): + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + self.state = .connected(context, .sentResponse(continuation)) + self.sendReportErrorRequest(requestID: requestID, error: error, context: context) + } + + case .connected(let context, .sendingResponse): + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + self.state = .connected(context, .sentResponse(continuation)) + self.sendResponseStreamingFailure(error: error, context: context) + } + + case .connected(_, .idle), + .connected(_, .sentResponse): + // The final response has already been sent. The only way to report the unhandled error + // now is to log it. Normally this library never logs higher than debug, we make an + // exception here, as there is no other way of reporting the error. + self.logger.error( + "Unhandled error after stream has finished", + metadata: [ + "lambda_request_id": "\(requestID)", + "lambda_error": "\(String(describing: error))", + ] + ) + + case .disconnected: + throw LambdaRuntimeError(code: .connectionToControlPlaneLost) + + case .closing: + throw LambdaRuntimeError(code: .connectionToControlPlaneGoingAway) + } + } + + func writeResponseBodyPart( + isolation: isolated (any Actor)? = #isolation, + _ byteBuffer: ByteBuffer, + requestID: String, + hasCustomHeaders: Bool + ) async throws { + switch self.state { + case .connected(_, .waitingForNextInvocation): + fatalError("Invalid state: \(self.state)") + + case .connected(let context, .waitingForResponse): + self.state = .connected(context, .sendingResponse) + try await self.sendResponseBodyPart( + byteBuffer, + sendHeadWithRequestID: requestID, + context: context, + hasCustomHeaders: hasCustomHeaders + ) + + case .connected(let context, .sendingResponse): + + precondition(!hasCustomHeaders, "Programming error: Custom headers should not be sent in this state") + + try await self.sendResponseBodyPart( + byteBuffer, + sendHeadWithRequestID: nil, + context: context, + hasCustomHeaders: hasCustomHeaders + ) + + case .connected(_, .idle), + .connected(_, .sentResponse): + throw LambdaRuntimeError(code: .writeAfterFinishHasBeenSent) + + case .disconnected: + throw LambdaRuntimeError(code: .connectionToControlPlaneLost) + + case .closing: + throw LambdaRuntimeError(code: .connectionToControlPlaneGoingAway) + } + } + + func finishResponseRequest( + isolation: isolated (any Actor)? = #isolation, + finalData: ByteBuffer?, + requestID: String + ) async throws { + switch self.state { + case .connected(_, .idle), + .connected(_, .waitingForNextInvocation): + fatalError("Invalid state: \(self.state)") + + case .connected(let context, .waitingForResponse): + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + self.state = .connected(context, .sentResponse(continuation)) + self.sendResponseFinish(finalData, sendHeadWithRequestID: requestID, context: context) + } + + case .connected(let context, .sendingResponse): + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + self.state = .connected(context, .sentResponse(continuation)) + self.sendResponseFinish(finalData, sendHeadWithRequestID: nil, context: context) + } + + case .connected(_, .sentResponse): + throw LambdaRuntimeError(code: .finishAfterFinishHasBeenSent) + + case .disconnected: + throw LambdaRuntimeError(code: .connectionToControlPlaneLost) + + case .closing: + throw LambdaRuntimeError(code: .connectionToControlPlaneGoingAway) + } + } + + private func sendResponseBodyPart( + isolation: isolated (any Actor)? = #isolation, + _ byteBuffer: ByteBuffer, + sendHeadWithRequestID: String?, + context: ChannelHandlerContext, + hasCustomHeaders: Bool + ) async throws { + + if let requestID = sendHeadWithRequestID { + // TODO: This feels super expensive. We should be able to make this cheaper. requestIDs are fixed length. + let url = Consts.invocationURLPrefix + "/" + requestID + Consts.postResponseURLSuffix + + var headers = self.streamingHeaders + if hasCustomHeaders { + // this header is required by Function URL when the user sends custom status code or headers + headers.add(name: "Content-Type", value: "application/vnd.awslambda.http-integration-response") + } + let httpRequest = HTTPRequestHead( + version: .http1_1, + method: .POST, + uri: url, + headers: headers + ) + + context.write(self.wrapOutboundOut(.head(httpRequest)), promise: nil) + } + + let future = context.write(self.wrapOutboundOut(.body(.byteBuffer(byteBuffer)))) + context.flush() + try await future.get() + } + + private func sendResponseFinish( + isolation: isolated (any Actor)? = #isolation, + _ byteBuffer: ByteBuffer?, + sendHeadWithRequestID: String?, + context: ChannelHandlerContext + ) { + if let requestID = sendHeadWithRequestID { + // TODO: This feels quite expensive. We should be able to make this cheaper. requestIDs are fixed length. + let url = "\(Consts.invocationURLPrefix)/\(requestID)\(Consts.postResponseURLSuffix)" + + // If we have less than 6MB, we don't want to use the streaming API. If we have more + // than 6MB, we must use the streaming mode. + var headers: HTTPHeaders! + if byteBuffer?.readableBytes ?? 0 < 6_000_000 { + headers = self.defaultHeaders + headers.add(name: "content-length", value: "\(byteBuffer?.readableBytes ?? 0)") + } else { + headers = self.largeResponseHeaders + } + let httpRequest = HTTPRequestHead( + version: .http1_1, + method: .POST, + uri: url, + headers: headers + ) + + context.write(self.wrapOutboundOut(.head(httpRequest)), promise: nil) + } + + if let byteBuffer { + context.write(self.wrapOutboundOut(.body(.byteBuffer(byteBuffer))), promise: nil) + } + + context.write(self.wrapOutboundOut(.end(nil)), promise: nil) + context.flush() + } + + private func sendNextRequest(context: ChannelHandlerContext) { + let httpRequest = HTTPRequestHead( + version: .http1_1, + method: .GET, + uri: self.nextInvocationPath, + headers: self.defaultHeaders + ) + + context.write(self.wrapOutboundOut(.head(httpRequest)), promise: nil) + context.write(self.wrapOutboundOut(.end(nil)), promise: nil) + context.flush() + } + + private func sendReportErrorRequest(requestID: String, error: any Error, context: ChannelHandlerContext) { + // TODO: This feels quite expensive. We should be able to make this cheaper. requestIDs are fixed length + let url = "\(Consts.invocationURLPrefix)/\(requestID)\(Consts.postErrorURLSuffix)" + + let httpRequest = HTTPRequestHead( + version: .http1_1, + method: .POST, + uri: url, + headers: self.errorHeaders + ) + + if self.reusableErrorBuffer == nil { + self.reusableErrorBuffer = context.channel.allocator.buffer(capacity: 1024) + } else { + self.reusableErrorBuffer!.clear() + } + + let errorResponse = ErrorResponse(errorType: Consts.functionError, errorMessage: "\(error)") + // TODO: Write this directly into our ByteBuffer + let bytes = errorResponse.toJSONBytes() + self.reusableErrorBuffer!.writeBytes(bytes) + + context.write(self.wrapOutboundOut(.head(httpRequest)), promise: nil) + context.write(self.wrapOutboundOut(.body(.byteBuffer(self.reusableErrorBuffer!))), promise: nil) + context.write(self.wrapOutboundOut(.end(nil)), promise: nil) + context.flush() + } + + private func sendResponseStreamingFailure(error: any Error, context: ChannelHandlerContext) { + let trailers: HTTPHeaders = [ + "Lambda-Runtime-Function-Error-Type": "Unhandled", + "Lambda-Runtime-Function-Error-Body": "Requires base64", + ] + + context.write(self.wrapOutboundOut(.end(trailers)), promise: nil) + context.flush() + } + + func cancelCurrentRequestAndCloseConnection() { + fatalError("Unimplemented") + } +} + +@available(LambdaSwift 2.0, *) +extension LambdaChannelHandler: ChannelInboundHandler { + typealias OutboundIn = Never + typealias InboundIn = NIOHTTPClientResponseFull + typealias OutboundOut = HTTPClientRequestPart + + func handlerAdded(context: ChannelHandlerContext) { + if context.channel.isActive { + self.state = .connected(context, .idle) + } + } + + func channelActive(context: ChannelHandlerContext) { + switch self.state { + case .disconnected: + self.state = .connected(context, .idle) + case .connected: + break + case .closing: + fatalError("Invalid state: \(self.state)") + } + } + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let response = unwrapInboundIn(data) + + // handle response content + + switch self.state { + case .connected(let context, .waitingForNextInvocation(let continuation)): + do { + let metadata = try InvocationMetadata(headers: response.head.headers) + self.state = .connected(context, .waitingForResponse) + continuation.resume(returning: Invocation(metadata: metadata, event: response.body ?? ByteBuffer())) + } catch { + self.state = .closing + + self.delegate.connectionWillClose(channel: context.channel) + context.close(promise: nil) + continuation.resume( + throwing: LambdaRuntimeError(code: .invocationMissingMetadata, underlying: error) + ) + } + + case .connected(let context, .sentResponse(let continuation)): + if response.head.status == .accepted { + self.state = .connected(context, .idle) + continuation.resume() + } else { + self.state = .connected(context, .idle) + continuation.resume(throwing: LambdaRuntimeError(code: .unexpectedStatusCodeForRequest)) + } + + case .disconnected, .closing, .connected(_, _): + break + } + + // As defined in RFC 7230 Section 6.3: + // HTTP/1.1 defaults to the use of "persistent connections", allowing + // multiple requests and responses to be carried over a single + // connection. The "close" connection option is used to signal that a + // connection will not persist after the current request/response. HTTP + // implementations SHOULD support persistent connections. + // + // That's why we only assume the connection shall be closed if we receive + // a "connection = close" header. + let serverCloseConnection = + response.head.headers["connection"].contains(where: { $0.lowercased() == "close" }) + + let closeConnection = serverCloseConnection || response.head.version != .http1_1 + + if closeConnection { + // If we were succeeding the request promise here directly and closing the connection + // after succeeding the promise we may run into a race condition: + // + // The lambda runtime will ask for the next work item directly after a succeeded post + // response request. The desire for the next work item might be faster than the attempt + // to close the connection. This will lead to a situation where we try to the connection + // but the next request has already been scheduled on the connection that we want to + // close. For this reason we postpone succeeding the promise until the connection has + // been closed. This codepath will only be hit in the very, very unlikely event of the + // Lambda control plane demanding to close connection. (It's more or less only + // implemented to support http1.1 correctly.) This behavior is ensured with the test + // `LambdaTest.testNoKeepAliveServer`. + self.state = .closing + self.delegate.connectionWillClose(channel: context.channel) + context.close(promise: nil) + } + } + + func errorCaught(context: ChannelHandlerContext, error: Error) { + self.logger.trace( + "Channel error caught", + metadata: [ + "error": "\(error)" + ] + ) + // pending responses will fail with lastError in channelInactive since we are calling context.close + self.delegate.connectionErrorHappened(error, channel: context.channel) + + self.lastError = error + context.channel.close(promise: nil) + } + + func channelInactive(context: ChannelHandlerContext) { + // fail any pending responses with last error or assume peer disconnected + switch self.state { + case .connected(_, let lambdaState): + switch lambdaState { + case .waitingForNextInvocation(let continuation): + continuation.resume(throwing: self.lastError ?? ChannelError.ioOnClosedChannel) + case .sentResponse(let continuation): + continuation.resume(throwing: self.lastError ?? ChannelError.ioOnClosedChannel) + case .idle, .sendingResponse, .waitingForResponse: + break + } + self.state = .disconnected + default: + break + } + + // we don't need to forward channelInactive to the delegate, as the delegate observes the + // closeFuture + context.fireChannelInactive() + } +} diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift new file mode 100644 index 00000000..b7a5a0a4 --- /dev/null +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift @@ -0,0 +1,441 @@ +//===----------------------------------------------------------------------===// +// +// 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 Logging +import NIOCore +import NIOHTTP1 +import NIOPosix + +@available(LambdaSwift 2.0, *) +@usableFromInline +final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol { + @usableFromInline + nonisolated let unownedExecutor: UnownedSerialExecutor + + @usableFromInline + struct Configuration: Sendable { + var ip: String + var port: Int + + @usableFromInline + init(ip: String, port: Int) { + self.ip = ip + self.port = port + } + } + + @usableFromInline + struct Writer: LambdaRuntimeClientResponseStreamWriter, Sendable { + private var runtimeClient: LambdaRuntimeClient + + fileprivate init(runtimeClient: LambdaRuntimeClient) { + self.runtimeClient = runtimeClient + } + + @usableFromInline + func write(_ buffer: NIOCore.ByteBuffer, hasCustomHeaders: Bool = false) async throws { + try await self.runtimeClient.write(buffer, hasCustomHeaders: hasCustomHeaders) + } + + @usableFromInline + func finish() async throws { + try await self.runtimeClient.writeAndFinish(nil) + } + + @usableFromInline + func writeAndFinish(_ buffer: NIOCore.ByteBuffer) async throws { + try await self.runtimeClient.writeAndFinish(buffer) + } + + @usableFromInline + func reportError(_ error: any Error) async throws { + try await self.runtimeClient.reportError(error) + } + } + + private typealias ConnectionContinuation = CheckedContinuation< + NIOLoopBound>, any Error + > + + private enum ConnectionState { + case disconnected + case connecting([ConnectionContinuation]) + case connected(Channel, LambdaChannelHandler) + } + + enum LambdaState { + /// this is the "normal" state. Transitions to `waitingForNextInvocation` + case idle(previousRequestID: String?) + /// this is the state while we wait for an invocation. A next call is running. + /// Transitions to `waitingForResponse` + case waitingForNextInvocation + /// The invocation was forwarded to the handler and we wait for a response. + /// Transitions to `sendingResponse` or `sentResponse`. + case waitingForResponse(requestID: String) + case sendingResponse(requestID: String) + case sentResponse(requestID: String) + } + + enum ClosingState { + case notClosing + case closing(CheckedContinuation) + case closed + } + + private let eventLoop: any EventLoop + private let logger: Logger + private let configuration: Configuration + + private var connectionState: ConnectionState = .disconnected + + private var lambdaState: LambdaState = .idle(previousRequestID: nil) + private var closingState: ClosingState = .notClosing + + // connections that are currently being closed. In the `run` method we must await all of them + // being fully closed before we can return from it. + private var closingConnections: [any Channel] = [] + + @inlinable + static func withRuntimeClient( + configuration: Configuration, + eventLoop: any EventLoop, + logger: Logger, + _ body: (LambdaRuntimeClient) async throws -> Result + ) async throws -> Result { + let runtime = LambdaRuntimeClient(configuration: configuration, eventLoop: eventLoop, logger: logger) + let result: Swift.Result + do { + result = .success(try await body(runtime)) + } catch { + result = .failure(error) + } + await runtime.close() + return try result.get() + } + + @usableFromInline + init(configuration: Configuration, eventLoop: any EventLoop, logger: Logger) { + self.unownedExecutor = eventLoop.executor.asUnownedSerialExecutor() + self.configuration = configuration + self.eventLoop = eventLoop + self.logger = logger + } + + @usableFromInline + func close() async { + self.logger.trace("Close lambda runtime client") + + guard case .notClosing = self.closingState else { + return + } + await withCheckedContinuation { continuation in + self.closingState = .closing(continuation) + + switch self.connectionState { + case .disconnected: + if self.closingConnections.isEmpty { + return continuation.resume() + } + + case .connecting(let continuations): + for continuation in continuations { + continuation.resume(throwing: LambdaRuntimeError(code: .closingRuntimeClient)) + } + self.connectionState = .connecting([]) + + case .connected(let channel, _): + channel.close(mode: .all, promise: nil) + } + } + } + + @usableFromInline + func nextInvocation() async throws -> (Invocation, Writer) { + + try Task.checkCancellation() + + return 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)") + } + } onCancel: { + Task { + await self.close() + } + } + } + + private func write(_ buffer: NIOCore.ByteBuffer, hasCustomHeaders: Bool = false) async throws { + switch self.lambdaState { + case .idle, .sentResponse: + throw LambdaRuntimeError(code: .writeAfterFinishHasBeenSent) + + case .waitingForNextInvocation: + fatalError("Invalid state: \(self.lambdaState)") + + case .waitingForResponse(let requestID): + self.lambdaState = .sendingResponse(requestID: requestID) + fallthrough + + case .sendingResponse(let requestID): + let handler = try await self.makeOrGetConnection() + guard case .sendingResponse(requestID) = self.lambdaState else { + fatalError("Invalid state: \(self.lambdaState)") + } + return try await handler.writeResponseBodyPart( + buffer, + requestID: requestID, + hasCustomHeaders: hasCustomHeaders + ) + } + } + + private func writeAndFinish(_ buffer: NIOCore.ByteBuffer?) async throws { + switch self.lambdaState { + case .idle, .sentResponse: + throw LambdaRuntimeError(code: .finishAfterFinishHasBeenSent) + + case .waitingForNextInvocation: + fatalError("Invalid state: \(self.lambdaState)") + + case .waitingForResponse(let requestID): + fallthrough + + case .sendingResponse(let requestID): + self.lambdaState = .sentResponse(requestID: requestID) + let handler = try await self.makeOrGetConnection() + guard case .sentResponse(requestID) = self.lambdaState else { + fatalError("Invalid state: \(self.lambdaState)") + } + try await handler.finishResponseRequest(finalData: buffer, requestID: requestID) + guard case .sentResponse(requestID) = self.lambdaState else { + fatalError("Invalid state: \(self.lambdaState)") + } + self.lambdaState = .idle(previousRequestID: requestID) + } + } + + private func reportError(_ error: any Error) async throws { + switch self.lambdaState { + case .idle, .waitingForNextInvocation, .sentResponse: + fatalError("Invalid state: \(self.lambdaState)") + + case .waitingForResponse(let requestID): + fallthrough + + case .sendingResponse(let requestID): + self.lambdaState = .sentResponse(requestID: requestID) + let handler = try await self.makeOrGetConnection() + guard case .sentResponse(requestID) = self.lambdaState else { + fatalError("Invalid state: \(self.lambdaState)") + } + try await handler.reportError(error, requestID: requestID) + guard case .sentResponse(requestID) = self.lambdaState else { + fatalError("Invalid state: \(self.lambdaState)") + } + self.lambdaState = .idle(previousRequestID: requestID) + } + } + + private func channelClosed(_ channel: any Channel) { + switch (self.connectionState, self.closingState) { + case (_, .closed): + fatalError("Invalid state: \(self.connectionState), \(self.closingState)") + + case (.disconnected, .notClosing): + if let index = self.closingConnections.firstIndex(where: { $0 === channel }) { + self.closingConnections.remove(at: index) + } + + case (.disconnected, .closing(let continuation)): + if let index = self.closingConnections.firstIndex(where: { $0 === channel }) { + self.closingConnections.remove(at: index) + } + + if self.closingConnections.isEmpty { + self.closingState = .closed + continuation.resume() + } + + case (.connecting(let array), .notClosing): + self.connectionState = .disconnected + for continuation in array { + continuation.resume(throwing: LambdaRuntimeError(code: .connectionToControlPlaneLost)) + } + + case (.connecting(let array), .closing(let continuation)): + self.connectionState = .disconnected + precondition(array.isEmpty, "If we are closing we should have failed all connection attempts already") + if self.closingConnections.isEmpty { + self.closingState = .closed + continuation.resume() + } + + case (.connected, .notClosing): + self.connectionState = .disconnected + + case (.connected, .closing(let continuation)): + self.connectionState = .disconnected + + if self.closingConnections.isEmpty { + self.closingState = .closed + continuation.resume() + } + } + } + + private func makeOrGetConnection() async throws -> LambdaChannelHandler { + switch self.connectionState { + case .disconnected: + self.connectionState = .connecting([]) + break + case .connecting(var array): + // Since we do get sequential invocations this case normally should never be hit. + // We'll support it anyway. + let loopBound = try await withCheckedThrowingContinuation { (continuation: ConnectionContinuation) in + array.append(continuation) + self.connectionState = .connecting(array) + } + return loopBound.value + case .connected(_, let handler): + return handler + } + + let bootstrap = ClientBootstrap(group: self.eventLoop) + .channelInitializer { channel in + do { + try channel.pipeline.syncOperations.addHTTPClientHandlers() + // Lambda quotas... An invocation payload is maximal 6MB in size: + // https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html + try channel.pipeline.syncOperations.addHandler( + NIOHTTPClientResponseAggregator(maxContentLength: 6 * 1024 * 1024) + ) + try channel.pipeline.syncOperations.addHandler( + LambdaChannelHandler( + delegate: self, + logger: self.logger, + configuration: self.configuration + ) + ) + return channel.eventLoop.makeSucceededFuture(()) + } catch { + return channel.eventLoop.makeFailedFuture(error) + } + } + .connectTimeout(.seconds(2)) + + do { + // connect directly via socket address to avoid happy eyeballs (perf) + let address = try SocketAddress(ipAddress: self.configuration.ip, port: self.configuration.port) + let channel = try await bootstrap.connect(to: address).get() + let handler = try channel.pipeline.syncOperations.handler( + type: LambdaChannelHandler.self + ) + self.logger.trace( + "Connection to control plane created", + metadata: [ + "lambda_port": "\(self.configuration.port)", + "lambda_ip": "\(self.configuration.ip)", + ] + ) + channel.closeFuture.whenComplete { result in + self.assumeIsolated { runtimeClient in + // close the channel + runtimeClient.channelClosed(channel) + runtimeClient.connectionState = .disconnected + } + } + + switch self.connectionState { + case .disconnected, .connected: + fatalError("Unexpected state: \(self.connectionState)") + + case .connecting(let array): + self.connectionState = .connected(channel, handler) + defer { + let loopBound = NIOLoopBound(handler, eventLoop: self.eventLoop) + for continuation in array { + continuation.resume(returning: loopBound) + } + } + return handler + } + } catch { + + switch self.connectionState { + case .disconnected, .connected: + fatalError("Unexpected state: \(self.connectionState)") + + case .connecting(let array): + self.connectionState = .disconnected + defer { + for continuation in array { + continuation.resume(throwing: error) + } + } + throw error + } + } + } +} + +@available(LambdaSwift 2.0, *) +extension LambdaRuntimeClient: LambdaChannelHandlerDelegate { + nonisolated func connectionErrorHappened(_ error: any Error, channel: any Channel) {} + + nonisolated func connectionWillClose(channel: any Channel) { + self.assumeIsolated { isolated in + switch isolated.connectionState { + case .disconnected: + // this case should never happen. But whatever + if channel.isActive { + isolated.closingConnections.append(channel) + } + + case .connecting(let continuations): + // this case should never happen. But whatever + if channel.isActive { + isolated.closingConnections.append(channel) + } + + for continuation in continuations { + continuation.resume(throwing: LambdaRuntimeError(code: .connectionToControlPlaneLost)) + } + + case .connected(let stateChannel, _): + guard channel === stateChannel else { + isolated.closingConnections.append(channel) + return + } + + isolated.connectionState = .disconnected + } + } + } +} diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeClientProtocol.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeClientProtocol.swift new file mode 100644 index 00000000..409fa8a0 --- /dev/null +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeClientProtocol.swift @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +@usableFromInline +package protocol LambdaRuntimeClientResponseStreamWriter: LambdaResponseStreamWriter { + func write(_ buffer: ByteBuffer, hasCustomHeaders: Bool) async throws + func finish() async throws + func writeAndFinish(_ buffer: ByteBuffer) async throws + func reportError(_ error: any Error) async throws +} + +@usableFromInline +@available(LambdaSwift 2.0, *) +package protocol LambdaRuntimeClientProtocol { + associatedtype Writer: LambdaRuntimeClientResponseStreamWriter + + func nextInvocation() async throws -> (Invocation, Writer) +} + +@usableFromInline +@available(LambdaSwift 2.0, *) +package struct Invocation: Sendable { + @usableFromInline + package var metadata: InvocationMetadata + @usableFromInline + package var event: ByteBuffer + + package init(metadata: InvocationMetadata, event: ByteBuffer) { + self.metadata = metadata + self.event = event + } +} diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift new file mode 100644 index 00000000..bc4865db --- /dev/null +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +@usableFromInline +package struct LambdaRuntimeError: Error { + @usableFromInline + package enum Code: Sendable { + /// internal error codes for LambdaRuntimeClient + case closingRuntimeClient + + case connectionToControlPlaneLost + case connectionToControlPlaneGoingAway + case invocationMissingMetadata + + case writeAfterFinishHasBeenSent + case finishAfterFinishHasBeenSent + case unexpectedStatusCodeForRequest + + case nextInvocationMissingHeaderRequestID + case nextInvocationMissingHeaderDeadline + case nextInvocationMissingHeaderInvokeFuctionARN + + case missingLambdaRuntimeAPIEnvironmentVariable + case runtimeCanOnlyBeStartedOnce + case handlerCanOnlyBeGetOnce + case invalidPort + } + + @usableFromInline + package init(code: Code, underlying: (any Error)? = nil) { + self.code = code + self.underlying = underlying + } + + @usableFromInline + package var code: Code + @usableFromInline + package var underlying: (any Error)? + +} diff --git a/Sources/AWSLambdaRuntime/SendableMetatype.swift b/Sources/AWSLambdaRuntime/SendableMetatype.swift new file mode 100644 index 00000000..a33f1aab --- /dev/null +++ b/Sources/AWSLambdaRuntime/SendableMetatype.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#if compiler(>=6.2) +@_documentation(visibility: internal) +public typealias _Lambda_SendableMetatype = SendableMetatype +#else +@_documentation(visibility: internal) +public typealias _Lambda_SendableMetatype = Any +#endif diff --git a/Sources/AWSLambdaRuntimeCore/Utils.swift b/Sources/AWSLambdaRuntime/Utils.swift similarity index 70% rename from Sources/AWSLambdaRuntimeCore/Utils.swift rename to Sources/AWSLambdaRuntime/Utils.swift index 9924a05b..f38e25c6 100644 --- a/Sources/AWSLambdaRuntimeCore/Utils.swift +++ b/Sources/AWSLambdaRuntime/Utils.swift @@ -12,10 +12,12 @@ // //===----------------------------------------------------------------------===// -import Dispatch +import NIOConcurrencyHelpers import NIOPosix -internal enum Consts { +// import Synchronization + +enum Consts { static let apiPrefix = "/2018-06-01" static let invocationURLPrefix = "\(apiPrefix)/runtime/invocation" static let getNextInvocationURLSuffix = "/next" @@ -27,7 +29,7 @@ internal enum Consts { } /// AWS Lambda HTTP Headers, used to populate the `LambdaContext` object. -internal enum AmazonHeaders { +enum AmazonHeaders { static let requestID = "Lambda-Runtime-Aws-Request-Id" static let traceID = "Lambda-Runtime-Trace-Id" static let clientContext = "X-Amz-Client-Context" @@ -36,41 +38,6 @@ internal enum AmazonHeaders { static let invokedFunctionARN = "Lambda-Runtime-Invoked-Function-Arn" } -/// Helper function to trap signals -internal func trap(signal sig: Signal, handler: @escaping (Signal) -> Void) -> DispatchSourceSignal { - let signalSource = DispatchSource.makeSignalSource(signal: sig.rawValue, queue: DispatchQueue.global()) - signal(sig.rawValue, SIG_IGN) - signalSource.setEventHandler(handler: { - signalSource.cancel() - handler(sig) - }) - signalSource.resume() - return signalSource -} - -internal enum Signal: Int32 { - case HUP = 1 - case INT = 2 - case QUIT = 3 - case ABRT = 6 - case KILL = 9 - case ALRM = 14 - case TERM = 15 -} - -extension DispatchWallTime { - internal init(millisSinceEpoch: Int64) { - let nanoSinceEpoch = UInt64(millisSinceEpoch) * 1_000_000 - let seconds = UInt64(nanoSinceEpoch / 1_000_000_000) - let nanoseconds = nanoSinceEpoch - (seconds * 1_000_000_000) - self.init(timespec: timespec(tv_sec: Int(seconds), tv_nsec: Int(nanoseconds))) - } - - internal var millisSinceEpoch: Int64 { - Int64(bitPattern: self.rawValue) / -1_000_000 - } -} - extension String { func encodeAsJSONString(into bytes: inout [UInt8]) { bytes.append(UInt8(ascii: "\"")) @@ -80,7 +47,7 @@ extension String { while nextIndex != stringBytes.endIndex { switch stringBytes[nextIndex] { - case 0 ..< 32, UInt8(ascii: "\""), UInt8(ascii: "\\"): + case 0..<32, UInt8(ascii: "\""), UInt8(ascii: "\\"): // All Unicode characters may be placed within the // quotation marks, except for the characters that MUST be escaped: // quotation mark, reverse solidus, and the control characters (U+0000 @@ -88,7 +55,7 @@ extension String { // https://tools.ietf.org/html/rfc7159#section-7 // copy the current range over - bytes.append(contentsOf: stringBytes[startCopyIndex ..< nextIndex]) + bytes.append(contentsOf: stringBytes[startCopyIndex.. String { + static func generateXRayTraceID() -> String { // The version number, that is, 1. let version: UInt = 1 // The time of the original request, in Unix epoch time, in 8 hexadecimal digits. - let now = UInt32(DispatchWallTime.now().millisSinceEpoch / 1000) + let now = UInt32(LambdaClock().now.millisecondsSinceEpoch() / 1000) let dateValue = String(now, radix: 16, uppercase: false) let datePadding = String(repeating: "0", count: max(0, 8 - dateValue.count)) // A 96-bit identifier for the trace, globally unique, in 24 hexadecimal digits. - let identifier = String(UInt64.random(in: UInt64.min ... UInt64.max) | 1 << 63, radix: 16, uppercase: false) - + String(UInt32.random(in: UInt32.min ... UInt32.max) | 1 << 31, radix: 16, uppercase: false) + let identifier = + String(UInt64.random(in: UInt64.min...UInt64.max) | 1 << 63, radix: 16, uppercase: false) + + String(UInt32.random(in: UInt32.min...UInt32.max) | 1 << 31, radix: 16, uppercase: false) return "\(version)-\(datePadding)\(dateValue)-\(identifier)" } } + +/// Temporary storage for value being sent from one isolation domain to another +// use NIOLockedValueBox instead of Mutex to avoid compiler crashes on 6.0 +// see https://github.com/swiftlang/swift/issues/78048 +@usableFromInline +struct SendingStorage: ~Copyable, @unchecked Sendable { + @usableFromInline + struct ValueAlreadySentError: Error { + @usableFromInline + init() {} + } + + @usableFromInline + // let storage: Mutex + let storage: NIOLockedValueBox + + @inlinable + init(_ value: sending Value) { + self.storage = .init(value) + } + + @inlinable + func get() throws -> Value { + // try self.storage.withLock { + try self.storage.withLockedValue { + guard let value = $0 else { throw ValueAlreadySentError() } + $0 = nil + return value + } + } +} diff --git a/Sources/AWSLambdaRuntime/Version.swift b/Sources/AWSLambdaRuntime/Version.swift new file mode 100644 index 00000000..f55c30f1 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Version.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// The version of the AWS Lambda Runtime. +/// +/// This is used in the User Agent header when making requests to the AWS Lambda data Plane. +/// +/// - Note: This is a static property that returns the current version of the AWS Lambda Runtime. +/// It is used to ensure that the runtime can be identified by the AWS Lambda service. +/// As such, we mainly care about major version and minor version. Patch and pre-release versions are ignored. +package enum Version { + /// The current version of the AWS Lambda Runtime. + package static let current = "2.0" +} diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-01-package-init.sh b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-01-package-init.sh deleted file mode 100644 index a54719a3..00000000 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-01-package-init.sh +++ /dev/null @@ -1,2 +0,0 @@ -# Create a project directory -mkdir SquareNumber && cd SquareNumber \ No newline at end of file diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-02-package-init.sh b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-02-package-init.sh deleted file mode 100644 index a96b825c..00000000 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-02-package-init.sh +++ /dev/null @@ -1,4 +0,0 @@ -# Create a project directory -mkdir SquareNumber && cd SquareNumber -# create a skeleton project -swift package init --type executable \ No newline at end of file diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-01-main.swift b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-01-main.swift deleted file mode 100644 index 35d722a6..00000000 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-01-main.swift +++ /dev/null @@ -1,3 +0,0 @@ - -@main -struct SquareNumberHandler: SimpleLambdaHandler {} diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-02-main.swift b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-02-main.swift deleted file mode 100644 index b4b0b28d..00000000 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-02-main.swift +++ /dev/null @@ -1,4 +0,0 @@ -import AWSLambdaRuntime - -@main -struct SquareNumberHandler: SimpleLambdaHandler {} diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-03-main.swift b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-03-main.swift deleted file mode 100644 index bf17f189..00000000 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-03-main.swift +++ /dev/null @@ -1,6 +0,0 @@ -import AWSLambdaRuntime - -@main -struct SquareNumberHandler: SimpleLambdaHandler { - func handle(_ event: Event, context: LambdaContext) async throws -> Output {} -} diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-04-main.swift b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-04-main.swift deleted file mode 100644 index 6647aec0..00000000 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-04-main.swift +++ /dev/null @@ -1,14 +0,0 @@ -import AWSLambdaRuntime - -struct Input: Codable { - let number: Double -} - -struct Number: Codable { - let result: Double -} - -@main -struct SquareNumberHandler: SimpleLambdaHandler { - func handle(_ event: Event, context: LambdaContext) async throws -> Output {} -} diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-05-main.swift b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-05-main.swift deleted file mode 100644 index 805fb5ab..00000000 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-05-main.swift +++ /dev/null @@ -1,17 +0,0 @@ -import AWSLambdaRuntime - -struct Input: Codable { - let number: Double -} - -struct Number: Codable { - let result: Double -} - -@main -struct SquareNumberHandler: SimpleLambdaHandler { - typealias Event = Input - typealias Output = Number - - func handle(_ event: Event, context: LambdaContext) async throws -> Output {} -} diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-06-main.swift b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-06-main.swift deleted file mode 100644 index a8257061..00000000 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-06-main.swift +++ /dev/null @@ -1,19 +0,0 @@ -import AWSLambdaRuntime - -struct Input: Codable { - let number: Double -} - -struct Number: Codable { - let result: Double -} - -@main -struct SquareNumberHandler: SimpleLambdaHandler { - typealias Event = Input - typealias Output = Number - - func handle(_ event: Event, context: LambdaContext) async throws -> Output { - Number(result: event.number * event.number) - } -} diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-03-console-output.sh b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-03-console-output.sh deleted file mode 100644 index 7ba3caf2..00000000 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-03-console-output.sh +++ /dev/null @@ -1,5 +0,0 @@ -2023-04-14T11:42:21+0200 info LocalLambdaServer : [AWSLambdaRuntimeCore] LocalLambdaServer started and listening on 127.0.0.1:7000, receiving events on /invoke -2023-04-14T11:42:21+0200 info Lambda : [AWSLambdaRuntimeCore] lambda runtime starting with LambdaConfiguration - General(logLevel: info)) - Lifecycle(id: 104957691689708, maxTimes: 0, stopSignal: TERM) - RuntimeEngine(ip: 127.0.0.1, port: 7000, requestTimeout: nil diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-04-curl.sh b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-04-curl.sh deleted file mode 100644 index 46dbccc0..00000000 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-04-curl.sh +++ /dev/null @@ -1,5 +0,0 @@ -curl --header "Content-Type: application/json" \ - --request POST \ - --data '{"number": 3}' \ - http://localhost:7000/invoke - diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-05-curl.sh b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-05-curl.sh deleted file mode 100644 index c5dcbe12..00000000 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-05-curl.sh +++ /dev/null @@ -1,6 +0,0 @@ -curl --header "Content-Type: application/json" \ - --request POST \ - --data '{"number": 3}' \ - http://localhost:7000/invoke - -{"result":9} diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-06-terminal.sh b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-06-terminal.sh deleted file mode 100644 index 39c4f4d7..00000000 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-06-terminal.sh +++ /dev/null @@ -1,2 +0,0 @@ -export LOCAL_LAMBDA_SERVER_ENABLED=true - diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-07-terminal.sh b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-07-terminal.sh deleted file mode 100644 index f6bc5e7f..00000000 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-07-terminal.sh +++ /dev/null @@ -1,2 +0,0 @@ -export LOCAL_LAMBDA_SERVER_ENABLED=true -swift run diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-08-terminal.sh b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-08-terminal.sh deleted file mode 100644 index b1a57a88..00000000 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-08-terminal.sh +++ /dev/null @@ -1,10 +0,0 @@ -export LOCAL_LAMBDA_SERVER_ENABLED=true -swift run - -Building for debugging... -Build complete! (0.20s) -2023-04-14T10:52:25+0200 info LocalLambdaServer : [AWSLambdaRuntimeCore] LocalLambdaServer started and listening on 127.0.0.1:7000, receiving events on /invoke -2023-04-14T10:52:25+0200 info Lambda : [AWSLambdaRuntimeCore] lambda runtime starting with LambdaConfiguration - General(logLevel: info)) - Lifecycle(id: 102943961260250, maxTimes: 0, stopSignal: TERM) - RuntimeEngine(ip: 127.0.0.1, port: 7000, requestTimeout: nil diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-02-plugin-archive.sh b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-02-plugin-archive.sh deleted file mode 100644 index 41e7a628..00000000 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-02-plugin-archive.sh +++ /dev/null @@ -1,2 +0,0 @@ -swift package --disable-sandbox plugin archive - diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-03-plugin-archive.sh b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-03-plugin-archive.sh deleted file mode 100644 index 9878f478..00000000 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-03-plugin-archive.sh +++ /dev/null @@ -1,19 +0,0 @@ -swift package --disable-sandbox plugin archive - -------------------------------------------------------------------------- -building "squarenumberlambda" in docker -------------------------------------------------------------------------- -updating "swift:amazonlinux2" docker image - amazonlinux2: Pulling from library/swift - Digest: sha256:5b0cbe56e35210fa90365ba3a4db9cd2b284a5b74d959fc1ee56a13e9c35b378 - Status: Image is up to date for swift:amazonlinux2 - docker.io/library/swift:amazonlinux2 -building "SquareNumberLambda" - Building for production... -... -------------------------------------------------------------------------- -archiving "SquareNumberLambda" -------------------------------------------------------------------------- -1 archive created - * SquareNumberLambda at /Users/YourUserName/SquareNumberLambda/.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/SquareNumberLambda/SquareNumberLambda.zip - diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-04-plugin-archive.sh b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-04-plugin-archive.sh deleted file mode 100644 index 7652bf1c..00000000 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-04-plugin-archive.sh +++ /dev/null @@ -1,21 +0,0 @@ -swift package --disable-sandbox plugin archive - -------------------------------------------------------------------------- -building "squarenumberlambda" in docker -------------------------------------------------------------------------- -updating "swift:amazonlinux2" docker image - amazonlinux2: Pulling from library/swift - Digest: sha256:5b0cbe56e35210fa90365ba3a4db9cd2b284a5b74d959fc1ee56a13e9c35b378 - Status: Image is up to date for swift:amazonlinux2 - docker.io/library/swift:amazonlinux2 -building "SquareNumberLambda" - Building for production... -... -------------------------------------------------------------------------- -archiving "SquareNumberLambda" -------------------------------------------------------------------------- -1 archive created - * SquareNumberLambda at /Users/YourUserName/SquareNumberLambda/.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/SquareNumberLambda/SquareNumberLambda.zip - - -cp .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/SquareNumberLambda/SquareNumberLambda.zip ~/Desktop diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-terminal-package-init.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-terminal-package-init.png deleted file mode 100644 index 4e4a2f24..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-terminal-package-init.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-xcode@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-xcode@2x.png deleted file mode 100644 index 4aedcca1..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-xcode@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-xcode~dark@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-xcode~dark@2x.png deleted file mode 100644 index d22af823..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-xcode~dark@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-03-01-rename-file@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-03-01-rename-file@2x.png deleted file mode 100644 index 53d681a9..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-03-01-rename-file@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-03-01-rename-file~dark@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-03-01-rename-file~dark@2x.png deleted file mode 100644 index 22e2814e..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-03-01-rename-file~dark@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-01-edit-scheme@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-01-edit-scheme@2x.png deleted file mode 100644 index 7ad57885..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-01-edit-scheme@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-01-edit-scheme~dark@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-01-edit-scheme~dark@2x.png deleted file mode 100644 index dc92b003..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-01-edit-scheme~dark@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-02-add-variable@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-02-add-variable@2x.png deleted file mode 100644 index 3393f211..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-02-add-variable@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-02-add-variable~dark@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-02-add-variable~dark@2x.png deleted file mode 100644 index 2b8e428d..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-02-add-variable~dark@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-03-select-region@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-03-select-region@2x.png deleted file mode 100644 index 05fb1343..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-03-select-region@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-04-select-lambda@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-04-select-lambda@2x.png deleted file mode 100644 index f6f13344..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-04-select-lambda@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-05-create-function@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-05-create-function@2x.png deleted file mode 100644 index d8c97d1e..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-05-create-function@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-05-create-function~dark@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-05-create-function~dark@2x.png deleted file mode 100644 index 0715079c..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-05-create-function~dark@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-06-create-function@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-06-create-function@2x.png deleted file mode 100644 index e3877acb..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-06-create-function@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-06-create-function~dark@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-06-create-function~dark@2x.png deleted file mode 100644 index 76795ecd..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-06-create-function~dark@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-07-upload-zip@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-07-upload-zip@2x.png deleted file mode 100644 index 23fd565a..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-07-upload-zip@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-07-upload-zip~dark@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-07-upload-zip~dark@2x.png deleted file mode 100644 index 4503837b..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-07-upload-zip~dark@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-08-upload-zip@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-08-upload-zip@2x.png deleted file mode 100644 index b47ecf34..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-08-upload-zip@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-08-upload-zip~dark@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-08-upload-zip~dark@2x.png deleted file mode 100644 index cc630173..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-08-upload-zip~dark@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-09-test-lambda@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-09-test-lambda@2x.png deleted file mode 100644 index 865d23b3..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-09-test-lambda@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-09-test-lambda~dark@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-09-test-lambda~dark@2x.png deleted file mode 100644 index b197f191..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-09-test-lambda~dark@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-10-test-lambda-result@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-10-test-lambda-result@2x.png deleted file mode 100644 index 174afb17..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-10-test-lambda-result@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-10-test-lambda-result~dark@2x.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-10-test-lambda-result~dark@2x.png deleted file mode 100644 index 61791e78..00000000 Binary files a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-10-test-lambda-result~dark@2x.png and /dev/null differ diff --git a/Sources/AWSLambdaRuntimeCore/HTTPClient.swift b/Sources/AWSLambdaRuntimeCore/HTTPClient.swift deleted file mode 100644 index 7e724485..00000000 --- a/Sources/AWSLambdaRuntimeCore/HTTPClient.swift +++ /dev/null @@ -1,320 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2021 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 NIOConcurrencyHelpers -import NIOCore -import NIOHTTP1 -import NIOPosix - -/// A barebone HTTP client to interact with AWS Runtime Engine which is an HTTP server. -/// Note that Lambda Runtime API dictate that only one requests runs at a time. -/// This means we can avoid locks and other concurrency concern we would otherwise need to build into the client -internal final class HTTPClient { - private let eventLoop: EventLoop - private let configuration: LambdaConfiguration.RuntimeEngine - private let targetHost: String - - private var state = State.disconnected - private var executing = false - - init(eventLoop: EventLoop, configuration: LambdaConfiguration.RuntimeEngine) { - self.eventLoop = eventLoop - self.configuration = configuration - self.targetHost = "\(self.configuration.ip):\(self.configuration.port)" - } - - func get(url: String, headers: HTTPHeaders, timeout: TimeAmount? = nil) -> EventLoopFuture { - self.execute(Request(targetHost: self.targetHost, - url: url, - method: .GET, - headers: headers, - timeout: timeout ?? self.configuration.requestTimeout)) - } - - func post(url: String, headers: HTTPHeaders, body: ByteBuffer?, timeout: TimeAmount? = nil) -> EventLoopFuture { - self.execute(Request(targetHost: self.targetHost, - url: url, - method: .POST, - headers: headers, - body: body, - timeout: timeout ?? self.configuration.requestTimeout)) - } - - /// cancels the current request if there is one - func cancel() { - guard self.executing else { - // there is no request running. nothing to cancel - return - } - - guard case .connected(let channel) = self.state else { - preconditionFailure("if we are executing, we expect to have an open channel") - } - - channel.triggerUserOutboundEvent(RequestCancelEvent(), promise: nil) - } - - // TODO: cap reconnect attempt - private func execute(_ request: Request, validate: Bool = true) -> EventLoopFuture { - if validate { - precondition(self.executing == false, "expecting single request at a time") - self.executing = true - } - - switch self.state { - case .disconnected: - return self.connect().flatMap { channel -> EventLoopFuture in - self.state = .connected(channel) - return self.execute(request, validate: false) - } - case .connected(let channel): - guard channel.isActive else { - self.state = .disconnected - return self.execute(request, validate: false) - } - - let promise = channel.eventLoop.makePromise(of: Response.self) - promise.futureResult.whenComplete { _ in - precondition(self.executing == true, "invalid execution state") - self.executing = false - } - let wrapper = HTTPRequestWrapper(request: request, promise: promise) - channel.writeAndFlush(wrapper).cascadeFailure(to: promise) - return promise.futureResult - } - } - - private func connect() -> EventLoopFuture { - let bootstrap = ClientBootstrap(group: self.eventLoop) - .channelInitializer { channel in - do { - try channel.pipeline.syncOperations.addHTTPClientHandlers() - // Lambda quotas... An invocation payload is maximal 6MB in size: - // https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html - try channel.pipeline.syncOperations.addHandler( - NIOHTTPClientResponseAggregator(maxContentLength: 6 * 1024 * 1024)) - try channel.pipeline.syncOperations.addHandler(LambdaChannelHandler()) - return channel.eventLoop.makeSucceededFuture(()) - } catch { - return channel.eventLoop.makeFailedFuture(error) - } - } - - do { - // connect directly via socket address to avoid happy eyeballs (perf) - let address = try SocketAddress(ipAddress: self.configuration.ip, port: self.configuration.port) - return bootstrap.connect(to: address) - } catch { - return self.eventLoop.makeFailedFuture(error) - } - } - - internal struct Request: Equatable { - let url: String - let method: HTTPMethod - let targetHost: String - let headers: HTTPHeaders - let body: ByteBuffer? - let timeout: TimeAmount? - - init(targetHost: String, url: String, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: ByteBuffer? = nil, timeout: TimeAmount?) { - self.targetHost = targetHost - self.url = url - self.method = method - self.headers = headers - self.body = body - self.timeout = timeout - } - } - - internal struct Response: Equatable { - var version: HTTPVersion - var status: HTTPResponseStatus - var headers: HTTPHeaders - var body: ByteBuffer? - } - - internal enum Errors: Error { - case connectionResetByPeer - case timeout - case cancelled - } - - private enum State { - case disconnected - case connected(Channel) - } -} - -// no need in locks since we validate only one request can run at a time -private final class LambdaChannelHandler: ChannelDuplexHandler { - typealias InboundIn = NIOHTTPClientResponseFull - typealias OutboundIn = HTTPRequestWrapper - typealias OutboundOut = HTTPClientRequestPart - - enum State { - case idle - case running(promise: EventLoopPromise, timeout: Scheduled?) - case waitForConnectionClose(HTTPClient.Response, EventLoopPromise) - } - - private var state: State = .idle - private var lastError: Error? - - init() {} - - func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { - guard case .idle = self.state else { - preconditionFailure("invalid state, outstanding request") - } - let wrapper = unwrapOutboundIn(data) - - var head = HTTPRequestHead( - version: .http1_1, - method: wrapper.request.method, - uri: wrapper.request.url, - headers: wrapper.request.headers - ) - head.headers.add(name: "host", value: wrapper.request.targetHost) - switch head.method { - case .POST, .PUT: - head.headers.add(name: "content-length", value: String(wrapper.request.body?.readableBytes ?? 0)) - default: - break - } - - let timeoutTask = wrapper.request.timeout.map { - context.eventLoop.scheduleTask(in: $0) { - guard case .running = self.state else { - preconditionFailure("invalid state") - } - - context.pipeline.fireErrorCaught(HTTPClient.Errors.timeout) - } - } - self.state = .running(promise: wrapper.promise, timeout: timeoutTask) - - context.write(wrapOutboundOut(.head(head)), promise: nil) - if let body = wrapper.request.body { - context.write(wrapOutboundOut(.body(IOData.byteBuffer(body))), promise: nil) - } - context.writeAndFlush(wrapOutboundOut(.end(nil)), promise: promise) - } - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - guard case .running(let promise, let timeout) = self.state else { - preconditionFailure("invalid state, no pending request") - } - - let response = unwrapInboundIn(data) - - let httpResponse = HTTPClient.Response( - version: response.head.version, - status: response.head.status, - headers: response.head.headers, - body: response.body - ) - - timeout?.cancel() - - // As defined in RFC 7230 Section 6.3: - // HTTP/1.1 defaults to the use of "persistent connections", allowing - // multiple requests and responses to be carried over a single - // connection. The "close" connection option is used to signal that a - // connection will not persist after the current request/response. HTTP - // implementations SHOULD support persistent connections. - // - // That's why we only assume the connection shall be closed if we receive - // a "connection = close" header. - let serverCloseConnection = - response.head.headers["connection"].contains(where: { $0.lowercased() == "close" }) - - let closeConnection = serverCloseConnection || response.head.version != .http1_1 - - if closeConnection { - // If we were succeeding the request promise here directly and closing the connection - // after succeeding the promise we may run into a race condition: - // - // The lambda runtime will ask for the next work item directly after a succeeded post - // response request. The desire for the next work item might be faster than the attempt - // to close the connection. This will lead to a situation where we try to the connection - // but the next request has already been scheduled on the connection that we want to - // close. For this reason we postpone succeeding the promise until the connection has - // been closed. This codepath will only be hit in the very, very unlikely event of the - // Lambda control plane demanding to close connection. (It's more or less only - // implemented to support http1.1 correctly.) This behavior is ensured with the test - // `LambdaTest.testNoKeepAliveServer`. - self.state = .waitForConnectionClose(httpResponse, promise) - _ = context.channel.close() - return - } else { - self.state = .idle - promise.succeed(httpResponse) - } - } - - func errorCaught(context: ChannelHandlerContext, error: Error) { - // pending responses will fail with lastError in channelInactive since we are calling context.close - self.lastError = error - context.channel.close(promise: nil) - } - - func channelInactive(context: ChannelHandlerContext) { - // fail any pending responses with last error or assume peer disconnected - context.fireChannelInactive() - - switch self.state { - case .idle: - break - case .running(let promise, let timeout): - self.state = .idle - timeout?.cancel() - promise.fail(self.lastError ?? HTTPClient.Errors.connectionResetByPeer) - - case .waitForConnectionClose(let response, let promise): - self.state = .idle - promise.succeed(response) - } - } - - func triggerUserOutboundEvent(context: ChannelHandlerContext, event: Any, promise: EventLoopPromise?) { - switch event { - case is RequestCancelEvent: - switch self.state { - case .idle: - break - case .running(let promise, let timeout): - self.state = .idle - timeout?.cancel() - promise.fail(HTTPClient.Errors.cancelled) - - // after the cancel error has been send, we want to close the connection so - // that no more packets can be read on this connection. - _ = context.channel.close() - case .waitForConnectionClose(_, let promise): - self.state = .idle - promise.fail(HTTPClient.Errors.cancelled) - } - default: - context.triggerUserOutboundEvent(event, promise: promise) - } - } -} - -private struct HTTPRequestWrapper { - let request: HTTPClient.Request - let promise: EventLoopPromise -} - -private struct RequestCancelEvent {} diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift deleted file mode 100644 index 1f8c9e0f..00000000 --- a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift +++ /dev/null @@ -1,302 +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 beind a #if DEBUG flag. -// For example: -// -// try Lambda.withLocalServer { -// Lambda.run { (context: LambdaContext, event: String, callback: @escaping (Result) -> Void) in -// callback(.success("Hello, \(event)!")) -// } -// } -extension Lambda { - /// Execute code in the context of a mock Lambda server. - /// - /// - parameters: - /// - invocationEndpoint: The endpoint to post events to. - /// - body: Code to run within the context of the mock server. Typically this would be a Lambda.run function call. - /// - /// - note: This API is designed strictly for local testing and is behind a DEBUG flag - internal static func withLocalServer(invocationEndpoint: String? = nil, _ body: @escaping () -> Value) throws -> Value { - let server = LocalLambda.Server(invocationEndpoint: invocationEndpoint) - try server.start().wait() - defer { try! server.stop() } - return body() - } -} - -// MARK: - Local Mock Server - -private enum LocalLambda { - struct Server { - private let logger: Logger - private let group: EventLoopGroup - private let host: String - private let port: Int - private let invocationEndpoint: String - - public init(invocationEndpoint: String?) { - let configuration = LambdaConfiguration() - var logger = Logger(label: "LocalLambdaServer") - logger.logLevel = configuration.general.logLevel - self.logger = logger - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - self.host = configuration.runtimeEngine.ip - self.port = configuration.runtimeEngine.port - self.invocationEndpoint = invocationEndpoint ?? "/invoke" - } - - func start() -> EventLoopFuture { - let bootstrap = ServerBootstrap(group: group) - .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) - .childChannelInitializer { channel in - channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap { _ in - channel.pipeline.addHandler(HTTPHandler(logger: self.logger, invocationEndpoint: self.invocationEndpoint)) - } - } - return bootstrap.bind(host: self.host, port: self.port).flatMap { channel -> EventLoopFuture in - guard channel.localAddress != nil else { - return channel.eventLoop.makeFailedFuture(ServerError.cantBind) - } - self.logger.info("LocalLambdaServer started and listening on \(self.host):\(self.port), receiving events on \(self.invocationEndpoint)") - return channel.eventLoop.makeSucceededFuture(()) - } - } - - func stop() throws { - try self.group.syncShutdownGracefully() - } - } - - final class HTTPHandler: ChannelInboundHandler { - public typealias InboundIn = HTTPServerRequestPart - public typealias OutboundOut = HTTPServerResponsePart - - private var pending = CircularBuffer<(head: HTTPRequestHead, body: ByteBuffer?)>() - - private static var invocations = CircularBuffer() - private static var invocationState = InvocationState.waitingForLambdaRequest - - private let logger: Logger - private let invocationEndpoint: String - - init(logger: Logger, invocationEndpoint: String) { - self.logger = logger - self.invocationEndpoint = invocationEndpoint - } - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - let requestPart = unwrapInboundIn(data) - - switch requestPart { - case .head(let head): - self.pending.append((head: head, body: nil)) - case .body(var buffer): - var request = self.pending.removeFirst() - if request.body == nil { - request.body = buffer - } else { - request.body!.writeBuffer(&buffer) - } - self.pending.prepend(request) - case .end: - let request = self.pending.removeFirst() - self.processRequest(context: context, request: request) - } - } - - func processRequest(context: ChannelHandlerContext, request: (head: HTTPRequestHead, body: ByteBuffer?)) { - switch (request.head.method, request.head.uri) { - // this endpoint is called by the client invoking the lambda - case (.POST, let url) where url.hasSuffix(self.invocationEndpoint): - guard let work = request.body else { - return self.writeResponse(context: context, response: .init(status: .badRequest)) - } - let requestID = "\(DispatchTime.now().uptimeNanoseconds)" // FIXME: - let promise = context.eventLoop.makePromise(of: Response.self) - promise.futureResult.whenComplete { result in - switch result { - case .failure(let error): - self.logger.error("invocation error: \(error)") - self.writeResponse(context: context, response: .init(status: .internalServerError)) - case .success(let response): - self.writeResponse(context: context, response: response) - } - } - let invocation = Invocation(requestID: requestID, request: work, responsePromise: promise) - switch Self.invocationState { - case .waitingForInvocation(let promise): - promise.succeed(invocation) - case .waitingForLambdaRequest, .waitingForLambdaResponse: - Self.invocations.append(invocation) - } - - // lambda invocation using the wrong http method - case (_, let url) where url.hasSuffix(self.invocationEndpoint): - self.writeResponse(context: context, status: .methodNotAllowed) - - // /next endpoint is called by the lambda polling for work - case (.GET, let url) where url.hasSuffix(Consts.getNextInvocationURLSuffix): - // check if our server is in the correct state - guard case .waitingForLambdaRequest = Self.invocationState else { - self.logger.error("invalid invocation state \(Self.invocationState)") - self.writeResponse(context: context, response: .init(status: .unprocessableEntity)) - return - } - - // pop the first task from the queue - switch Self.invocations.popFirst() { - case .none: - // if there is nothing in the queue, - // create a promise that we can fullfill when we get a new task - let promise = context.eventLoop.makePromise(of: Invocation.self) - promise.futureResult.whenComplete { result in - switch result { - case .failure(let error): - self.logger.error("invocation error: \(error)") - self.writeResponse(context: context, status: .internalServerError) - case .success(let invocation): - Self.invocationState = .waitingForLambdaResponse(invocation) - self.writeResponse(context: context, response: invocation.makeResponse()) - } - } - Self.invocationState = .waitingForInvocation(promise) - case .some(let invocation): - // if there is a task pending, we can immediatly respond with it. - Self.invocationState = .waitingForLambdaResponse(invocation) - self.writeResponse(context: context, response: invocation.makeResponse()) - } - - // :requestID/response endpoint is called by the lambda posting the response - case (.POST, let url) where url.hasSuffix(Consts.postResponseURLSuffix): - let parts = request.head.uri.split(separator: "/") - guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { - // the request is malformed, since we were expecting a requestId in the path - return self.writeResponse(context: context, status: .badRequest) - } - guard case .waitingForLambdaResponse(let invocation) = Self.invocationState else { - // a response was send, but we did not expect to receive one - self.logger.error("invalid invocation state \(Self.invocationState)") - return self.writeResponse(context: context, status: .unprocessableEntity) - } - guard requestID == invocation.requestID else { - // the request's requestId is not matching the one we are expecting - self.logger.error("invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)") - return self.writeResponse(context: context, status: .badRequest) - } - - invocation.responsePromise.succeed(.init(status: .ok, body: request.body)) - self.writeResponse(context: context, status: .accepted) - Self.invocationState = .waitingForLambdaRequest - - // :requestID/error endpoint is called by the lambda posting an error response - case (.POST, let url) where url.hasSuffix(Consts.postErrorURLSuffix): - let parts = request.head.uri.split(separator: "/") - guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { - // the request is malformed, since we were expecting a requestId in the path - return self.writeResponse(context: context, status: .badRequest) - } - guard case .waitingForLambdaResponse(let invocation) = Self.invocationState else { - // a response was send, but we did not expect to receive one - self.logger.error("invalid invocation state \(Self.invocationState)") - return self.writeResponse(context: context, status: .unprocessableEntity) - } - guard requestID == invocation.requestID else { - // the request's requestId is not matching the one we are expecting - self.logger.error("invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)") - return self.writeResponse(context: context, status: .badRequest) - } - - invocation.responsePromise.succeed(.init(status: .internalServerError, body: request.body)) - self.writeResponse(context: context, status: .accepted) - Self.invocationState = .waitingForLambdaRequest - - // unknown call - default: - self.writeResponse(context: context, status: .notFound) - } - } - - func writeResponse(context: ChannelHandlerContext, status: HTTPResponseStatus) { - self.writeResponse(context: context, response: .init(status: status)) - } - - func writeResponse(context: ChannelHandlerContext, response: Response) { - var headers = HTTPHeaders(response.headers ?? []) - headers.add(name: "content-length", value: "\(response.body?.readableBytes ?? 0)") - let head = HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: response.status, headers: headers) - - context.write(wrapOutboundOut(.head(head))).whenFailure { error in - self.logger.error("\(self) write error \(error)") - } - - if let buffer = response.body { - context.write(wrapOutboundOut(.body(.byteBuffer(buffer)))).whenFailure { error in - self.logger.error("\(self) write error \(error)") - } - } - - context.writeAndFlush(wrapOutboundOut(.end(nil))).whenComplete { result in - if case .failure(let error) = result { - self.logger.error("\(self) write error \(error)") - } - } - } - - struct Response { - var status: HTTPResponseStatus = .ok - var headers: [(String, String)]? - var body: ByteBuffer? - } - - struct Invocation { - let requestID: String - let request: ByteBuffer - let responsePromise: EventLoopPromise - - func makeResponse() -> Response { - var response = Response() - response.body = self.request - // required headers - response.headers = [ - (AmazonHeaders.requestID, self.requestID), - (AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:\(Int16.random(in: Int16.min ... Int16.max)):function:custom-runtime"), - (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), - (AmazonHeaders.deadline, "\(DispatchWallTime.distantFuture.millisSinceEpoch)"), - ] - return response - } - } - - enum InvocationState { - case waitingForInvocation(EventLoopPromise) - case waitingForLambdaRequest - case waitingForLambdaResponse(Invocation) - } - } - - enum ServerError: Error { - case notReady - case cantBind - } -} -#endif diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+String.swift b/Sources/AWSLambdaRuntimeCore/Lambda+String.swift deleted file mode 100644 index e7674e28..00000000 --- a/Sources/AWSLambdaRuntimeCore/Lambda+String.swift +++ /dev/null @@ -1,77 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// -import NIOCore - -// MARK: - SimpleLambdaHandler String support - -extension SimpleLambdaHandler where Event == String { - /// Implementation of a `ByteBuffer` to `String` decoding. - @inlinable - public func decode(buffer: ByteBuffer) throws -> Event { - guard let value = buffer.getString(at: buffer.readerIndex, length: buffer.readableBytes) else { - throw CodecError.invalidString - } - return value - } -} - -extension SimpleLambdaHandler where Output == String { - /// Implementation of `String` to `ByteBuffer` encoding. - @inlinable - public func encode(value: Output, into buffer: inout ByteBuffer) throws { - buffer.writeString(value) - } -} - -// MARK: - LambdaHandler String support - -extension LambdaHandler where Event == String { - /// Implementation of a `ByteBuffer` to `String` decoding. - @inlinable - public func decode(buffer: ByteBuffer) throws -> Event { - guard let value = buffer.getString(at: buffer.readerIndex, length: buffer.readableBytes) else { - throw CodecError.invalidString - } - return value - } -} - -extension LambdaHandler where Output == String { - /// Implementation of `String` to `ByteBuffer` encoding. - @inlinable - public func encode(value: Output, into buffer: inout ByteBuffer) throws { - buffer.writeString(value) - } -} - -// MARK: - EventLoopLambdaHandler String support - -extension EventLoopLambdaHandler where Event == String { - /// Implementation of `String` to `ByteBuffer` encoding. - @inlinable - public func decode(buffer: ByteBuffer) throws -> Event { - guard let value = buffer.getString(at: buffer.readerIndex, length: buffer.readableBytes) else { - throw CodecError.invalidString - } - return value - } -} - -extension EventLoopLambdaHandler where Output == String { - /// Implementation of a `ByteBuffer` to `String` decoding. - @inlinable - public func encode(value: Output, into buffer: inout ByteBuffer) throws { - buffer.writeString(value) - } -} diff --git a/Sources/AWSLambdaRuntimeCore/Lambda.swift b/Sources/AWSLambdaRuntimeCore/Lambda.swift deleted file mode 100644 index f23dc57c..00000000 --- a/Sources/AWSLambdaRuntimeCore/Lambda.swift +++ /dev/null @@ -1,171 +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 -// -//===----------------------------------------------------------------------===// - -#if os(Linux) -import Glibc -#else -import Darwin.C -#endif - -#if swift(<5.9) -import Backtrace -#endif -import Logging -import NIOCore -import NIOPosix - -public enum Lambda { - /// Run a Lambda defined by implementing the ``SimpleLambdaHandler`` protocol. - /// The Runtime will manage the Lambdas application lifecycle automatically. - /// - /// - parameters: - /// - configuration: A Lambda runtime configuration object - /// - handlerType: The Handler to create and invoke. - /// - /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - internal static func run( - configuration: LambdaConfiguration = .init(), - handlerType: Handler.Type - ) -> Result { - Self.run(configuration: configuration, handlerProvider: CodableSimpleLambdaHandler.makeHandler(context:)) - } - - /// Run a Lambda defined by implementing the ``LambdaHandler`` protocol. - /// The Runtime will manage the Lambdas application lifecycle automatically. It will invoke the - /// ``LambdaHandler/makeHandler(context:)`` to create a new Handler. - /// - /// - parameters: - /// - configuration: A Lambda runtime configuration object - /// - handlerType: The Handler to create and invoke. - /// - /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - internal static func run( - configuration: LambdaConfiguration = .init(), - handlerType: Handler.Type - ) -> Result { - Self.run(configuration: configuration, handlerProvider: CodableLambdaHandler.makeHandler(context:)) - } - - /// Run a Lambda defined by implementing the ``EventLoopLambdaHandler`` protocol. - /// The Runtime will manage the Lambdas application lifecycle automatically. It will invoke the - /// ``EventLoopLambdaHandler/makeHandler(context:)`` to create a new Handler. - /// - /// - parameters: - /// - configuration: A Lambda runtime configuration object - /// - handlerType: The Handler to create and invoke. - /// - /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - internal static func run( - configuration: LambdaConfiguration = .init(), - handlerType: Handler.Type - ) -> Result { - Self.run(configuration: configuration, handlerProvider: CodableEventLoopLambdaHandler.makeHandler(context:)) - } - - /// Run a Lambda defined by implementing the ``ByteBufferLambdaHandler`` protocol. - /// The Runtime will manage the Lambdas application lifecycle automatically. It will invoke the - /// ``ByteBufferLambdaHandler/makeHandler(context:)`` to create a new Handler. - /// - /// - parameters: - /// - configuration: A Lambda runtime configuration object - /// - handlerType: The Handler to create and invoke. - /// - /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - internal static func run( - configuration: LambdaConfiguration = .init(), - handlerType: (some ByteBufferLambdaHandler).Type - ) -> Result { - Self.run(configuration: configuration, handlerProvider: handlerType.makeHandler(context:)) - } - - /// Run a Lambda defined by implementing the ``LambdaRuntimeHandler`` protocol. - /// - parameters: - /// - configuration: A Lambda runtime configuration object - /// - handlerProvider: A provider of the ``LambdaRuntimeHandler`` to invoke. - /// - /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - internal static func run( - configuration: LambdaConfiguration = .init(), - handlerProvider: @escaping (LambdaInitializationContext) -> EventLoopFuture - ) -> Result { - let _run = { (configuration: LambdaConfiguration) -> Result in - #if swift(<5.9) - Backtrace.install() - #endif - var logger = Logger(label: "Lambda") - logger.logLevel = configuration.general.logLevel - - var result: Result! - MultiThreadedEventLoopGroup.withCurrentThreadAsEventLoop { eventLoop in - let runtime = LambdaRuntime( - handlerProvider: handlerProvider, - eventLoop: eventLoop, - logger: logger, - configuration: configuration - ) - #if DEBUG - let signalSource = trap(signal: configuration.lifecycle.stopSignal) { signal in - logger.info("intercepted signal: \(signal)") - runtime.shutdown() - } - #endif - - runtime.start().flatMap { - runtime.shutdownFuture - }.whenComplete { lifecycleResult in - #if DEBUG - signalSource.cancel() - #endif - eventLoop.shutdownGracefully { error in - if let error = error { - preconditionFailure("Failed to shutdown eventloop: \(error)") - } - } - result = lifecycleResult - } - } - logger.info("shutdown completed") - return result - } - - // start local server for debugging in DEBUG mode only - #if DEBUG - if Lambda.env("LOCAL_LAMBDA_SERVER_ENABLED").flatMap(Bool.init) ?? false { - do { - return try Lambda.withLocalServer { - _run(configuration) - } - } catch { - return .failure(error) - } - } else { - return _run(configuration) - } - #else - return _run(configuration) - #endif - } -} - -// MARK: - Public API - -extension Lambda { - /// Utility to access/read environment variables - public static func env(_ name: String) -> String? { - guard let value = getenv(name) else { - return nil - } - return String(cString: value) - } -} diff --git a/Sources/AWSLambdaRuntimeCore/LambdaConfiguration.swift b/Sources/AWSLambdaRuntimeCore/LambdaConfiguration.swift deleted file mode 100644 index 33d056f8..00000000 --- a/Sources/AWSLambdaRuntimeCore/LambdaConfiguration.swift +++ /dev/null @@ -1,86 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2021 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 Logging -import NIOCore - -internal struct LambdaConfiguration: CustomStringConvertible { - let general: General - let lifecycle: Lifecycle - let runtimeEngine: RuntimeEngine - - init() { - self.init(general: .init(), lifecycle: .init(), runtimeEngine: .init()) - } - - init(general: General? = nil, lifecycle: Lifecycle? = nil, runtimeEngine: RuntimeEngine? = nil) { - self.general = general ?? General() - self.lifecycle = lifecycle ?? Lifecycle() - self.runtimeEngine = runtimeEngine ?? RuntimeEngine() - } - - struct General: CustomStringConvertible { - let logLevel: Logger.Level - - init(logLevel: Logger.Level? = nil) { - self.logLevel = logLevel ?? Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info - } - - var description: String { - "\(General.self)(logLevel: \(self.logLevel))" - } - } - - struct Lifecycle: CustomStringConvertible { - let id: String - let maxTimes: Int - let stopSignal: Signal - - init(id: String? = nil, maxTimes: Int? = nil, stopSignal: Signal? = nil) { - self.id = id ?? "\(DispatchTime.now().uptimeNanoseconds)" - self.maxTimes = maxTimes ?? Lambda.env("MAX_REQUESTS").flatMap(Int.init) ?? 0 - self.stopSignal = stopSignal ?? Lambda.env("STOP_SIGNAL").flatMap(Int32.init).flatMap(Signal.init) ?? Signal.TERM - precondition(self.maxTimes >= 0, "maxTimes must be equal or larger than 0") - } - - var description: String { - "\(Lifecycle.self)(id: \(self.id), maxTimes: \(self.maxTimes), stopSignal: \(self.stopSignal))" - } - } - - struct RuntimeEngine: CustomStringConvertible { - let ip: String - let port: Int - let requestTimeout: TimeAmount? - - init(address: String? = nil, keepAlive: Bool? = nil, requestTimeout: TimeAmount? = nil) { - let ipPort = (address ?? Lambda.env("AWS_LAMBDA_RUNTIME_API"))?.split(separator: ":") ?? ["127.0.0.1", "7000"] - guard ipPort.count == 2, let port = Int(ipPort[1]) else { - preconditionFailure("invalid ip+port configuration \(ipPort)") - } - self.ip = String(ipPort[0]) - self.port = port - self.requestTimeout = requestTimeout ?? Lambda.env("REQUEST_TIMEOUT").flatMap(Int64.init).flatMap { .milliseconds($0) } - } - - var description: String { - "\(RuntimeEngine.self)(ip: \(self.ip), port: \(self.port), requestTimeout: \(String(describing: self.requestTimeout))" - } - } - - var description: String { - "\(Self.self)\n \(self.general))\n \(self.lifecycle)\n \(self.runtimeEngine)" - } -} diff --git a/Sources/AWSLambdaRuntimeCore/LambdaContext.swift b/Sources/AWSLambdaRuntimeCore/LambdaContext.swift deleted file mode 100644 index 24e960a4..00000000 --- a/Sources/AWSLambdaRuntimeCore/LambdaContext.swift +++ /dev/null @@ -1,215 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 swift(<5.9) -@preconcurrency import Dispatch -#else -import Dispatch -#endif - -import Logging -import NIOCore - -// MARK: - InitializationContext - -/// Lambda runtime initialization context. -/// The Lambda runtime generates and passes the `LambdaInitializationContext` to the Handlers -/// ``ByteBufferLambdaHandler/makeHandler(context:)`` or ``LambdaHandler/init(context:)`` -/// as an argument. -public struct LambdaInitializationContext: Sendable { - /// `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. - /// - /// - 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`. - public let allocator: ByteBufferAllocator - - /// ``LambdaTerminator`` to register shutdown operations. - public let terminator: LambdaTerminator - - init(logger: Logger, eventLoop: EventLoop, allocator: ByteBufferAllocator, terminator: LambdaTerminator) { - self.eventLoop = eventLoop - self.logger = logger - self.allocator = allocator - self.terminator = terminator - } - - /// This interface is not part of the public API and must not be used by adopters. This API is not part of semver versioning. - public static func __forTestsOnly( - logger: Logger, - eventLoop: EventLoop - ) -> LambdaInitializationContext { - LambdaInitializationContext( - logger: logger, - eventLoop: eventLoop, - allocator: ByteBufferAllocator(), - terminator: LambdaTerminator() - ) - } -} - -// MARK: - Context - -/// Lambda runtime context. -/// The Lambda runtime generates and passes the `LambdaContext` to the Lambda handler as an argument. -public struct LambdaContext: CustomDebugStringConvertible, Sendable { - final class _Storage: Sendable { - let requestID: String - let traceID: String - let invokedFunctionARN: String - let deadline: DispatchWallTime - let cognitoIdentity: String? - let clientContext: String? - let logger: Logger - let eventLoop: EventLoop - let allocator: ByteBufferAllocator - - init( - requestID: String, - traceID: String, - invokedFunctionARN: String, - deadline: DispatchWallTime, - cognitoIdentity: String?, - clientContext: String?, - logger: Logger, - eventLoop: EventLoop, - allocator: ByteBufferAllocator - ) { - self.requestID = requestID - self.traceID = traceID - self.invokedFunctionARN = invokedFunctionARN - self.deadline = deadline - self.cognitoIdentity = cognitoIdentity - self.clientContext = clientContext - self.logger = logger - self.eventLoop = eventLoop - self.allocator = allocator - } - } - - private var storage: _Storage - - /// The request ID, which identifies the request that triggered the function invocation. - public var requestID: String { - self.storage.requestID - } - - /// The AWS X-Ray tracing header. - public var traceID: String { - self.storage.traceID - } - - /// The ARN of the Lambda function, version, or alias that's specified in the invocation. - public var invokedFunctionARN: String { - self.storage.invokedFunctionARN - } - - /// The timestamp that the function times out. - public var deadline: DispatchWallTime { - self.storage.deadline - } - - /// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider. - public var cognitoIdentity: String? { - self.storage.cognitoIdentity - } - - /// For invocations from the AWS Mobile SDK, data about the client application and device. - public var clientContext: String? { - self.storage.clientContext - } - - /// `Logger` to log with. - /// - /// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable. - public var logger: Logger { - self.storage.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 var eventLoop: EventLoop { - self.storage.eventLoop - } - - /// `ByteBufferAllocator` to allocate `ByteBuffer`. - /// This is useful when implementing ``EventLoopLambdaHandler``. - public var allocator: ByteBufferAllocator { - self.storage.allocator - } - - init(requestID: String, - traceID: String, - invokedFunctionARN: String, - deadline: DispatchWallTime, - cognitoIdentity: String? = nil, - clientContext: String? = nil, - logger: Logger, - eventLoop: EventLoop, - allocator: ByteBufferAllocator) { - self.storage = _Storage( - requestID: requestID, - traceID: traceID, - invokedFunctionARN: invokedFunctionARN, - deadline: deadline, - cognitoIdentity: cognitoIdentity, - clientContext: clientContext, - logger: logger, - eventLoop: eventLoop, - allocator: allocator - ) - } - - public func getRemainingTime() -> TimeAmount { - let deadline = self.deadline.millisSinceEpoch - let now = DispatchWallTime.now().millisSinceEpoch - - let remaining = deadline - now - return .milliseconds(remaining) - } - - public var debugDescription: String { - "\(Self.self)(requestID: \(self.requestID), traceID: \(self.traceID), invokedFunctionARN: \(self.invokedFunctionARN), cognitoIdentity: \(self.cognitoIdentity ?? "nil"), clientContext: \(self.clientContext ?? "nil"), deadline: \(self.deadline))" - } - - /// This interface is not part of the public API and must not be used by adopters. This API is not part of semver versioning. - public static func __forTestsOnly( - requestID: String, - traceID: String, - invokedFunctionARN: String, - timeout: DispatchTimeInterval, - logger: Logger, - eventLoop: EventLoop - ) -> LambdaContext { - LambdaContext( - requestID: requestID, - traceID: traceID, - invokedFunctionARN: invokedFunctionARN, - deadline: .now() + timeout, - logger: logger, - eventLoop: eventLoop, - allocator: ByteBufferAllocator() - ) - } -} diff --git a/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift b/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift deleted file mode 100644 index 3a7e3c27..00000000 --- a/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift +++ /dev/null @@ -1,465 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -import Dispatch -import NIOCore - -// MARK: - SimpleLambdaHandler - -/// Strongly typed, processing protocol for a Lambda that takes a user defined -/// ``SimpleLambdaHandler/Event`` and returns a user defined -/// ``SimpleLambdaHandler/Output`` asynchronously. -/// -/// - note: Most users should implement the ``LambdaHandler`` protocol instead -/// which defines the Lambda initialization method. -public protocol SimpleLambdaHandler { - /// The lambda function's input. In most cases this should be `Codable`. If your event originates from an - /// AWS service, have a look at [AWSLambdaEvents](https://github.com/swift-server/swift-aws-lambda-events), - /// which provides a number of commonly used AWS Event implementations. - associatedtype Event - /// The lambda function's output. Can be `Void`. - associatedtype Output - - init() - - /// The Lambda handling method. - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - event: Event of type `Event` representing the event or request. - /// - context: Runtime ``LambdaContext``. - /// - /// - Returns: A Lambda result ot type `Output`. - func handle(_ event: Event, context: LambdaContext) async throws -> Output - - /// Encode a response of type ``Output`` to `ByteBuffer`. - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - parameters: - /// - value: Response of type ``Output``. - /// - buffer: A `ByteBuffer` to encode into, will be overwritten. - /// - /// - Returns: A `ByteBuffer` with the encoded version of the `value`. - func encode(value: Output, into buffer: inout ByteBuffer) throws - - /// Decode a `ByteBuffer` to a request or event of type ``Event``. - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - /// - parameters: - /// - buffer: The `ByteBuffer` to decode. - /// - /// - Returns: A request or event of type ``Event``. - func decode(buffer: ByteBuffer) throws -> Event -} - -@usableFromInline -final class CodableSimpleLambdaHandler: ByteBufferLambdaHandler { - @usableFromInline - let handler: Underlying - @usableFromInline - private(set) var outputBuffer: ByteBuffer - - @inlinable - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { - let promise = context.eventLoop.makePromise(of: CodableSimpleLambdaHandler.self) - promise.completeWithTask { - let handler = Underlying() - return CodableSimpleLambdaHandler(handler: handler, allocator: context.allocator) - } - return promise.futureResult - } - - @inlinable - init(handler: Underlying, allocator: ByteBufferAllocator) { - self.handler = handler - self.outputBuffer = allocator.buffer(capacity: 1024 * 1024) - } - - @inlinable - func handle(_ buffer: ByteBuffer, context: LambdaContext) -> EventLoopFuture { - let promise = context.eventLoop.makePromise(of: ByteBuffer?.self) - promise.completeWithTask { - let input: Underlying.Event - do { - input = try self.handler.decode(buffer: buffer) - } catch { - throw CodecError.requestDecoding(error) - } - - let output = try await self.handler.handle(input, context: context) - - do { - self.outputBuffer.clear() - try self.handler.encode(value: output, into: &self.outputBuffer) - return self.outputBuffer - } catch { - throw CodecError.responseEncoding(error) - } - } - return promise.futureResult - } -} - -/// Implementation of `ByteBuffer` to `Void` decoding. -extension SimpleLambdaHandler where Output == Void { - @inlinable - public func encode(value: Output, into buffer: inout ByteBuffer) throws {} -} - -extension SimpleLambdaHandler { - /// Initializes and runs the Lambda function. - /// - /// If you precede your ``SimpleLambdaHandler`` conformer's declaration with the - /// [@main](https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID626) - /// attribute, the system calls the conformer's `main()` method to launch the lambda function. - /// - /// The lambda runtime provides a default implementation of the method that manages the launch - /// process. - public static func main() { - _ = Lambda.run(configuration: .init(), handlerType: Self.self) - } -} - -// MARK: - LambdaHandler - -/// Strongly typed, processing protocol for a Lambda that takes a user defined -/// ``LambdaHandler/Event`` and returns a user defined -/// ``LambdaHandler/Output`` asynchronously. -/// -/// - note: Most users should implement this protocol instead of the lower -/// level protocols ``EventLoopLambdaHandler`` and -/// ``ByteBufferLambdaHandler``. -public protocol LambdaHandler { - /// The lambda function's input. In most cases this should be `Codable`. If your event originates from an - /// AWS service, have a look at [AWSLambdaEvents](https://github.com/swift-server/swift-aws-lambda-events), - /// which provides a number of commonly used AWS Event implementations. - associatedtype Event - /// The lambda function's output. Can be `Void`. - associatedtype Output - - /// The Lambda initialization method. - /// Use this method to initialize resources that will be used in every request. - /// - /// Examples for this can be HTTP or database clients. - /// - parameters: - /// - context: Runtime ``LambdaInitializationContext``. - init(context: LambdaInitializationContext) async throws - - /// The Lambda handling method. - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - event: Event of type `Event` representing the event or request. - /// - context: Runtime ``LambdaContext``. - /// - /// - Returns: A Lambda result ot type `Output`. - func handle(_ event: Event, context: LambdaContext) async throws -> Output - - /// Encode a response of type ``Output`` to `ByteBuffer`. - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - parameters: - /// - value: Response of type ``Output``. - /// - buffer: A `ByteBuffer` to encode into, will be overwritten. - /// - /// - Returns: A `ByteBuffer` with the encoded version of the `value`. - func encode(value: Output, into buffer: inout ByteBuffer) throws - - /// Decode a `ByteBuffer` to a request or event of type ``Event``. - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - /// - parameters: - /// - buffer: The `ByteBuffer` to decode. - /// - /// - Returns: A request or event of type ``Event``. - func decode(buffer: ByteBuffer) throws -> Event -} - -@usableFromInline -final class CodableLambdaHandler: ByteBufferLambdaHandler { - @usableFromInline - let handler: Underlying - @usableFromInline - private(set) var outputBuffer: ByteBuffer - - @inlinable - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { - let promise = context.eventLoop.makePromise(of: CodableLambdaHandler.self) - promise.completeWithTask { - let handler = try await Underlying(context: context) - return CodableLambdaHandler(handler: handler, allocator: context.allocator) - } - return promise.futureResult - } - - @inlinable - init(handler: Underlying, allocator: ByteBufferAllocator) { - self.handler = handler - self.outputBuffer = allocator.buffer(capacity: 1024 * 1024) - } - - @inlinable - func handle(_ buffer: ByteBuffer, context: LambdaContext) -> EventLoopFuture { - let promise = context.eventLoop.makePromise(of: ByteBuffer?.self) - promise.completeWithTask { - let input: Underlying.Event - do { - input = try self.handler.decode(buffer: buffer) - } catch { - throw CodecError.requestDecoding(error) - } - - let output = try await self.handler.handle(input, context: context) - - do { - self.outputBuffer.clear() - try self.handler.encode(value: output, into: &self.outputBuffer) - return self.outputBuffer - } catch { - throw CodecError.responseEncoding(error) - } - } - return promise.futureResult - } -} - -/// Implementation of `ByteBuffer` to `Void` decoding. -extension LambdaHandler where Output == Void { - @inlinable - public func encode(value: Output, into buffer: inout ByteBuffer) throws {} -} - -extension LambdaHandler { - /// Initializes and runs the Lambda function. - /// - /// If you precede your ``LambdaHandler`` conformer's declaration with the - /// [@main](https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID626) - /// attribute, the system calls the conformer's `main()` method to launch the lambda function. - /// - /// The lambda runtime provides a default implementation of the method that manages the launch - /// process. - public static func main() { - _ = Lambda.run(configuration: .init(), handlerType: Self.self) - } -} - -/// unchecked sendable wrapper for the handler -/// this is safe since lambda runtime is designed to calls the handler serially -@usableFromInline -internal struct UncheckedSendableHandler: @unchecked Sendable where Event == Underlying.Event, Output == Underlying.Output { - @usableFromInline - let underlying: Underlying - - @inlinable - init(underlying: Underlying) { - self.underlying = underlying - } - - @inlinable - func handle(_ event: Event, context: LambdaContext) async throws -> Output { - try await self.underlying.handle(event, context: context) - } -} - -// MARK: - EventLoopLambdaHandler - -/// Strongly typed, `EventLoopFuture` based processing protocol for a Lambda that takes a user -/// defined ``EventLoopLambdaHandler/Event`` and returns a user defined ``EventLoopLambdaHandler/Output`` asynchronously. -/// -/// - note: To implement a Lambda, implement either ``LambdaHandler`` or the -/// ``EventLoopLambdaHandler`` protocol. The ``LambdaHandler`` will offload -/// the Lambda execution to an async Task making processing safer but slower (due to -/// fewer thread hops). -/// The ``EventLoopLambdaHandler`` will execute the Lambda on the same `EventLoop` -/// as the core runtime engine, making the processing faster but requires more care from the -/// implementation to never block the `EventLoop`. Implement this protocol only in performance -/// critical situations and implement ``LambdaHandler`` in all other circumstances. -public protocol EventLoopLambdaHandler { - /// The lambda functions input. In most cases this should be `Codable`. If your event originates from an - /// AWS service, have a look at [AWSLambdaEvents](https://github.com/swift-server/swift-aws-lambda-events), - /// which provides a number of commonly used AWS Event implementations. - associatedtype Event - /// The lambda functions output. Can be `Void`. - associatedtype Output - - /// Create a Lambda handler for the runtime. - /// - /// Use this to initialize all your resources that you want to cache between invocations. This could be database - /// connections and HTTP clients for example. It is encouraged to use the given `EventLoop`'s conformance - /// to `EventLoopGroup` when initializing NIO dependencies. This will improve overall performance, as it - /// minimizes thread hopping. - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture - - /// The Lambda handling method. - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - context: Runtime ``LambdaContext``. - /// - event: Event of type `Event` 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 ``Output`` or an `Error`. - func handle(_ event: Event, context: LambdaContext) -> EventLoopFuture - - /// Encode a response of type ``Output`` to `ByteBuffer`. - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - parameters: - /// - value: Response of type ``Output``. - /// - buffer: A `ByteBuffer` to encode into, will be overwritten. - /// - /// - Returns: A `ByteBuffer` with the encoded version of the `value`. - func encode(value: Output, into buffer: inout ByteBuffer) throws - - /// Decode a `ByteBuffer` to a request or event of type ``Event``. - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - /// - parameters: - /// - buffer: The `ByteBuffer` to decode. - /// - /// - Returns: A request or event of type ``Event``. - func decode(buffer: ByteBuffer) throws -> Event -} - -/// Implementation of `ByteBuffer` to `Void` decoding. -extension EventLoopLambdaHandler where Output == Void { - @inlinable - public func encode(value: Output, into buffer: inout ByteBuffer) throws {} -} - -@usableFromInline -final class CodableEventLoopLambdaHandler: ByteBufferLambdaHandler { - @usableFromInline - let handler: Underlying - @usableFromInline - private(set) var outputBuffer: ByteBuffer - - @inlinable - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { - Underlying.makeHandler(context: context).map { handler -> CodableEventLoopLambdaHandler in - CodableEventLoopLambdaHandler(handler: handler, allocator: context.allocator) - } - } - - @inlinable - init(handler: Underlying, allocator: ByteBufferAllocator) { - self.handler = handler - self.outputBuffer = allocator.buffer(capacity: 1024 * 1024) - } - - @inlinable - func handle(_ buffer: ByteBuffer, context: LambdaContext) -> EventLoopFuture { - let input: Underlying.Event - do { - input = try self.handler.decode(buffer: buffer) - } catch { - return context.eventLoop.makeFailedFuture(CodecError.requestDecoding(error)) - } - - return self.handler.handle(input, context: context).flatMapThrowing { output in - do { - self.outputBuffer.clear() - try self.handler.encode(value: output, into: &self.outputBuffer) - return self.outputBuffer - } catch { - throw CodecError.responseEncoding(error) - } - } - } -} - -extension EventLoopLambdaHandler { - /// Initializes and runs the Lambda function. - /// - /// If you precede your ``EventLoopLambdaHandler`` conformer's declaration with the - /// [@main](https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID626) - /// attribute, the system calls the conformer's `main()` method to launch the lambda function. - /// - /// The lambda runtime provides a default implementation of the method that manages the launch - /// process. - public static func main() { - _ = Lambda.run(configuration: .init(), handlerType: Self.self) - } -} - -// MARK: - ByteBufferLambdaHandler - -/// An `EventLoopFuture` based processing protocol for a Lambda that takes a `ByteBuffer` and returns -/// an optional `ByteBuffer` asynchronously. -/// -/// - note: This is a low level protocol designed to power the higher level ``EventLoopLambdaHandler`` and -/// ``LambdaHandler`` based APIs. -/// Most users are not expected to use this protocol. -public protocol ByteBufferLambdaHandler: LambdaRuntimeHandler { - /// Create a Lambda handler for the runtime. - /// - /// Use this to initialize all your resources that you want to cache between invocations. This could be database - /// connections and HTTP clients for example. It is encouraged to use the given `EventLoop`'s conformance - /// to `EventLoopGroup` when initializing NIO dependencies. This will improve overall performance, as it - /// minimizes thread hopping. - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture - - /// The Lambda handling method. - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - context: Runtime ``LambdaContext``. - /// - event: The event or input 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(_ buffer: ByteBuffer, context: LambdaContext) -> EventLoopFuture -} - -extension ByteBufferLambdaHandler { - /// Initializes and runs the Lambda function. - /// - /// If you precede your ``ByteBufferLambdaHandler`` conformer's declaration with the - /// [@main](https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID626) - /// attribute, the system calls the conformer's `main()` method to launch the lambda function. - /// - /// The lambda runtime provides a default implementation of the method that manages the launch - /// process. - public static func main() { - _ = Lambda.run(configuration: .init(), handlerType: Self.self) - } -} - -// MARK: - LambdaRuntimeHandler - -/// An `EventLoopFuture` based processing protocol for a Lambda that takes a `ByteBuffer` and returns -/// an optional `ByteBuffer` asynchronously. -/// -/// - note: This is a low level protocol designed to enable use cases where a frameworks initializes the -/// runtime with a handler outside the normal initialization of -/// ``ByteBufferLambdaHandler``, ``EventLoopLambdaHandler`` and ``LambdaHandler`` based APIs. -/// Most users are not expected to use this protocol. -public protocol LambdaRuntimeHandler { - /// The Lambda handling method. - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - context: Runtime ``LambdaContext``. - /// - event: The event or input 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(_ buffer: ByteBuffer, context: LambdaContext) -> EventLoopFuture -} - -// MARK: - Other - -@usableFromInline -enum CodecError: Error { - case requestDecoding(Error) - case responseEncoding(Error) - case invalidString -} diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRunner.swift b/Sources/AWSLambdaRuntimeCore/LambdaRunner.swift deleted file mode 100644 index 9557f41f..00000000 --- a/Sources/AWSLambdaRuntimeCore/LambdaRunner.swift +++ /dev/null @@ -1,167 +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 Logging -import NIOCore - -/// LambdaRunner manages the Lambda runtime workflow, or business logic. -internal final class LambdaRunner { - private let runtimeClient: LambdaRuntimeClient - private let eventLoop: EventLoop - private let allocator: ByteBufferAllocator - - private var isGettingNextInvocation = false - - init(eventLoop: EventLoop, configuration: LambdaConfiguration) { - self.eventLoop = eventLoop - self.runtimeClient = LambdaRuntimeClient(eventLoop: self.eventLoop, configuration: configuration.runtimeEngine) - self.allocator = ByteBufferAllocator() - } - - /// Run the user provided initializer. This *must* only be called once. - /// - /// - Returns: An `EventLoopFuture` fulfilled with the outcome of the initialization. - func initialize( - handlerProvider: @escaping (LambdaInitializationContext) -> EventLoopFuture, - logger: Logger, - terminator: LambdaTerminator - ) -> EventLoopFuture { - logger.debug("initializing lambda") - // 1. create the handler from the factory - // 2. report initialization error if one occurred - let context = LambdaInitializationContext( - logger: logger, - eventLoop: self.eventLoop, - allocator: self.allocator, - terminator: terminator - ) - - return handlerProvider(context) - // Hopping back to "our" EventLoop is important in case the factory returns a future - // that originated from a foreign EventLoop/EventLoopGroup. - // This can happen if the factory uses a library (let's say a database client) that manages its own threads/loops - // for whatever reason and returns a future that originated from that foreign EventLoop. - .hop(to: self.eventLoop) - .peekError { error in - self.runtimeClient.reportInitializationError(logger: logger, error: error).peekError { reportingError in - // We're going to bail out because the init failed, so there's not a lot we can do other than log - // that we couldn't report this error back to the runtime. - logger.error("failed reporting initialization error to lambda runtime engine: \(reportingError)") - } - } - } - - func run(handler: some LambdaRuntimeHandler, logger: Logger) -> EventLoopFuture { - logger.debug("lambda invocation sequence starting") - // 1. request invocation from lambda runtime engine - self.isGettingNextInvocation = true - return self.runtimeClient.getNextInvocation(logger: logger).peekError { error in - logger.debug("could not fetch work from lambda runtime engine: \(error)") - }.flatMap { invocation, bytes in - // 2. send invocation to handler - self.isGettingNextInvocation = false - let context = LambdaContext( - logger: logger, - eventLoop: self.eventLoop, - allocator: self.allocator, - invocation: invocation - ) - logger.debug("sending invocation to lambda handler") - return handler.handle(bytes, context: context) - // Hopping back to "our" EventLoop is important in case the handler returns a future that - // originated from a foreign EventLoop/EventLoopGroup. - // This can happen if the handler uses a library (lets say a DB client) that manages its own threads/loops - // for whatever reason and returns a future that originated from that foreign EventLoop. - .hop(to: self.eventLoop) - .mapResult { result in - if case .failure(let error) = result { - logger.warning("lambda handler returned an error: \(error)") - } - return (invocation, result) - } - }.flatMap { invocation, result in - // 3. report results to runtime engine - self.runtimeClient.reportResults(logger: logger, invocation: invocation, result: result).peekError { error in - logger.error("could not report results to lambda runtime engine: \(error)") - } - } - } - - /// cancels the current run, if we are waiting for next invocation (long poll from Lambda control plane) - /// only needed for debugging purposes. - func cancelWaitingForNextInvocation() { - if self.isGettingNextInvocation { - self.runtimeClient.cancel() - } - } -} - -extension LambdaContext { - init(logger: Logger, eventLoop: EventLoop, allocator: ByteBufferAllocator, invocation: Invocation) { - self.init(requestID: invocation.requestID, - traceID: invocation.traceID, - invokedFunctionARN: invocation.invokedFunctionARN, - deadline: DispatchWallTime(millisSinceEpoch: invocation.deadlineInMillisSinceEpoch), - cognitoIdentity: invocation.cognitoIdentity, - clientContext: invocation.clientContext, - logger: logger, - eventLoop: eventLoop, - allocator: allocator) - } -} - -// TODO: move to nio? -extension EventLoopFuture { - // callback does not have side effects, failing with original result - func peekError(_ callback: @escaping (Error) -> Void) -> EventLoopFuture { - self.flatMapError { error in - callback(error) - return self - } - } - - // callback does not have side effects, failing with original result - func peekError(_ callback: @escaping (Error) -> EventLoopFuture) -> EventLoopFuture { - self.flatMapError { error in - let promise = self.eventLoop.makePromise(of: Value.self) - callback(error).whenComplete { _ in - promise.completeWith(self) - } - return promise.futureResult - } - } - - func mapResult(_ callback: @escaping (Result) -> NewValue) -> EventLoopFuture { - self.map { value in - callback(.success(value)) - }.flatMapErrorThrowing { error in - callback(.failure(error)) - } - } -} - -extension Result { - private var successful: Bool { - switch self { - case .success: - return true - case .failure: - return false - } - } -} - -/// This is safe since lambda runtime synchronizes by dispatching all methods to a single `EventLoop` -extension LambdaRunner: @unchecked Sendable {} diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift deleted file mode 100644 index c570a0b3..00000000 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift +++ /dev/null @@ -1,347 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -import Logging -import NIOConcurrencyHelpers -import NIOCore - -/// `LambdaRuntime` manages the Lambda process lifecycle. -/// -/// Use this API, if you build a higher level web framework which shall be able to run inside the Lambda environment. -public final class LambdaRuntime { - private let eventLoop: EventLoop - private let shutdownPromise: EventLoopPromise - private let logger: Logger - private let configuration: LambdaConfiguration - - private let handlerProvider: (LambdaInitializationContext) -> EventLoopFuture - - private var state = State.idle { - willSet { - self.eventLoop.assertInEventLoop() - precondition(newValue.order > self.state.order, "invalid state \(newValue) after \(self.state.order)") - } - } - - /// Create a new `LambdaRuntime`. - /// - /// - parameters: - /// - handlerProvider: A provider of the ``Handler`` the `LambdaRuntime` will manage. - /// - eventLoop: An `EventLoop` to run the Lambda on. - /// - logger: A `Logger` to log the Lambda events. - @usableFromInline - convenience init( - handlerProvider: @escaping (LambdaInitializationContext) -> EventLoopFuture, - eventLoop: EventLoop, - logger: Logger - ) { - self.init( - handlerProvider: handlerProvider, - eventLoop: eventLoop, - logger: logger, - configuration: .init() - ) - } - - /// Create a new `LambdaRuntime`. - /// - /// - parameters: - /// - handlerProvider: A provider of the ``Handler`` the `LambdaRuntime` will manage. - /// - eventLoop: An `EventLoop` to run the Lambda on. - /// - logger: A `Logger` to log the Lambda events. - init( - handlerProvider: @escaping (LambdaInitializationContext) -> EventLoopFuture, - eventLoop: EventLoop, - logger: Logger, - configuration: LambdaConfiguration - ) { - self.eventLoop = eventLoop - self.shutdownPromise = eventLoop.makePromise(of: Int.self) - self.logger = logger - self.configuration = configuration - - self.handlerProvider = handlerProvider - } - - deinit { - guard case .shutdown = self.state else { - preconditionFailure("invalid state \(self.state)") - } - } - - /// The `Lifecycle` shutdown future. - /// - /// - Returns: An `EventLoopFuture` that is fulfilled after the Lambda lifecycle has fully shutdown. - public var shutdownFuture: EventLoopFuture { - self.shutdownPromise.futureResult - } - - /// Start the `LambdaRuntime`. - /// - /// - Returns: An `EventLoopFuture` that is fulfilled after the Lambda hander has been created and initialized, and a first run has been scheduled. - public func start() -> EventLoopFuture { - if self.eventLoop.inEventLoop { - return self._start() - } else { - return self.eventLoop.flatSubmit { self._start() } - } - } - - private func _start() -> EventLoopFuture { - // This method must be called on the `EventLoop` the `LambdaRuntime` has been initialized with. - self.eventLoop.assertInEventLoop() - - logger.info("lambda runtime starting with \(self.configuration)") - self.state = .initializing - - var logger = self.logger - logger[metadataKey: "lifecycleId"] = .string(self.configuration.lifecycle.id) - let terminator = LambdaTerminator() - let runner = LambdaRunner(eventLoop: self.eventLoop, configuration: self.configuration) - - let startupFuture = runner.initialize(handlerProvider: self.handlerProvider, logger: logger, terminator: terminator) - startupFuture.flatMap { handler -> EventLoopFuture> in - // after the startup future has succeeded, we have a handler that we can use - // to `run` the lambda. - let finishedPromise = self.eventLoop.makePromise(of: Int.self) - self.state = .active(runner, handler) - self.run(promise: finishedPromise) - return finishedPromise.futureResult.mapResult { $0 } - }.flatMap { runnerResult -> EventLoopFuture in - // after the lambda finishPromise has succeeded or failed we need to - // shutdown the handler - terminator.terminate(eventLoop: self.eventLoop).flatMapErrorThrowing { error in - // if, we had an error shutting down the handler, we want to concatenate it with - // the runner result - logger.error("Error shutting down handler: \(error)") - throw LambdaRuntimeError.shutdownError(shutdownError: error, runnerResult: runnerResult) - }.flatMapResult { _ -> Result in - // we had no error shutting down the lambda. let's return the runner's result - runnerResult - } - }.always { _ in - // triggered when the Lambda has finished its last run or has a startup failure. - self.markShutdown() - }.cascade(to: self.shutdownPromise) - - return startupFuture.map { _ in } - } - - // MARK: - Private - - /// Begin the `LambdaRuntime` shutdown. - public func shutdown() { - // make this method thread safe by dispatching onto the eventloop - self.eventLoop.execute { - let oldState = self.state - self.state = .shuttingdown - if case .active(let runner, _) = oldState { - runner.cancelWaitingForNextInvocation() - } - } - } - - private func markShutdown() { - self.state = .shutdown - } - - @inline(__always) - private func run(promise: EventLoopPromise) { - func _run(_ count: Int) { - switch self.state { - case .active(let runner, let handler): - if self.configuration.lifecycle.maxTimes > 0, count >= self.configuration.lifecycle.maxTimes { - return promise.succeed(count) - } - var logger = self.logger - logger[metadataKey: "lifecycleIteration"] = "\(count)" - runner.run(handler: handler, logger: logger).whenComplete { result in - switch result { - case .success: - logger.log(level: .debug, "lambda invocation sequence completed successfully") - // recursive! per aws lambda runtime spec the polling requests are to be done one at a time - _run(count + 1) - case .failure(HTTPClient.Errors.cancelled): - if case .shuttingdown = self.state { - // if we ware shutting down, we expect to that the get next - // invocation request might have been cancelled. For this reason we - // succeed the promise here. - logger.log(level: .info, "lambda invocation sequence has been cancelled for shutdown") - return promise.succeed(count) - } - logger.log(level: .error, "lambda invocation sequence has been cancelled unexpectedly") - promise.fail(HTTPClient.Errors.cancelled) - case .failure(let error): - logger.log(level: .error, "lambda invocation sequence completed with error: \(error)") - promise.fail(error) - } - } - case .shuttingdown: - promise.succeed(count) - default: - preconditionFailure("invalid run state: \(self.state)") - } - } - - _run(0) - } - - private enum State { - case idle - case initializing - case active(LambdaRunner, any LambdaRuntimeHandler) - case shuttingdown - case shutdown - - internal var order: Int { - switch self { - case .idle: - return 0 - case .initializing: - return 1 - case .active: - return 2 - case .shuttingdown: - return 3 - case .shutdown: - return 4 - } - } - } -} - -public enum LambdaRuntimeFactory { - /// Create a new `LambdaRuntime`. - /// - /// - parameters: - /// - handlerType: The ``SimpleLambdaHandler`` type the `LambdaRuntime` shall create and manage. - /// - eventLoop: An `EventLoop` to run the Lambda on. - /// - logger: A `Logger` to log the Lambda events. - @inlinable - public static func makeRuntime( - _ handlerType: Handler.Type, - eventLoop: any EventLoop, - logger: Logger - ) -> LambdaRuntime { - LambdaRuntime>( - handlerProvider: CodableSimpleLambdaHandler.makeHandler(context:), - eventLoop: eventLoop, - logger: logger - ) - } - - /// Create a new `LambdaRuntime`. - /// - /// - parameters: - /// - handlerType: The ``LambdaHandler`` type the `LambdaRuntime` shall create and manage. - /// - eventLoop: An `EventLoop` to run the Lambda on. - /// - logger: A `Logger` to log the Lambda events. - @inlinable - public static func makeRuntime( - _ handlerType: Handler.Type, - eventLoop: any EventLoop, - logger: Logger - ) -> LambdaRuntime { - LambdaRuntime>( - handlerProvider: CodableLambdaHandler.makeHandler(context:), - eventLoop: eventLoop, - logger: logger - ) - } - - /// Create a new `LambdaRuntime`. - /// - /// - parameters: - /// - handlerType: The ``EventLoopLambdaHandler`` type the `LambdaRuntime` shall create and manage. - /// - eventLoop: An `EventLoop` to run the Lambda on. - /// - logger: A `Logger` to log the Lambda events. - @inlinable - public static func makeRuntime( - _ handlerType: Handler.Type, - eventLoop: any EventLoop, - logger: Logger - ) -> LambdaRuntime { - LambdaRuntime>( - handlerProvider: CodableEventLoopLambdaHandler.makeHandler(context:), - eventLoop: eventLoop, - logger: logger - ) - } - - /// Create a new `LambdaRuntime`. - /// - /// - parameters: - /// - handlerType: The ``ByteBufferLambdaHandler`` type the `LambdaRuntime` shall create and manage. - /// - eventLoop: An `EventLoop` to run the Lambda on. - /// - logger: A `Logger` to log the Lambda events. - @inlinable - public static func makeRuntime( - _ handlerType: Handler.Type, - eventLoop: any EventLoop, - logger: Logger - ) -> LambdaRuntime { - LambdaRuntime( - handlerProvider: Handler.makeHandler(context:), - eventLoop: eventLoop, - logger: logger - ) - } - - /// Create a new `LambdaRuntime`. - /// - /// - parameters: - /// - handlerProvider: A provider of the ``Handler`` the `LambdaRuntime` will manage. - /// - eventLoop: An `EventLoop` to run the Lambda on. - /// - logger: A `Logger` to log the Lambda events. - @inlinable - public static func makeRuntime( - handlerProvider: @escaping (LambdaInitializationContext) -> EventLoopFuture, - eventLoop: any EventLoop, - logger: Logger - ) -> LambdaRuntime { - LambdaRuntime( - handlerProvider: handlerProvider, - eventLoop: eventLoop, - logger: logger - ) - } - - /// Create a new `LambdaRuntime`. - /// - /// - parameters: - /// - handlerProvider: A provider of the ``Handler`` the `LambdaRuntime` will manage. - /// - eventLoop: An `EventLoop` to run the Lambda on. - /// - logger: A `Logger` to log the Lambda events. - @inlinable - public static func makeRuntime( - handlerProvider: @escaping (LambdaInitializationContext) async throws -> Handler, - eventLoop: any EventLoop, - logger: Logger - ) -> LambdaRuntime { - LambdaRuntime( - handlerProvider: { context in - let promise = eventLoop.makePromise(of: Handler.self) - promise.completeWithTask { - try await handlerProvider(context) - } - return promise.futureResult - }, - eventLoop: eventLoop, - logger: logger - ) - } -} - -/// This is safe since lambda runtime synchronizes by dispatching all methods to a single `EventLoop` -extension LambdaRuntime: @unchecked Sendable {} diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift deleted file mode 100644 index bcc65736..00000000 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift +++ /dev/null @@ -1,144 +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 Logging -import NIOCore -import NIOHTTP1 - -/// An HTTP based client for AWS Runtime Engine. This encapsulates the RESTful methods exposed by the Runtime Engine: -/// * /runtime/invocation/next -/// * /runtime/invocation/response -/// * /runtime/invocation/error -/// * /runtime/init/error -internal struct LambdaRuntimeClient { - private let eventLoop: EventLoop - private let allocator = ByteBufferAllocator() - private let httpClient: HTTPClient - - init(eventLoop: EventLoop, configuration: LambdaConfiguration.RuntimeEngine) { - self.eventLoop = eventLoop - self.httpClient = HTTPClient(eventLoop: eventLoop, configuration: configuration) - } - - /// Requests invocation from the control plane. - func getNextInvocation(logger: Logger) -> EventLoopFuture<(Invocation, ByteBuffer)> { - let url = Consts.invocationURLPrefix + Consts.getNextInvocationURLSuffix - logger.debug("requesting work from lambda runtime engine using \(url)") - return self.httpClient.get(url: url, headers: LambdaRuntimeClient.defaultHeaders).flatMapThrowing { response in - guard response.status == .ok else { - throw LambdaRuntimeError.badStatusCode(response.status) - } - let invocation = try Invocation(headers: response.headers) - guard let event = response.body else { - throw LambdaRuntimeError.noBody - } - return (invocation, event) - }.flatMapErrorThrowing { error in - switch error { - case HTTPClient.Errors.timeout: - throw LambdaRuntimeError.upstreamError("timeout") - case HTTPClient.Errors.connectionResetByPeer: - throw LambdaRuntimeError.upstreamError("connectionResetByPeer") - default: - throw error - } - } - } - - /// Reports a result to the Runtime Engine. - func reportResults(logger: Logger, invocation: Invocation, result: Result) -> EventLoopFuture { - var url = Consts.invocationURLPrefix + "/" + invocation.requestID - var body: ByteBuffer? - let headers: HTTPHeaders - - switch result { - case .success(let buffer): - url += Consts.postResponseURLSuffix - body = buffer - headers = LambdaRuntimeClient.defaultHeaders - case .failure(let error): - url += Consts.postErrorURLSuffix - let errorResponse = ErrorResponse(errorType: Consts.functionError, errorMessage: "\(error)") - let bytes = errorResponse.toJSONBytes() - body = self.allocator.buffer(capacity: bytes.count) - body!.writeBytes(bytes) - headers = LambdaRuntimeClient.errorHeaders - } - logger.debug("reporting results to lambda runtime engine using \(url)") - return self.httpClient.post(url: url, headers: headers, body: body).flatMapThrowing { response in - guard response.status == .accepted else { - throw LambdaRuntimeError.badStatusCode(response.status) - } - return () - }.flatMapErrorThrowing { error in - switch error { - case HTTPClient.Errors.timeout: - throw LambdaRuntimeError.upstreamError("timeout") - case HTTPClient.Errors.connectionResetByPeer: - throw LambdaRuntimeError.upstreamError("connectionResetByPeer") - default: - throw error - } - } - } - - /// Reports an initialization error to the Runtime Engine. - func reportInitializationError(logger: Logger, error: Error) -> EventLoopFuture { - let url = Consts.postInitErrorURL - let errorResponse = ErrorResponse(errorType: Consts.initializationError, errorMessage: "\(error)") - let bytes = errorResponse.toJSONBytes() - var body = self.allocator.buffer(capacity: bytes.count) - body.writeBytes(bytes) - logger.warning("reporting initialization error to lambda runtime engine using \(url)") - return self.httpClient.post(url: url, headers: LambdaRuntimeClient.errorHeaders, body: body).flatMapThrowing { response in - guard response.status == .accepted else { - throw LambdaRuntimeError.badStatusCode(response.status) - } - return () - }.flatMapErrorThrowing { error in - switch error { - case HTTPClient.Errors.timeout: - throw LambdaRuntimeError.upstreamError("timeout") - case HTTPClient.Errors.connectionResetByPeer: - throw LambdaRuntimeError.upstreamError("connectionResetByPeer") - default: - throw error - } - } - } - - /// Cancels the current request, if one is running. Only needed for debugging purposes - func cancel() { - self.httpClient.cancel() - } -} - -internal enum LambdaRuntimeError: Error { - case badStatusCode(HTTPResponseStatus) - case upstreamError(String) - case invocationMissingHeader(String) - case noBody - case json(Error) - case shutdownError(shutdownError: Error, runnerResult: Result) -} - -extension LambdaRuntimeClient { - internal static let defaultHeaders = HTTPHeaders([("user-agent", "Swift-Lambda/Unknown")]) - - /// These headers must be sent along an invocation or initialization error report - internal static let errorHeaders = HTTPHeaders([ - ("user-agent", "Swift-Lambda/Unknown"), - ("lambda-runtime-function-error-type", "Unhandled"), - ]) -} diff --git a/Sources/AWSLambdaRuntimeCore/Terminator.swift b/Sources/AWSLambdaRuntimeCore/Terminator.swift deleted file mode 100644 index cba8fd99..00000000 --- a/Sources/AWSLambdaRuntimeCore/Terminator.swift +++ /dev/null @@ -1,144 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 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 -// -//===----------------------------------------------------------------------===// - -import NIOConcurrencyHelpers -import NIOCore - -/// Lambda terminator. -/// Utility to manage the lambda shutdown sequence. -public final class LambdaTerminator { - fileprivate typealias Handler = (EventLoop) -> EventLoopFuture - - private var storage: Storage - - init() { - self.storage = Storage() - } - - /// Register a shutdown handler with the terminator. - /// - /// - parameters: - /// - name: Display name for logging purposes. - /// - handler: The shutdown handler to call when terminating the Lambda. - /// Shutdown handlers are called in the reverse order of being registered. - /// - /// - Returns: A ``RegistrationKey`` that can be used to de-register the handler when its no longer needed. - @discardableResult - public func register(name: String, handler: @escaping (EventLoop) -> EventLoopFuture) -> RegistrationKey { - let key = RegistrationKey() - self.storage.add(key: key, name: name, handler: handler) - return key - } - - /// De-register a shutdown handler with the terminator. - /// - /// - parameters: - /// - key: A ``RegistrationKey`` obtained from calling the register API. - public func deregister(_ key: RegistrationKey) { - self.storage.remove(key) - } - - /// Begin the termination cycle. - /// Shutdown handlers are called in the reverse order of being registered. - /// - /// - parameters: - /// - eventLoop: The `EventLoop` to run the termination on. - /// - /// - Returns: An `EventLoopFuture` with the result of the termination cycle. - internal func terminate(eventLoop: EventLoop) -> EventLoopFuture { - func terminate(_ iterator: IndexingIterator<[(name: String, handler: Handler)]>, errors: [Error], promise: EventLoopPromise) { - var iterator = iterator - guard let handler = iterator.next()?.handler else { - if errors.isEmpty { - return promise.succeed(()) - } else { - return promise.fail(TerminationError(underlying: errors)) - } - } - handler(eventLoop).whenComplete { result in - var errors = errors - if case .failure(let error) = result { - errors.append(error) - } - return terminate(iterator, errors: errors, promise: promise) - } - } - - // terminate in cascading, reverse order - let promise = eventLoop.makePromise(of: Void.self) - terminate(self.storage.handlers.reversed().makeIterator(), errors: [], promise: promise) - return promise.futureResult - } -} - -extension LambdaTerminator { - /// Lambda terminator registration key. - public struct RegistrationKey: Hashable, CustomStringConvertible { - var value: String - - init() { - // UUID basically - self.value = LambdaRequestID().uuidString - } - - public var description: String { - self.value - } - } -} - -extension LambdaTerminator { - fileprivate final class Storage { - private let lock: NIOLock - private var index: [RegistrationKey] - private var map: [RegistrationKey: (name: String, handler: Handler)] - - init() { - self.lock = .init() - self.index = [] - self.map = [:] - } - - func add(key: RegistrationKey, name: String, handler: @escaping Handler) { - self.lock.withLock { - self.index.append(key) - self.map[key] = (name: name, handler: handler) - } - } - - func remove(_ key: RegistrationKey) { - self.lock.withLock { - self.index = self.index.filter { $0 != key } - self.map[key] = nil - } - } - - var handlers: [(name: String, handler: Handler)] { - self.lock.withLock { - self.index.compactMap { self.map[$0] } - } - } - } -} - -extension LambdaTerminator { - struct TerminationError: Error { - let underlying: [Error] - } -} - -// Ideally this would not be @unchecked Sendable, but Sendable checks do not understand locks -// We can transition this to an actor once we drop support for older Swift versions -extension LambdaTerminator: @unchecked Sendable {} -extension LambdaTerminator.Storage: @unchecked Sendable {} diff --git a/Sources/AWSLambdaTesting/Lambda+Testing.swift b/Sources/AWSLambdaTesting/Lambda+Testing.swift deleted file mode 100644 index 9f77e8ac..00000000 --- a/Sources/AWSLambdaTesting/Lambda+Testing.swift +++ /dev/null @@ -1,126 +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 -// -//===----------------------------------------------------------------------===// - -// This functionality is designed to help with Lambda unit testing with XCTest -// For example: -// -// func test() { -// struct MyLambda: LambdaHandler { -// typealias Event = String -// typealias Output = String -// -// init(context: Lambda.InitializationContext) {} -// -// func handle(_ event: String, context: LambdaContext) async throws -> String { -// "echo" + event -// } -// } -// -// let input = UUID().uuidString -// var result: String? -// XCTAssertNoThrow(result = try Lambda.test(MyLambda.self, with: input)) -// XCTAssertEqual(result, "echo" + input) -// } - -@testable import AWSLambdaRuntime -@testable import AWSLambdaRuntimeCore -import Dispatch -import Logging -import NIOCore -import NIOPosix - -extension Lambda { - public struct TestConfig { - public var requestID: String - public var traceID: String - public var invokedFunctionARN: String - public var timeout: DispatchTimeInterval - - public init(requestID: String = "\(DispatchTime.now().uptimeNanoseconds)", - traceID: String = "Root=\(DispatchTime.now().uptimeNanoseconds);Parent=\(DispatchTime.now().uptimeNanoseconds);Sampled=1", - invokedFunctionARN: String = "arn:aws:lambda:us-west-1:\(DispatchTime.now().uptimeNanoseconds):function:custom-runtime", - timeout: DispatchTimeInterval = .seconds(5)) { - self.requestID = requestID - self.traceID = traceID - self.invokedFunctionARN = invokedFunctionARN - self.timeout = timeout - } - } - - public static func test( - _ handlerType: Handler.Type, - with event: Handler.Event, - using config: TestConfig = .init() - ) async throws -> Handler.Output { - let context = Self.makeContext(config: config) - let handler = Handler() - return try await handler.handle(event, context: context.1) - } - - public static func test( - _ handlerType: Handler.Type, - with event: Handler.Event, - using config: TestConfig = .init() - ) async throws -> Handler.Output { - let context = Self.makeContext(config: config) - let handler = try await Handler(context: context.0) - return try await handler.handle(event, context: context.1) - } - - public static func test( - _ handlerType: Handler.Type, - with event: Handler.Event, - using config: TestConfig = .init() - ) async throws -> Handler.Output { - let context = Self.makeContext(config: config) - let handler = try await Handler.makeHandler(context: context.0).get() - return try await handler.handle(event, context: context.1).get() - } - - public static func test( - _ handlerType: Handler.Type, - with buffer: ByteBuffer, - using config: TestConfig = .init() - ) async throws -> ByteBuffer? { - let context = Self.makeContext(config: config) - let handler = try await Handler.makeHandler(context: context.0).get() - return try await handler.handle(buffer, context: context.1).get() - } - - private static func makeContext(config: TestConfig) -> (LambdaInitializationContext, LambdaContext) { - let logger = Logger(label: "test") - - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! eventLoopGroup.syncShutdownGracefully() - } - let eventLoop = eventLoopGroup.next() - - let initContext = LambdaInitializationContext.__forTestsOnly( - logger: logger, - eventLoop: eventLoop - ) - - let context = LambdaContext.__forTestsOnly( - requestID: config.requestID, - traceID: config.traceID, - invokedFunctionARN: config.invokedFunctionARN, - timeout: config.timeout, - logger: logger, - eventLoop: eventLoop - ) - - return (initContext, context) - } -} diff --git a/Sources/MockServer/MockHTTPServer.swift b/Sources/MockServer/MockHTTPServer.swift new file mode 100644 index 00000000..ad69b216 --- /dev/null +++ b/Sources/MockServer/MockHTTPServer.swift @@ -0,0 +1,292 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +@available(LambdaSwift 2.0, *) +@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 invocations 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)", + ] + ) + + // 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 { + + group.addTask { + await self.handleConnection(channel: connectionChannel, maxInvocations: self.maxInvocations) + logger.trace("Done handling connection") + } + + // This mock server only accepts one connection + // the Lambda Function Runtime will send multiple requests on that single connection + // This Mock Server closes the connection when MAX_INVOCATION is reached + break + } + } + } + // it's ok to keep this at `info` level because it is only used for local testing and unit tests + 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, + maxInvocations: Int + ) async { + + var requestHead: HTTPRequestHead! + var requestBody: ByteBuffer? + + // each Lambda invocation results in TWO HTTP requests (GET /next and POST /response) + let maxRequests = maxInvocations * 2 + let requestCount = SharedCounter(maxValue: maxRequests) + + // 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 request + // 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": "\(maxRequests)"] + ) + 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 = .accepted + } 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 9b995bd5..00000000 --- a/Sources/MockServer/main.swift +++ /dev/null @@ -1,166 +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 Foundation -import NIOCore -import NIOHTTP1 -import NIOPosix - -internal 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() - } -} - -internal 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)") - } - } - } -} - -internal enum ServerError: Error { - case notReady - case cantBind -} - -internal 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" -} - -internal 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/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift deleted file mode 100644 index ac6c0838..00000000 --- a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift +++ /dev/null @@ -1,179 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2021 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 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaRuntimeCore -import NIOCore -import NIOEmbedded -import NIOHTTP1 -import XCTest - -final class ControlPlaneRequestEncoderTests: XCTestCase { - let host = "192.168.0.1" - - var client: EmbeddedChannel! - var server: EmbeddedChannel! - - override func setUp() { - self.client = EmbeddedChannel(handler: ControlPlaneRequestEncoderHandler(host: self.host)) - self.server = EmbeddedChannel(handlers: [ - ByteToMessageHandler(HTTPRequestDecoder(leftOverBytesStrategy: .dropBytes)), - NIOHTTPServerRequestAggregator(maxContentLength: 1024 * 1024), - ]) - } - - override func tearDown() { - XCTAssertNoThrow(try self.client.finish(acceptAlreadyClosed: false)) - XCTAssertNoThrow(try self.server.finish(acceptAlreadyClosed: false)) - self.client = nil - self.server = nil - } - - func testNextRequest() { - var request: NIOHTTPServerRequestFull? - XCTAssertNoThrow(request = try self.sendRequest(.next)) - - XCTAssertEqual(request?.head.isKeepAlive, true) - XCTAssertEqual(request?.head.method, .GET) - 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"]) - - XCTAssertNil(try self.server.readInbound(as: NIOHTTPServerRequestFull.self)) - } - - func testPostInvocationSuccessWithoutBody() { - let requestID = UUID().uuidString - var request: NIOHTTPServerRequestFull? - XCTAssertNoThrow(request = try self.sendRequest(.invocationResponse(requestID, nil))) - - XCTAssertEqual(request?.head.isKeepAlive, true) - XCTAssertEqual(request?.head.method, .POST) - 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["content-length"], ["0"]) - - XCTAssertNil(try self.server.readInbound(as: NIOHTTPServerRequestFull.self)) - } - - func testPostInvocationSuccessWithBody() { - let requestID = UUID().uuidString - let payload = ByteBuffer(string: "hello swift lambda!") - - var request: NIOHTTPServerRequestFull? - XCTAssertNoThrow(request = try self.sendRequest(.invocationResponse(requestID, payload))) - - XCTAssertEqual(request?.head.isKeepAlive, true) - XCTAssertEqual(request?.head.method, .POST) - 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["content-length"], ["\(payload.readableBytes)"]) - XCTAssertEqual(request?.body, payload) - - XCTAssertNil(try self.server.readInbound(as: NIOHTTPServerRequestFull.self)) - } - - func testPostInvocationErrorWithBody() { - let requestID = UUID().uuidString - let error = ErrorResponse(errorType: "SomeError", errorMessage: "An error happened") - var request: NIOHTTPServerRequestFull? - XCTAssertNoThrow(request = try self.sendRequest(.invocationError(requestID, error))) - - XCTAssertEqual(request?.head.isKeepAlive, true) - XCTAssertEqual(request?.head.method, .POST) - 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["lambda-runtime-function-error-type"], ["Unhandled"]) - let expectedBody = #"{"errorType":"SomeError","errorMessage":"An error happened"}"# - - XCTAssertEqual(request?.head.headers["content-length"], ["\(expectedBody.utf8.count)"]) - XCTAssertEqual(try request?.body?.getString(at: 0, length: XCTUnwrap(request?.body?.readableBytes)), - expectedBody) - - XCTAssertNil(try self.server.readInbound(as: NIOHTTPServerRequestFull.self)) - } - - func testPostStartupError() { - let error = ErrorResponse(errorType: "StartupError", errorMessage: "Urgh! Startup failed. 😨") - var request: NIOHTTPServerRequestFull? - XCTAssertNoThrow(request = try self.sendRequest(.initializationError(error))) - - XCTAssertEqual(request?.head.isKeepAlive, true) - XCTAssertEqual(request?.head.method, .POST) - 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["lambda-runtime-function-error-type"], ["Unhandled"]) - let expectedBody = #"{"errorType":"StartupError","errorMessage":"Urgh! Startup failed. 😨"}"# - XCTAssertEqual(request?.head.headers["content-length"], ["\(expectedBody.utf8.count)"]) - XCTAssertEqual(try request?.body?.getString(at: 0, length: XCTUnwrap(request?.body?.readableBytes)), - expectedBody) - - XCTAssertNil(try self.server.readInbound(as: NIOHTTPServerRequestFull.self)) - } - - func testMultipleNextAndResponseSuccessRequests() { - for _ in 0 ..< 1000 { - var nextRequest: NIOHTTPServerRequestFull? - XCTAssertNoThrow(nextRequest = try self.sendRequest(.next)) - XCTAssertEqual(nextRequest?.head.method, .GET) - XCTAssertEqual(nextRequest?.head.uri, "/2018-06-01/runtime/invocation/next") - - let requestID = UUID().uuidString - let payload = ByteBuffer(string: "hello swift lambda!") - var successRequest: NIOHTTPServerRequestFull? - XCTAssertNoThrow(successRequest = try self.sendRequest(.invocationResponse(requestID, payload))) - XCTAssertEqual(successRequest?.head.method, .POST) - XCTAssertEqual(successRequest?.head.uri, "/2018-06-01/runtime/invocation/\(requestID)/response") - } - } - - func sendRequest(_ request: ControlPlaneRequest) throws -> NIOHTTPServerRequestFull? { - try self.client.writeOutbound(request) - while let part = try self.client.readOutbound(as: ByteBuffer.self) { - XCTAssertNoThrow(try self.server.writeInbound(part)) - } - return try self.server.readInbound(as: NIOHTTPServerRequestFull.self) - } -} - -private final class ControlPlaneRequestEncoderHandler: ChannelOutboundHandler { - typealias OutboundIn = ControlPlaneRequest - typealias OutboundOut = ByteBuffer - - private var encoder: ControlPlaneRequestEncoder - - init(host: String) { - self.encoder = ControlPlaneRequestEncoder(host: host) - } - - func handlerAdded(context: ChannelHandlerContext) { - self.encoder.writerAdded(context: context) - } - - func handlerRemoved(context: ChannelHandlerContext) { - self.encoder.writerRemoved(context: context) - } - - func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { - self.encoder.writeRequest(self.unwrapOutboundIn(data), context: context, promise: promise) - } -} diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlerTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlerTest.swift deleted file mode 100644 index ac4b2a65..00000000 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlerTest.swift +++ /dev/null @@ -1,294 +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 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaRuntimeCore -import NIOCore -import XCTest - -class LambdaHandlerTest: XCTestCase { - // MARK: - SimpleLambdaHandler - - func testBootstrapSimpleNoInit() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct TestBootstrapHandler: SimpleLambdaHandler { - func handle(_ event: String, context: LambdaContext) async throws -> String { - event - } - } - - let maxTimes = Int.random(in: 10 ... 20) - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: TestBootstrapHandler.self) - assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) - } - - func testBootstrapSimpleInit() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct TestBootstrapHandler: SimpleLambdaHandler { - var initialized = false - - init() { - XCTAssertFalse(self.initialized) - self.initialized = true - } - - func handle(_ event: String, context: LambdaContext) async throws -> String { - event - } - } - - let maxTimes = Int.random(in: 10 ... 20) - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: TestBootstrapHandler.self) - assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) - } - - // MARK: - LambdaHandler - - func testBootstrapSuccess() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct TestBootstrapHandler: LambdaHandler { - var initialized = false - - init(context: LambdaInitializationContext) async throws { - XCTAssertFalse(self.initialized) - try await Task.sleep(nanoseconds: 100 * 1000 * 1000) // 0.1 seconds - self.initialized = true - } - - func handle(_ event: String, context: LambdaContext) async throws -> String { - event - } - } - - let maxTimes = Int.random(in: 10 ... 20) - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: TestBootstrapHandler.self) - assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) - } - - func testBootstrapFailure() { - let server = MockLambdaServer(behavior: FailedBootstrapBehavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct TestBootstrapHandler: LambdaHandler { - var initialized = false - - init(context: LambdaInitializationContext) async throws { - XCTAssertFalse(self.initialized) - try await Task.sleep(nanoseconds: 100 * 1000 * 1000) // 0.1 seconds - throw TestError("kaboom") - } - - func handle(_ event: String, context: LambdaContext) async throws { - XCTFail("How can this be called if init failed") - } - } - - let maxTimes = Int.random(in: 10 ... 20) - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: TestBootstrapHandler.self) - assertLambdaRuntimeResult(result, shouldFailWithError: TestError("kaboom")) - } - - func testHandlerSuccess() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: SimpleLambdaHandler { - func handle(_ event: String, context: LambdaContext) async throws -> String { - event - } - } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: Handler.self) - assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) - } - - func testVoidHandlerSuccess() { - let server = MockLambdaServer(behavior: Behavior(result: .success(nil))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: SimpleLambdaHandler { - func handle(_ event: String, context: LambdaContext) async throws {} - } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - - let result = Lambda.run(configuration: configuration, handlerType: Handler.self) - assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) - } - - func testHandlerFailure() { - let server = MockLambdaServer(behavior: Behavior(result: .failure(TestError("boom")))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: SimpleLambdaHandler { - func handle(_ event: String, context: LambdaContext) async throws -> String { - throw TestError("boom") - } - } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: Handler.self) - assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) - } - - // MARK: - EventLoopLambdaHandler - - func testEventLoopSuccess() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: EventLoopLambdaHandler { - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture(Handler()) - } - - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture(event) - } - } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: Handler.self) - assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) - } - - func testVoidEventLoopSuccess() { - let server = MockLambdaServer(behavior: Behavior(result: .success(nil))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: EventLoopLambdaHandler { - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture(Handler()) - } - - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture(()) - } - } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: Handler.self) - assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) - } - - func testEventLoopFailure() { - let server = MockLambdaServer(behavior: Behavior(result: .failure(TestError("boom")))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: EventLoopLambdaHandler { - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture(Handler()) - } - - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { - context.eventLoop.makeFailedFuture(TestError("boom")) - } - } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: Handler.self) - assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) - } - - func testEventLoopBootstrapFailure() { - let server = MockLambdaServer(behavior: FailedBootstrapBehavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: EventLoopLambdaHandler { - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { - context.eventLoop.makeFailedFuture(TestError("kaboom")) - } - - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { - XCTFail("Must never be called") - return context.eventLoop.makeFailedFuture(TestError("boom")) - } - } - - let result = Lambda.run(configuration: .init(), handlerType: Handler.self) - assertLambdaRuntimeResult(result, shouldFailWithError: TestError("kaboom")) - } -} - -private struct Behavior: LambdaServerBehavior { - let requestId: String - let event: String - let result: Result - - init(requestId: String = UUID().uuidString, event: String = "hello", result: Result = .success("hello")) { - self.requestId = requestId - self.event = event - self.result = result - } - - func getInvocation() -> GetInvocationResult { - .success((requestId: self.requestId, event: self.event)) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTAssertEqual(self.requestId, requestId, "expecting requestId to match") - switch self.result { - case .success(let expected): - XCTAssertEqual(expected, response, "expecting response to match") - return .success(()) - case .failure: - XCTFail("unexpected to fail, but succeeded with: \(response ?? "undefined")") - return .failure(.internalServerError) - } - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTAssertEqual(self.requestId, requestId, "expecting requestId to match") - switch self.result { - case .success: - XCTFail("unexpected to succeed, but failed with: \(error)") - return .failure(.internalServerError) - case .failure(let expected): - XCTAssertEqual(expected.description, error.errorMessage, "expecting error to match") - return .success(()) - } - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } -} diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlers.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlers.swift deleted file mode 100644 index dd371238..00000000 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlers.swift +++ /dev/null @@ -1,52 +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 AWSLambdaRuntimeCore -import NIOCore -import XCTest - -struct EchoHandler: EventLoopLambdaHandler { - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture(EchoHandler()) - } - - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture(event) - } -} - -struct StartupError: Error {} - -struct StartupErrorHandler: EventLoopLambdaHandler { - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { - context.eventLoop.makeFailedFuture(StartupError()) - } - - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { - XCTFail("Must never be called") - return context.eventLoop.makeSucceededFuture(event) - } -} - -struct RuntimeError: Error {} - -struct RuntimeErrorHandler: EventLoopLambdaHandler { - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture(RuntimeErrorHandler()) - } - - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { - context.eventLoop.makeFailedFuture(RuntimeError()) - } -} diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRunnerTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaRunnerTest.swift deleted file mode 100644 index 6fd91aec..00000000 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaRunnerTest.swift +++ /dev/null @@ -1,196 +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 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaRuntimeCore -import NIOCore -import XCTest - -class LambdaRunnerTest: XCTestCase { - func testSuccess() { - struct Behavior: LambdaServerBehavior { - let requestId = UUID().uuidString - let event = "hello" - func getInvocation() -> GetInvocationResult { - .success((self.requestId, self.event)) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTAssertEqual(self.requestId, requestId, "expecting requestId to match") - XCTAssertEqual(self.event, response, "expecting response to match") - return .success(()) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } - } - XCTAssertNoThrow(try runLambda(behavior: Behavior(), handlerType: EchoHandler.self)) - } - - func testFailure() { - struct Behavior: LambdaServerBehavior { - let requestId = UUID().uuidString - func getInvocation() -> GetInvocationResult { - .success((requestId: self.requestId, event: "hello")) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should report error") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTAssertEqual(self.requestId, requestId, "expecting requestId to match") - XCTAssertEqual(String(describing: RuntimeError()), error.errorMessage, "expecting error to match") - return .success(()) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } - } - XCTAssertNoThrow(try runLambda(behavior: Behavior(), handlerType: RuntimeErrorHandler.self)) - } - - func testCustomProviderSuccess() { - struct Behavior: LambdaServerBehavior { - let requestId = UUID().uuidString - let event = "hello" - func getInvocation() -> GetInvocationResult { - .success((self.requestId, self.event)) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTAssertEqual(self.requestId, requestId, "expecting requestId to match") - XCTAssertEqual(self.event, response, "expecting response to match") - return .success(()) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } - } - XCTAssertNoThrow(try runLambda(behavior: Behavior(), handlerProvider: { context in - context.eventLoop.makeSucceededFuture(EchoHandler()) - })) - } - - func testCustomProviderFailure() { - struct Behavior: LambdaServerBehavior { - let requestId = UUID().uuidString - let event = "hello" - func getInvocation() -> GetInvocationResult { - .success((self.requestId, self.event)) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should not report processing") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTAssertEqual(String(describing: CustomError()), error.errorMessage, "expecting error to match") - return .success(()) - } - } - - struct CustomError: Error {} - - XCTAssertThrowsError(try runLambda(behavior: Behavior(), handlerProvider: { context -> EventLoopFuture in - context.eventLoop.makeFailedFuture(CustomError()) - })) { error in - XCTAssertNotNil(error as? CustomError, "expecting error to match") - } - } - - func testCustomAsyncProviderSuccess() { - struct Behavior: LambdaServerBehavior { - let requestId = UUID().uuidString - let event = "hello" - func getInvocation() -> GetInvocationResult { - .success((self.requestId, self.event)) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTAssertEqual(self.requestId, requestId, "expecting requestId to match") - XCTAssertEqual(self.event, response, "expecting response to match") - return .success(()) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } - } - XCTAssertNoThrow(try runLambda(behavior: Behavior(), handlerProvider: { _ async throws -> EchoHandler in - EchoHandler() - })) - } - - func testCustomAsyncProviderFailure() { - struct Behavior: LambdaServerBehavior { - let requestId = UUID().uuidString - let event = "hello" - func getInvocation() -> GetInvocationResult { - .success((self.requestId, self.event)) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should not report processing") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTAssertEqual(String(describing: CustomError()), error.errorMessage, "expecting error to match") - return .success(()) - } - } - - struct CustomError: Error {} - - XCTAssertThrowsError(try runLambda(behavior: Behavior(), handlerProvider: { _ async throws -> EchoHandler in - throw CustomError() - })) { error in - XCTAssertNotNil(error as? CustomError, "expecting error to match") - } - } -} diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTest.swift deleted file mode 100644 index 83e18c2e..00000000 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTest.swift +++ /dev/null @@ -1,345 +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 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaRuntimeCore -import Logging -import NIOCore -import NIOFoundationCompat -import NIOHTTP1 -import NIOPosix -import NIOTestUtils -import XCTest - -class LambdaRuntimeClientTest: XCTestCase { - func testSuccess() { - let behavior = Behavior() - XCTAssertNoThrow(try runLambda(behavior: behavior, handlerType: EchoHandler.self)) - XCTAssertEqual(behavior.state, 6) - } - - func testFailure() { - let behavior = Behavior() - XCTAssertNoThrow(try runLambda(behavior: behavior, handlerType: RuntimeErrorHandler.self)) - XCTAssertEqual(behavior.state, 10) - } - - func testStartupFailure() { - let behavior = Behavior() - XCTAssertThrowsError(try runLambda(behavior: behavior, handlerType: StartupErrorHandler.self)) { - XCTAssert($0 is StartupError) - } - XCTAssertEqual(behavior.state, 1) - } - - func testGetInvocationServerInternalError() { - struct Behavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - .failure(.internalServerError) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should not report results") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } - } - XCTAssertThrowsError(try runLambda(behavior: Behavior(), handlerType: EchoHandler.self)) { - XCTAssertEqual($0 as? LambdaRuntimeError, .badStatusCode(.internalServerError)) - } - } - - func testGetInvocationServerNoBodyError() { - struct Behavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - .success(("1", "")) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should not report results") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } - } - XCTAssertThrowsError(try runLambda(behavior: Behavior(), handlerType: EchoHandler.self)) { - XCTAssertEqual($0 as? LambdaRuntimeError, .noBody) - } - } - - func testGetInvocationServerMissingHeaderRequestIDError() { - struct Behavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - // no request id -> no context - .success(("", "hello")) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should not report results") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } - } - XCTAssertThrowsError(try runLambda(behavior: Behavior(), handlerType: EchoHandler.self)) { - XCTAssertEqual($0 as? LambdaRuntimeError, .invocationMissingHeader(AmazonHeaders.requestID)) - } - } - - func testProcessResponseInternalServerError() { - struct Behavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - .success((requestId: "1", event: "event")) - } - - func processResponse(requestId: String, response: String?) -> Result { - .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } - } - XCTAssertThrowsError(try runLambda(behavior: Behavior(), handlerType: EchoHandler.self)) { - XCTAssertEqual($0 as? LambdaRuntimeError, .badStatusCode(.internalServerError)) - } - } - - func testProcessErrorInternalServerError() { - struct Behavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - .success((requestId: "1", event: "event")) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should not report results") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } - } - XCTAssertThrowsError(try runLambda(behavior: Behavior(), handlerType: RuntimeErrorHandler.self)) { - XCTAssertEqual($0 as? LambdaRuntimeError, .badStatusCode(.internalServerError)) - } - } - - func testProcessInitErrorOnBootstrapFailure() { - struct Behavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - XCTFail("should not get invocation") - return .failure(.internalServerError) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should not report results") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - .failure(.internalServerError) - } - } - XCTAssertThrowsError(try runLambda(behavior: Behavior(), handlerType: StartupErrorHandler.self)) { - XCTAssert($0 is StartupError) - } - } - - func testErrorResponseToJSON() { - // we want to check if quotes and back slashes are correctly escaped - let windowsError = ErrorResponse( - errorType: "error", - errorMessage: #"underlyingError: "An error with a windows path C:\Windows\""# - ) - let windowsBytes = windowsError.toJSONBytes() - XCTAssertEqual(#"{"errorType":"error","errorMessage":"underlyingError: \"An error with a windows path C:\\Windows\\\""}"#, String(decoding: windowsBytes, as: Unicode.UTF8.self)) - - // we want to check if unicode sequences work - let emojiError = ErrorResponse( - errorType: "error", - errorMessage: #"🥑👨‍👩‍👧‍👧👩‍👩‍👧‍👧👨‍👨‍👧"# - ) - let emojiBytes = emojiError.toJSONBytes() - XCTAssertEqual(#"{"errorType":"error","errorMessage":"🥑👨‍👩‍👧‍👧👩‍👩‍👧‍👧👨‍👨‍👧"}"#, String(decoding: emojiBytes, as: Unicode.UTF8.self)) - } - - func testInitializationErrorReport() { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - - let server = NIOHTTP1TestServer(group: eventLoopGroup) - defer { XCTAssertNoThrow(try server.stop()) } - - let logger = Logger(label: "TestLogger") - let client = LambdaRuntimeClient(eventLoop: eventLoopGroup.next(), configuration: .init(address: "127.0.0.1:\(server.serverPort)")) - let result = client.reportInitializationError(logger: logger, error: TestError("boom")) - - var inboundHeader: HTTPServerRequestPart? - XCTAssertNoThrow(inboundHeader = try server.readInbound()) - guard case .head(let head) = try? XCTUnwrap(inboundHeader) else { XCTFail("Expected to get a head first"); return } - XCTAssertEqual(head.headers["lambda-runtime-function-error-type"], ["Unhandled"]) - XCTAssertEqual(head.headers["user-agent"], ["Swift-Lambda/Unknown"]) - - var inboundBody: HTTPServerRequestPart? - XCTAssertNoThrow(inboundBody = try server.readInbound()) - guard case .body(let body) = try? XCTUnwrap(inboundBody) else { XCTFail("Expected body after head"); return } - XCTAssertEqual(try JSONDecoder().decode(ErrorResponse.self, from: body).errorMessage, "boom") - - XCTAssertEqual(try server.readInbound(), .end(nil)) - - XCTAssertNoThrow(try server.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .accepted)))) - XCTAssertNoThrow(try server.writeOutbound(.end(nil))) - XCTAssertNoThrow(try result.wait()) - } - - func testInvocationErrorReport() { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - - let server = NIOHTTP1TestServer(group: eventLoopGroup) - defer { XCTAssertNoThrow(try server.stop()) } - - let logger = Logger(label: "TestLogger") - let client = LambdaRuntimeClient(eventLoop: eventLoopGroup.next(), configuration: .init(address: "127.0.0.1:\(server.serverPort)")) - - let header = HTTPHeaders([ - (AmazonHeaders.requestID, "test"), - (AmazonHeaders.deadline, String(Date(timeIntervalSinceNow: 60).millisSinceEpoch)), - (AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime"), - (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), - ]) - var inv: Invocation? - XCTAssertNoThrow(inv = try Invocation(headers: header)) - guard let invocation = inv else { return } - - let result = client.reportResults(logger: logger, invocation: invocation, result: Result.failure(TestError("boom"))) - - var inboundHeader: HTTPServerRequestPart? - XCTAssertNoThrow(inboundHeader = try server.readInbound()) - guard case .head(let head) = try? XCTUnwrap(inboundHeader) else { XCTFail("Expected to get a head first"); return } - XCTAssertEqual(head.headers["lambda-runtime-function-error-type"], ["Unhandled"]) - XCTAssertEqual(head.headers["user-agent"], ["Swift-Lambda/Unknown"]) - - var inboundBody: HTTPServerRequestPart? - XCTAssertNoThrow(inboundBody = try server.readInbound()) - guard case .body(let body) = try? XCTUnwrap(inboundBody) else { XCTFail("Expected body after head"); return } - XCTAssertEqual(try JSONDecoder().decode(ErrorResponse.self, from: body).errorMessage, "boom") - - XCTAssertEqual(try server.readInbound(), .end(nil)) - - XCTAssertNoThrow(try server.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .accepted)))) - XCTAssertNoThrow(try server.writeOutbound(.end(nil))) - XCTAssertNoThrow(try result.wait()) - } - - func testInvocationSuccessResponse() { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - - let server = NIOHTTP1TestServer(group: eventLoopGroup) - defer { XCTAssertNoThrow(try server.stop()) } - - let logger = Logger(label: "TestLogger") - let client = LambdaRuntimeClient(eventLoop: eventLoopGroup.next(), configuration: .init(address: "127.0.0.1:\(server.serverPort)")) - - let header = HTTPHeaders([ - (AmazonHeaders.requestID, "test"), - (AmazonHeaders.deadline, String(Date(timeIntervalSinceNow: 60).millisSinceEpoch)), - (AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime"), - (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), - ]) - var inv: Invocation? - XCTAssertNoThrow(inv = try Invocation(headers: header)) - guard let invocation = inv else { return } - - let result = client.reportResults(logger: logger, invocation: invocation, result: Result.success(nil)) - - var inboundHeader: HTTPServerRequestPart? - XCTAssertNoThrow(inboundHeader = try server.readInbound()) - guard case .head(let head) = try? XCTUnwrap(inboundHeader) else { XCTFail("Expected to get a head first"); return } - XCTAssertFalse(head.headers.contains(name: "lambda-runtime-function-error-type")) - XCTAssertEqual(head.headers["user-agent"], ["Swift-Lambda/Unknown"]) - - XCTAssertEqual(try server.readInbound(), .end(nil)) - - XCTAssertNoThrow(try server.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .accepted)))) - XCTAssertNoThrow(try server.writeOutbound(.end(nil))) - XCTAssertNoThrow(try result.wait()) - } - - class Behavior: LambdaServerBehavior { - var state = 0 - - func processInitError(error: ErrorResponse) -> Result { - self.state += 1 - return .success(()) - } - - func getInvocation() -> GetInvocationResult { - self.state += 2 - return .success(("1", "hello")) - } - - func processResponse(requestId: String, response: String?) -> Result { - self.state += 4 - return .success(()) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - self.state += 8 - return .success(()) - } - } -} diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeTest.swift deleted file mode 100644 index 39764ccc..00000000 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeTest.swift +++ /dev/null @@ -1,138 +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 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaRuntimeCore -import Logging -import NIOCore -import NIOHTTP1 -import NIOPosix -import XCTest - -class LambdaRuntimeTest: XCTestCase { - func testShutdownFutureIsFulfilledWithStartUpError() { - let server = MockLambdaServer(behavior: FailedBootstrapBehavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - - let eventLoop = eventLoopGroup.next() - let logger = Logger(label: "TestLogger") - let runtime = LambdaRuntimeFactory.makeRuntime(StartupErrorHandler.self, eventLoop: eventLoop, logger: logger) - - // eventLoop.submit in this case returns an EventLoopFuture> - // which is why we need `wait().wait()` - XCTAssertThrowsError(try eventLoop.flatSubmit { runtime.start() }.wait()) { - XCTAssert($0 is StartupError) - } - - XCTAssertThrowsError(_ = try runtime.shutdownFuture.wait()) { - XCTAssert($0 is StartupError) - } - } - - func testShutdownIsCalledWhenLambdaShutsdown() { - let server = MockLambdaServer(behavior: BadBehavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - - let eventLoop = eventLoopGroup.next() - let logger = Logger(label: "TestLogger") - let runtime = LambdaRuntimeFactory.makeRuntime(EchoHandler.self, eventLoop: eventLoop, logger: logger) - - XCTAssertNoThrow(_ = try eventLoop.flatSubmit { runtime.start() }.wait()) - XCTAssertThrowsError(_ = try runtime.shutdownFuture.wait()) { - XCTAssertEqual(.badStatusCode(HTTPResponseStatus.internalServerError), $0 as? LambdaRuntimeError) - } - } - - func testLambdaResultIfShutsdownIsUnclean() { - let server = MockLambdaServer(behavior: BadBehavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - - struct ShutdownError: Error { - let description: String - } - - struct ShutdownErrorHandler: EventLoopLambdaHandler { - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { - // register shutdown operation - context.terminator.register(name: "test 1", handler: { eventLoop in - eventLoop.makeFailedFuture(ShutdownError(description: "error 1")) - }) - context.terminator.register(name: "test 2", handler: { eventLoop in - eventLoop.makeSucceededVoidFuture() - }) - context.terminator.register(name: "test 3", handler: { eventLoop in - eventLoop.makeFailedFuture(ShutdownError(description: "error 2")) - }) - context.terminator.register(name: "test 4", handler: { eventLoop in - eventLoop.makeSucceededVoidFuture() - }) - context.terminator.register(name: "test 5", handler: { eventLoop in - eventLoop.makeFailedFuture(ShutdownError(description: "error 3")) - }) - return context.eventLoop.makeSucceededFuture(ShutdownErrorHandler()) - } - - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { - context.eventLoop.makeSucceededVoidFuture() - } - } - - let eventLoop = eventLoopGroup.next() - let logger = Logger(label: "TestLogger") - let runtime = LambdaRuntimeFactory.makeRuntime(ShutdownErrorHandler.self, eventLoop: eventLoop, logger: logger) - - XCTAssertNoThrow(try eventLoop.flatSubmit { runtime.start() }.wait()) - XCTAssertThrowsError(try runtime.shutdownFuture.wait()) { error in - guard case LambdaRuntimeError.shutdownError(let shutdownError, .failure(let runtimeError)) = error else { - XCTFail("Unexpected error: \(error)"); return - } - - XCTAssertEqual(shutdownError as? LambdaTerminator.TerminationError, LambdaTerminator.TerminationError(underlying: [ - ShutdownError(description: "error 3"), - ShutdownError(description: "error 2"), - ShutdownError(description: "error 1"), - ])) - XCTAssertEqual(runtimeError as? LambdaRuntimeError, .badStatusCode(.internalServerError)) - } - } -} - -struct BadBehavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - .failure(.internalServerError) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should not report a response") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report an error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report an error") - return .failure(.internalServerError) - } -} diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift deleted file mode 100644 index ffb50953..00000000 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift +++ /dev/null @@ -1,363 +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 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaRuntimeCore -import Logging -import NIOCore -import NIOPosix -import XCTest - -class LambdaTest: XCTestCase { - func testSuccess() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let maxTimes = Int.random(in: 10 ... 20) - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self) - assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) - } - - func testFailure() { - let server = MockLambdaServer(behavior: Behavior(result: .failure(RuntimeError()))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let maxTimes = Int.random(in: 10 ... 20) - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: RuntimeErrorHandler.self) - assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) - } - - func testBootstrapFailure() { - let server = MockLambdaServer(behavior: FailedBootstrapBehavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let result = Lambda.run(configuration: .init(), handlerType: StartupErrorHandler.self) - assertLambdaRuntimeResult(result, shouldFailWithError: StartupError()) - } - - func testBootstrapFailureAndReportErrorFailure() { - struct Behavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - XCTFail("should not get invocation") - return .failure(.internalServerError) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should not report a response") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report an error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - .failure(.internalServerError) - } - } - - let server = MockLambdaServer(behavior: FailedBootstrapBehavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let result = Lambda.run(configuration: .init(), handlerType: StartupErrorHandler.self) - assertLambdaRuntimeResult(result, shouldFailWithError: StartupError()) - } - - func testStartStopInDebugMode() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let signal = Signal.ALRM - let maxTimes = 1000 - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes, stopSignal: signal)) - - DispatchQueue(label: "test").async { - // we need to schedule the signal before we start the long running `Lambda.run`, since - // `Lambda.run` will block the main thread. - usleep(100_000) - kill(getpid(), signal.rawValue) - } - let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self) - - switch result { - case .success(let invocationCount): - XCTAssertGreaterThan(invocationCount, 0, "should have stopped before any request made") - XCTAssertLessThan(invocationCount, maxTimes, "should have stopped before \(maxTimes)") - case .failure(let error): - XCTFail("Unexpected error: \(error)") - } - } - - func testTimeout() { - let timeout: Int64 = 100 - let server = MockLambdaServer(behavior: Behavior(requestId: "timeout", event: "\(timeout * 2)")) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: 1), - runtimeEngine: .init(requestTimeout: .milliseconds(timeout))) - let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self) - assertLambdaRuntimeResult(result, shouldFailWithError: LambdaRuntimeError.upstreamError("timeout")) - } - - func testDisconnect() { - let server = MockLambdaServer(behavior: Behavior(requestId: "disconnect")) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: 1)) - let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self) - assertLambdaRuntimeResult(result, shouldFailWithError: LambdaRuntimeError.upstreamError("connectionResetByPeer")) - } - - func testBigEvent() { - let event = String(repeating: "*", count: 104_448) - let server = MockLambdaServer(behavior: Behavior(event: event, result: .success(event))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: 1)) - let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self) - assertLambdaRuntimeResult(result, shouldHaveRun: 1) - } - - func testKeepAliveServer() { - let server = MockLambdaServer(behavior: Behavior(), keepAlive: true) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let maxTimes = 10 - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self) - assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) - } - - func testNoKeepAliveServer() { - let server = MockLambdaServer(behavior: Behavior(), keepAlive: false) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let maxTimes = 10 - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self) - assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) - } - - func testServerFailure() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Behavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - .failure(.internalServerError) - } - - func processResponse(requestId: String, response: String?) -> Result { - .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } - } - - let result = Lambda.run(configuration: .init(), handlerType: EchoHandler.self) - assertLambdaRuntimeResult(result, shouldFailWithError: LambdaRuntimeError.badStatusCode(.internalServerError)) - } - - func testDeadline() { - let delta = Int.random(in: 1 ... 600) - - let milli1 = Date(timeIntervalSinceNow: Double(delta)).millisSinceEpoch - let milli2 = (DispatchWallTime.now() + .seconds(delta)).millisSinceEpoch - XCTAssertEqual(Double(milli1), Double(milli2), accuracy: 2.0) - - let now1 = DispatchWallTime.now() - let now2 = DispatchWallTime(millisSinceEpoch: Date().millisSinceEpoch) - XCTAssertEqual(Double(now2.rawValue), Double(now1.rawValue), accuracy: 2_000_000.0) - - let future1 = DispatchWallTime.now() + .seconds(delta) - let future2 = DispatchWallTime(millisSinceEpoch: Date(timeIntervalSinceNow: Double(delta)).millisSinceEpoch) - XCTAssertEqual(Double(future1.rawValue), Double(future2.rawValue), accuracy: 2_000_000.0) - - let past1 = DispatchWallTime.now() - .seconds(delta) - let past2 = DispatchWallTime(millisSinceEpoch: Date(timeIntervalSinceNow: Double(-delta)).millisSinceEpoch) - XCTAssertEqual(Double(past1.rawValue), Double(past2.rawValue), accuracy: 2_000_000.0) - - let context = LambdaContext( - requestID: UUID().uuidString, - traceID: UUID().uuidString, - invokedFunctionARN: UUID().uuidString, - deadline: .now() + .seconds(1), - cognitoIdentity: nil, - clientContext: nil, - logger: Logger(label: "test"), - eventLoop: MultiThreadedEventLoopGroup(numberOfThreads: 1).next(), - allocator: ByteBufferAllocator() - ) - XCTAssertGreaterThan(context.deadline, .now()) - - let expiredContext = LambdaContext( - requestID: context.requestID, - traceID: context.traceID, - invokedFunctionARN: context.invokedFunctionARN, - deadline: .now() - .seconds(1), - cognitoIdentity: context.cognitoIdentity, - clientContext: context.clientContext, - logger: context.logger, - eventLoop: context.eventLoop, - allocator: context.allocator - ) - XCTAssertLessThan(expiredContext.deadline, .now()) - } - - func testGetRemainingTime() { - let context = LambdaContext( - requestID: UUID().uuidString, - traceID: UUID().uuidString, - invokedFunctionARN: UUID().uuidString, - deadline: .now() + .seconds(1), - cognitoIdentity: nil, - clientContext: nil, - logger: Logger(label: "test"), - eventLoop: MultiThreadedEventLoopGroup(numberOfThreads: 1).next(), - allocator: ByteBufferAllocator() - ) - XCTAssertLessThanOrEqual(context.getRemainingTime(), .seconds(1)) - XCTAssertGreaterThan(context.getRemainingTime(), .milliseconds(800)) - } - - #if compiler(>=5.6) - func testSendable() async throws { - struct Handler: EventLoopLambdaHandler { - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture(Handler()) - } - - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture("hello") - } - } - - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - - let server = try await MockLambdaServer(behavior: Behavior()).start().get() - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let logger = Logger(label: "TestLogger") - let configuration = LambdaConfiguration(runtimeEngine: .init(requestTimeout: .milliseconds(100))) - - let handler1 = Handler() - let task = Task.detached { - print(configuration.description) - logger.info("hello") - let runner = LambdaRunner(eventLoop: eventLoopGroup.next(), configuration: configuration) - - try await runner.run( - handler: CodableEventLoopLambdaHandler( - handler: handler1, - allocator: ByteBufferAllocator() - ), - logger: logger - ).get() - - try await runner.initialize(handlerType: CodableEventLoopLambdaHandler.self, logger: logger, terminator: LambdaTerminator()).flatMap { handler2 in - runner.run(handler: handler2, logger: logger) - }.get() - } - - try await task.value - } - #endif -} - -private struct Behavior: LambdaServerBehavior { - let requestId: String - let event: String - let result: Result - - init(requestId: String = UUID().uuidString, event: String = "hello", result: Result = .success("hello")) { - self.requestId = requestId - self.event = event - self.result = result - } - - func getInvocation() -> GetInvocationResult { - .success((requestId: self.requestId, event: self.event)) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTAssertEqual(self.requestId, requestId, "expecting requestId to match") - switch self.result { - case .success(let expected): - XCTAssertEqual(expected, response, "expecting response to match") - return .success(()) - case .failure: - XCTFail("unexpected to fail, but succeeded with: \(response ?? "undefined")") - return .failure(.internalServerError) - } - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTAssertEqual(self.requestId, requestId, "expecting requestId to match") - switch self.result { - case .success: - XCTFail("unexpected to succeed, but failed with: \(error)") - return .failure(.internalServerError) - case .failure(let expected): - XCTAssertEqual(String(describing: expected), error.errorMessage, "expecting error to match") - return .success(()) - } - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } -} - -struct FailedBootstrapBehavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - XCTFail("should not get invocation") - return .failure(.internalServerError) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should not report a response") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report an error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - .success(()) - } -} diff --git a/Tests/AWSLambdaRuntimeCoreTests/Utils.swift b/Tests/AWSLambdaRuntimeCoreTests/Utils.swift deleted file mode 100644 index 8e2d3c38..00000000 --- a/Tests/AWSLambdaRuntimeCoreTests/Utils.swift +++ /dev/null @@ -1,143 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2021 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 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaRuntimeCore -import Logging -import NIOCore -import NIOPosix -import XCTest - -func runLambda(behavior: LambdaServerBehavior, handlerType: Handler.Type) throws { - try runLambda(behavior: behavior, handlerProvider: CodableSimpleLambdaHandler.makeHandler(context:)) -} - -func runLambda(behavior: LambdaServerBehavior, handlerType: Handler.Type) throws { - try runLambda(behavior: behavior, handlerProvider: CodableLambdaHandler.makeHandler(context:)) -} - -func runLambda(behavior: LambdaServerBehavior, handlerType: Handler.Type) throws { - try runLambda(behavior: behavior, handlerProvider: CodableEventLoopLambdaHandler.makeHandler(context:)) -} - -func runLambda( - behavior: LambdaServerBehavior, - handlerProvider: @escaping (LambdaInitializationContext) -> EventLoopFuture -) throws { - try runLambda(behavior: behavior, handlerProvider: { context in - handlerProvider(context).map { - CodableEventLoopLambdaHandler(handler: $0, allocator: context.allocator) - } - }) -} - -func runLambda( - behavior: LambdaServerBehavior, - handlerProvider: @escaping (LambdaInitializationContext) async throws -> Handler -) throws { - try runLambda(behavior: behavior, handlerProvider: { context in - let handler = try await handlerProvider(context) - return CodableEventLoopLambdaHandler(handler: handler, allocator: context.allocator) - }) -} - -func runLambda( - behavior: LambdaServerBehavior, - handlerProvider: @escaping (LambdaInitializationContext) async throws -> Handler -) throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - try runLambda( - behavior: behavior, - handlerProvider: { context in - let promise = eventLoopGroup.next().makePromise(of: Handler.self) - promise.completeWithTask { - try await handlerProvider(context) - } - return promise.futureResult - } - ) -} - -func runLambda( - behavior: LambdaServerBehavior, - handlerProvider: @escaping (LambdaInitializationContext) -> EventLoopFuture -) throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - let logger = Logger(label: "TestLogger") - let configuration = LambdaConfiguration(runtimeEngine: .init(requestTimeout: .milliseconds(100))) - let terminator = LambdaTerminator() - let runner = LambdaRunner(eventLoop: eventLoopGroup.next(), configuration: configuration) - let server = try MockLambdaServer(behavior: behavior).start().wait() - defer { XCTAssertNoThrow(try server.stop().wait()) } - try runner.initialize(handlerProvider: handlerProvider, logger: logger, terminator: terminator).flatMap { handler in - runner.run(handler: handler, logger: logger) - }.wait() -} - -func assertLambdaRuntimeResult(_ result: Result, shouldHaveRun: Int = 0, shouldFailWithError: Error? = nil, file: StaticString = #file, line: UInt = #line) { - switch result { - case .success where shouldFailWithError != nil: - XCTFail("should fail with \(shouldFailWithError!)", file: file, line: line) - case .success(let count) where shouldFailWithError == nil: - XCTAssertEqual(shouldHaveRun, count, "should have run \(shouldHaveRun) times", file: file, line: line) - case .failure(let error) where shouldFailWithError == nil: - XCTFail("should succeed, but failed with \(error)", file: file, line: line) - case .failure(let error) where shouldFailWithError != nil: - XCTAssertEqual(String(describing: shouldFailWithError!), String(describing: error), "expected error to mactch", file: file, line: line) - default: - XCTFail("invalid state") - } -} - -struct TestError: Error, Equatable, CustomStringConvertible { - let description: String - - init(_ description: String) { - self.description = description - } -} - -extension Date { - internal var millisSinceEpoch: Int64 { - Int64(self.timeIntervalSince1970 * 1000) - } -} - -extension LambdaRuntimeError: Equatable { - public static func == (lhs: Self, rhs: Self) -> Bool { - // technically incorrect, but good enough for our tests - String(describing: lhs) == String(describing: rhs) - } -} - -extension LambdaTerminator.TerminationError: Equatable { - public static func == (lhs: Self, rhs: Self) -> Bool { - guard lhs.underlying.count == rhs.underlying.count else { - return false - } - // technically incorrect, but good enough for our tests - return String(describing: lhs) == String(describing: rhs) - } -} - -// for backward compatibility in tests -extension LambdaRunner { - func initialize( - handlerType: Handler.Type, - logger: Logger, - terminator: LambdaTerminator - ) -> EventLoopFuture { - self.initialize(handlerProvider: handlerType.makeHandler(context:), logger: logger, terminator: terminator) - } -} diff --git a/Tests/AWSLambdaRuntimeTests/CollectEverythingLogHandler.swift b/Tests/AWSLambdaRuntimeTests/CollectEverythingLogHandler.swift new file mode 100644 index 00000000..98004b6a --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/CollectEverythingLogHandler.swift @@ -0,0 +1,139 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +@available(LambdaSwift 2.0, *) +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/AWSLambdaRuntimeTests/ControlPlaneRequestEncoderTests.swift b/Tests/AWSLambdaRuntimeTests/ControlPlaneRequestEncoderTests.swift new file mode 100644 index 00000000..c050ae5e --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/ControlPlaneRequestEncoderTests.swift @@ -0,0 +1,229 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2021 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 +import NIOEmbedded +import NIOHTTP1 +import Testing + +@testable import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +struct ControlPlaneRequestEncoderTests { + let host = "192.168.0.1" + + @available(LambdaSwift 2.0, *) + func createChannels() -> (client: EmbeddedChannel, server: EmbeddedChannel) { + let client = EmbeddedChannel(handler: ControlPlaneRequestEncoderHandler(host: self.host)) + let server = EmbeddedChannel(handlers: [ + ByteToMessageHandler(HTTPRequestDecoder(leftOverBytesStrategy: .dropBytes)), + NIOHTTPServerRequestAggregator(maxContentLength: 1024 * 1024), + ]) + return (client, server) + } + + @Test + @available(LambdaSwift 2.0, *) + func testNextRequest() throws { + let (client, server) = createChannels() + defer { + _ = try? client.finish(acceptAlreadyClosed: false) + _ = try? server.finish(acceptAlreadyClosed: false) + } + + let request = try sendRequest(.next, client: client, server: server) + + #expect(request?.head.isKeepAlive == true) + #expect(request?.head.method == .GET) + #expect(request?.head.uri == "/2018-06-01/runtime/invocation/next") + #expect(request?.head.version == .http1_1) + #expect(request?.head.headers["host"] == [self.host]) + #expect(request?.head.headers["user-agent"] == [.userAgent]) + + #expect(try server.readInbound(as: NIOHTTPServerRequestFull.self) == nil) + } + + @Test + @available(LambdaSwift 2.0, *) + func testPostInvocationSuccessWithoutBody() throws { + let (client, server) = createChannels() + defer { + _ = try? client.finish(acceptAlreadyClosed: false) + _ = try? server.finish(acceptAlreadyClosed: false) + } + + let requestID = UUID().uuidString + let request = try sendRequest(.invocationResponse(requestID, nil), client: client, server: server) + + #expect(request?.head.isKeepAlive == true) + #expect(request?.head.method == .POST) + #expect(request?.head.uri == "/2018-06-01/runtime/invocation/\(requestID)/response") + #expect(request?.head.version == .http1_1) + #expect(request?.head.headers["host"] == [self.host]) + #expect(request?.head.headers["user-agent"] == [.userAgent]) + #expect(request?.head.headers["content-length"] == ["0"]) + + #expect(try server.readInbound(as: NIOHTTPServerRequestFull.self) == nil) + } + + @Test + @available(LambdaSwift 2.0, *) + func testPostInvocationSuccessWithBody() throws { + let (client, server) = createChannels() + defer { + _ = try? client.finish(acceptAlreadyClosed: false) + _ = try? server.finish(acceptAlreadyClosed: false) + } + + let requestID = UUID().uuidString + let payload = ByteBuffer(string: "hello swift lambda!") + + let request = try sendRequest(.invocationResponse(requestID, payload), client: client, server: server) + + #expect(request?.head.isKeepAlive == true) + #expect(request?.head.method == .POST) + #expect(request?.head.uri == "/2018-06-01/runtime/invocation/\(requestID)/response") + #expect(request?.head.version == .http1_1) + #expect(request?.head.headers["host"] == [self.host]) + #expect(request?.head.headers["user-agent"] == [.userAgent]) + #expect(request?.head.headers["content-length"] == ["\(payload.readableBytes)"]) + #expect(request?.body == payload) + + #expect(try server.readInbound(as: NIOHTTPServerRequestFull.self) == nil) + } + + @Test + @available(LambdaSwift 2.0, *) + func testPostInvocationErrorWithBody() throws { + let (client, server) = createChannels() + defer { + _ = try? client.finish(acceptAlreadyClosed: false) + _ = try? server.finish(acceptAlreadyClosed: false) + } + + let requestID = UUID().uuidString + let error = ErrorResponse(errorType: "SomeError", errorMessage: "An error happened") + let request = try sendRequest(.invocationError(requestID, error), client: client, server: server) + + #expect(request?.head.isKeepAlive == true) + #expect(request?.head.method == .POST) + #expect(request?.head.uri == "/2018-06-01/runtime/invocation/\(requestID)/error") + #expect(request?.head.version == .http1_1) + #expect(request?.head.headers["host"] == [self.host]) + #expect(request?.head.headers["user-agent"] == [.userAgent]) + #expect(request?.head.headers["lambda-runtime-function-error-type"] == ["Unhandled"]) + let expectedBody = #"{"errorType":"SomeError","errorMessage":"An error happened"}"# + + #expect(request?.head.headers["content-length"] == ["\(expectedBody.utf8.count)"]) + let bodyString = request?.body?.getString(at: 0, length: request?.body?.readableBytes ?? 0) + #expect(bodyString == expectedBody) + + #expect(try server.readInbound(as: NIOHTTPServerRequestFull.self) == nil) + } + + @Test + @available(LambdaSwift 2.0, *) + func testPostStartupError() throws { + let (client, server) = createChannels() + defer { + _ = try? client.finish(acceptAlreadyClosed: false) + _ = try? server.finish(acceptAlreadyClosed: false) + } + + let error = ErrorResponse(errorType: "StartupError", errorMessage: "Urgh! Startup failed. 😨") + let request = try sendRequest(.initializationError(error), client: client, server: server) + + #expect(request?.head.isKeepAlive == true) + #expect(request?.head.method == .POST) + #expect(request?.head.uri == "/2018-06-01/runtime/init/error") + #expect(request?.head.version == .http1_1) + #expect(request?.head.headers["host"] == [self.host]) + #expect(request?.head.headers["user-agent"] == [.userAgent]) + #expect(request?.head.headers["lambda-runtime-function-error-type"] == ["Unhandled"]) + let expectedBody = #"{"errorType":"StartupError","errorMessage":"Urgh! Startup failed. 😨"}"# + #expect(request?.head.headers["content-length"] == ["\(expectedBody.utf8.count)"]) + let bodyString = request?.body?.getString(at: 0, length: request?.body?.readableBytes ?? 0) + #expect(bodyString == expectedBody) + + #expect(try server.readInbound(as: NIOHTTPServerRequestFull.self) == nil) + } + + @Test + @available(LambdaSwift 2.0, *) + func testMultipleNextAndResponseSuccessRequests() throws { + let (client, server) = createChannels() + defer { + _ = try? client.finish(acceptAlreadyClosed: false) + _ = try? server.finish(acceptAlreadyClosed: false) + } + + for _ in 0..<1000 { + let nextRequest = try sendRequest(.next, client: client, server: server) + #expect(nextRequest?.head.method == .GET) + #expect(nextRequest?.head.uri == "/2018-06-01/runtime/invocation/next") + + let requestID = UUID().uuidString + let payload = ByteBuffer(string: "hello swift lambda!") + let successRequest = try sendRequest( + .invocationResponse(requestID, payload), + client: client, + server: server + ) + #expect(successRequest?.head.method == .POST) + #expect(successRequest?.head.uri == "/2018-06-01/runtime/invocation/\(requestID)/response") + } + } + + @available(LambdaSwift 2.0, *) + func sendRequest( + _ request: ControlPlaneRequest, + client: EmbeddedChannel, + server: EmbeddedChannel + ) throws -> NIOHTTPServerRequestFull? { + try client.writeOutbound(request) + while let part = try client.readOutbound(as: ByteBuffer.self) { + try server.writeInbound(part) + } + return try server.readInbound(as: NIOHTTPServerRequestFull.self) + } +} + +@available(LambdaSwift 2.0, *) +private final class ControlPlaneRequestEncoderHandler: ChannelOutboundHandler { + typealias OutboundIn = ControlPlaneRequest + typealias OutboundOut = ByteBuffer + + private var encoder: ControlPlaneRequestEncoder + + init(host: String) { + self.encoder = ControlPlaneRequestEncoder(host: host) + } + + func handlerAdded(context: ChannelHandlerContext) { + self.encoder.writerAdded(context: context) + } + + func handlerRemoved(context: ChannelHandlerContext) { + self.encoder.writerRemoved(context: context) + } + + func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { + self.encoder.writeRequest(self.unwrapOutboundIn(data), context: context, promise: promise) + } +} diff --git a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestTests.swift b/Tests/AWSLambdaRuntimeTests/InvocationTests.swift similarity index 65% rename from Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestTests.swift rename to Tests/AWSLambdaRuntimeTests/InvocationTests.swift index d55e0b67..6d1d7afa 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestTests.swift +++ b/Tests/AWSLambdaRuntimeTests/InvocationTests.swift @@ -12,11 +12,21 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntimeCore import NIOHTTP1 -import XCTest +import Testing -class InvocationTest: XCTestCase { +@testable import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@Suite +struct InvocationTest { + @Test + @available(LambdaSwift 2.0, *) func testInvocationTraceID() throws { let headers = HTTPHeaders([ (AmazonHeaders.requestID, "test"), @@ -24,14 +34,10 @@ class InvocationTest: XCTestCase { (AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime"), ]) - var invocation: Invocation? - - XCTAssertNoThrow(invocation = try Invocation(headers: headers)) - XCTAssertNotNil(invocation) + var maybeInvocation: InvocationMetadata? - guard !invocation!.traceID.isEmpty else { - XCTFail("Invocation traceID is empty") - return - } + #expect(throws: Never.self) { maybeInvocation = try InvocationMetadata(headers: headers) } + let invocation = try #require(maybeInvocation) + #expect(!invocation.traceID.isEmpty) } } diff --git a/Tests/AWSLambdaRuntimeTests/Lambda+CodableTest.swift b/Tests/AWSLambdaRuntimeTests/Lambda+CodableTest.swift deleted file mode 100644 index 43e50423..00000000 --- a/Tests/AWSLambdaRuntimeTests/Lambda+CodableTest.swift +++ /dev/null @@ -1,250 +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 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaRuntime -@testable import AWSLambdaRuntimeCore -import Logging -import NIOCore -import NIOFoundationCompat -import NIOPosix -import XCTest - -class CodableLambdaTest: XCTestCase { - var eventLoopGroup: EventLoopGroup! - let allocator = ByteBufferAllocator() - - override func setUp() { - self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - } - - override func tearDown() { - XCTAssertNoThrow(try self.eventLoopGroup.syncShutdownGracefully()) - } - - func testCodableVoidEventLoopFutureHandler() { - struct Handler: EventLoopLambdaHandler { - var expected: Request? - - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture(Handler()) - } - - func handle(_ event: Request, context: LambdaContext) -> EventLoopFuture { - XCTAssertEqual(event, self.expected) - return context.eventLoop.makeSucceededVoidFuture() - } - } - - let context = self.newContext() - let request = Request(requestId: UUID().uuidString) - - let handler = CodableEventLoopLambdaHandler( - handler: Handler(expected: request), - allocator: context.allocator - ) - - var inputBuffer = context.allocator.buffer(capacity: 1024) - XCTAssertNoThrow(try JSONEncoder().encode(request, into: &inputBuffer)) - var outputBuffer: ByteBuffer? - XCTAssertNoThrow(outputBuffer = try handler.handle(inputBuffer, context: context).wait()) - XCTAssertEqual(outputBuffer?.readableBytes, 0) - } - - func testCodableEventLoopFutureHandler() { - struct Handler: EventLoopLambdaHandler { - var expected: Request? - - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture(Handler()) - } - - func handle(_ event: Request, context: LambdaContext) -> EventLoopFuture { - XCTAssertEqual(event, self.expected) - return context.eventLoop.makeSucceededFuture(Response(requestId: event.requestId)) - } - } - - let context = self.newContext() - let request = Request(requestId: UUID().uuidString) - var response: Response? - - let handler = CodableEventLoopLambdaHandler( - handler: Handler(expected: request), - allocator: context.allocator - ) - - var inputBuffer = context.allocator.buffer(capacity: 1024) - XCTAssertNoThrow(try JSONEncoder().encode(request, into: &inputBuffer)) - var outputBuffer: ByteBuffer? - XCTAssertNoThrow(outputBuffer = try handler.handle(inputBuffer, context: context).wait()) - XCTAssertNoThrow(response = try JSONDecoder().decode(Response.self, from: XCTUnwrap(outputBuffer))) - XCTAssertEqual(response?.requestId, request.requestId) - } - - func testCodableVoidHandler() async throws { - struct Handler: LambdaHandler { - init(context: AWSLambdaRuntimeCore.LambdaInitializationContext) async throws {} - - var expected: Request? - - func handle(_ event: Request, context: LambdaContext) async throws { - XCTAssertEqual(event, self.expected) - } - } - - let context = self.newContext() - let request = Request(requestId: UUID().uuidString) - - var underlying = try await Handler(context: self.newInitContext()) - underlying.expected = request - let handler = CodableLambdaHandler( - handler: underlying, - allocator: context.allocator - ) - - var inputBuffer = context.allocator.buffer(capacity: 1024) - XCTAssertNoThrow(try JSONEncoder().encode(request, into: &inputBuffer)) - var outputBuffer: ByteBuffer? - XCTAssertNoThrow(outputBuffer = try handler.handle(inputBuffer, context: context).wait()) - XCTAssertEqual(outputBuffer?.readableBytes, 0) - } - - func testCodableHandler() async throws { - struct Handler: LambdaHandler { - init(context: AWSLambdaRuntimeCore.LambdaInitializationContext) async throws {} - - var expected: Request? - - func handle(_ event: Request, context: LambdaContext) async throws -> Response { - XCTAssertEqual(event, self.expected) - return Response(requestId: event.requestId) - } - } - - let context = self.newContext() - let request = Request(requestId: UUID().uuidString) - var response: Response? - - var underlying = try await Handler(context: self.newInitContext()) - underlying.expected = request - let handler = CodableLambdaHandler( - handler: underlying, - allocator: context.allocator - ) - - var inputBuffer = context.allocator.buffer(capacity: 1024) - XCTAssertNoThrow(try JSONEncoder().encode(request, into: &inputBuffer)) - - var outputBuffer: ByteBuffer? - XCTAssertNoThrow(outputBuffer = try handler.handle(inputBuffer, context: context).wait()) - XCTAssertNoThrow(response = try JSONDecoder().decode(Response.self, from: XCTUnwrap(outputBuffer))) - XCTAssertNoThrow(try handler.handle(inputBuffer, context: context).wait()) - XCTAssertEqual(response?.requestId, request.requestId) - } - - func testCodableVoidSimpleHandler() async throws { - struct Handler: SimpleLambdaHandler { - var expected: Request? - - func handle(_ event: Request, context: LambdaContext) async throws { - XCTAssertEqual(event, self.expected) - } - } - - let context = self.newContext() - let request = Request(requestId: UUID().uuidString) - - var underlying = Handler() - underlying.expected = request - let handler = CodableSimpleLambdaHandler( - handler: underlying, - allocator: context.allocator - ) - - var inputBuffer = context.allocator.buffer(capacity: 1024) - XCTAssertNoThrow(try JSONEncoder().encode(request, into: &inputBuffer)) - var outputBuffer: ByteBuffer? - XCTAssertNoThrow(outputBuffer = try handler.handle(inputBuffer, context: context).wait()) - XCTAssertEqual(outputBuffer?.readableBytes, 0) - } - - func testCodableSimpleHandler() async throws { - struct Handler: SimpleLambdaHandler { - var expected: Request? - - func handle(_ event: Request, context: LambdaContext) async throws -> Response { - XCTAssertEqual(event, self.expected) - return Response(requestId: event.requestId) - } - } - - let context = self.newContext() - let request = Request(requestId: UUID().uuidString) - var response: Response? - - var underlying = Handler() - underlying.expected = request - let handler = CodableSimpleLambdaHandler( - handler: underlying, - allocator: context.allocator - ) - - var inputBuffer = context.allocator.buffer(capacity: 1024) - XCTAssertNoThrow(try JSONEncoder().encode(request, into: &inputBuffer)) - - var outputBuffer: ByteBuffer? - XCTAssertNoThrow(outputBuffer = try handler.handle(inputBuffer, context: context).wait()) - XCTAssertNoThrow(response = try JSONDecoder().decode(Response.self, from: XCTUnwrap(outputBuffer))) - XCTAssertNoThrow(try handler.handle(inputBuffer, context: context).wait()) - XCTAssertEqual(response?.requestId, request.requestId) - } - - // convenience method - func newContext() -> LambdaContext { - LambdaContext( - requestID: UUID().uuidString, - traceID: "abc123", - invokedFunctionARN: "aws:arn:", - deadline: .now() + .seconds(3), - cognitoIdentity: nil, - clientContext: nil, - logger: Logger(label: "test"), - eventLoop: self.eventLoopGroup.next(), - allocator: ByteBufferAllocator() - ) - } - - func newInitContext() -> LambdaInitializationContext { - LambdaInitializationContext( - logger: Logger(label: "test"), - eventLoop: self.eventLoopGroup.next(), - allocator: ByteBufferAllocator(), - terminator: LambdaTerminator() - ) - } -} - -private struct Request: Codable, Equatable { - let requestId: String - init(requestId: String) { - self.requestId = requestId - } -} - -private struct Response: Codable, Equatable { - let requestId: String - init(requestId: String) { - self.requestId = requestId - } -} diff --git a/Tests/AWSLambdaRuntimeTests/Lambda+CodableTests.swift b/Tests/AWSLambdaRuntimeTests/Lambda+CodableTests.swift new file mode 100644 index 00000000..2027616b --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/Lambda+CodableTests.swift @@ -0,0 +1,101 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime +import Logging +import NIOCore +import Testing + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@Suite +struct JSONTests { + + let logger = Logger(label: "JSONTests") + + struct Foo: Codable { + var bar: String + } + + @Test + func testEncodingConformance() { + let encoder = LambdaJSONOutputEncoder(JSONEncoder()) + let foo = Foo(bar: "baz") + var byteBuffer = ByteBuffer() + + #expect(throws: Never.self) { + try encoder.encode(foo, into: &byteBuffer) + } + + #expect(byteBuffer == ByteBuffer(string: #"{"bar":"baz"}"#)) + } + + @Test + @available(LambdaSwift 2.0, *) + func testJSONHandlerWithOutput() async { + let jsonEncoder = JSONEncoder() + let jsonDecoder = JSONDecoder() + + let closureHandler = ClosureHandler { (foo: Foo, context) in + foo + } + + var handler = LambdaCodableAdapter( + encoder: jsonEncoder, + decoder: jsonDecoder, + handler: LambdaHandlerAdapter(handler: closureHandler) + ) + + let event = ByteBuffer(string: #"{"bar":"baz"}"#) + let writer = MockLambdaWriter() + let context = LambdaContext.__forTestsOnly( + requestID: UUID().uuidString, + traceID: UUID().uuidString, + invokedFunctionARN: "arn:", + timeout: .milliseconds(6000), + logger: self.logger + ) + + await #expect(throws: Never.self) { + try await handler.handle(event, responseWriter: writer, context: context) + } + + let result = await writer.output + #expect(result == ByteBuffer(string: #"{"bar":"baz"}"#)) + } + + final actor MockLambdaWriter: LambdaResponseStreamWriter { + private var _buffer: ByteBuffer? + + var output: ByteBuffer? { + self._buffer + } + + func writeAndFinish(_ buffer: ByteBuffer) async throws { + self._buffer = buffer + } + + func write(_ buffer: ByteBuffer, hasCustomHeaders: Bool = false) async throws { + fatalError("Unexpected call") + } + + func finish() async throws { + fatalError("Unexpected call") + } + } +} diff --git a/Tests/AWSLambdaRuntimeTests/LambdaClockTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaClockTests.swift new file mode 100644 index 00000000..affd323a --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/LambdaClockTests.swift @@ -0,0 +1,164 @@ +//===----------------------------------------------------------------------===// +// +// 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 Testing + +@testable import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@Suite("LambdaClock Tests") +struct LambdaClockTests { + + @Test("Clock provides current time") + @available(LambdaSwift 2.0, *) + func clockProvidesCurrentTime() { + let clock = LambdaClock() + let now = clock.now + + // Verify we get a reasonable timestamp (after today) + let dateOfWritingThisTestInMillis: Int64 = 1_754_130_134_000 + #expect(now.instant > dateOfWritingThisTestInMillis) + } + + @Test("Instant can be advanced by duration") + @available(LambdaSwift 2.0, *) + func instantCanBeAdvancedByDuration() { + let clock = LambdaClock() + let start = clock.now + let advanced = start.advanced(by: .seconds(30)) + + #expect(advanced.instant == start.instant + 30_000) + } + + @Test("Duration calculation between instants") + @available(LambdaSwift 2.0, *) + func durationCalculationBetweenInstants() { + let clock = LambdaClock() + let start = clock.now + let end = start.advanced(by: .seconds(5)) + + let duration = start.duration(to: end) + #expect(duration == .seconds(5)) + } + + @Test("Instant comparison works correctly") + @available(LambdaSwift 2.0, *) + func instantComparisonWorksCorrectly() { + let clock = LambdaClock() + let earlier = clock.now + let later = earlier.advanced(by: .milliseconds(1)) + + #expect(earlier < later) + #expect(!(later < earlier)) + } + + @Test("Clock minimum resolution is milliseconds") + @available(LambdaSwift 2.0, *) + func clockMinimumResolutionIsMilliseconds() { + let clock = LambdaClock() + #expect(clock.minimumResolution == .milliseconds(1)) + } + + @Test("Sleep until deadline works") + @available(LambdaSwift 2.0, *) + func sleepUntilDeadlineWorks() async throws { + let clock = LambdaClock() + let start = clock.now + let deadline = start.advanced(by: .milliseconds(50)) + + try await clock.sleep(until: deadline, tolerance: nil) + + let end = clock.now + let elapsed = start.duration(to: end) + + // Allow some tolerance for timing precision + #expect(elapsed >= .milliseconds(40)) + #expect(elapsed <= .milliseconds(100)) + } + + @Test("Sleep with past deadline returns immediately") + @available(LambdaSwift 2.0, *) + func sleepWithPastDeadlineReturnsImmediately() async throws { + let clock = LambdaClock() + let now = clock.now + let pastDeadline = now.advanced(by: .milliseconds(-100)) + + let start = clock.now + try await clock.sleep(until: pastDeadline, tolerance: nil) + let end = clock.now + + let elapsed = start.duration(to: end) + // Should return almost immediately + #expect(elapsed < .milliseconds(10)) + } + + @Test("Duration to future instant returns negative duration") + @available(LambdaSwift 2.0, *) + func durationToFutureInstantReturnsNegativeDuration() { + let clock = LambdaClock() + let futureDeadline = clock.now.advanced(by: .seconds(30)) + let currentTime = clock.now + + // This simulates getRemainingTime() where deadline is in future + let remainingTime = futureDeadline.duration(to: currentTime) + + // Should be negative since we're going from future to present + #expect(remainingTime < .zero) + #expect(remainingTime <= .seconds(-29)) // Allow some timing tolerance + } + + @Test("LambdaClock now matches Foundation Date within tolerance") + @available(LambdaSwift 2.0, *) + func lambdaClockNowMatchesFoundationDate() { + + let clock = LambdaClock() + + // Get timestamps as close together as possible + let lambdaClockNow = clock.now + let foundationDate = Date() + + // Convert Foundation Date to milliseconds since epoch + let foundationMillis = Int64(foundationDate.timeIntervalSince1970 * 1000) + let lambdaClockMillis = lambdaClockNow.millisecondsSinceEpoch() + + // Allow small tolerance for timing differences between calls + let difference = abs(foundationMillis - lambdaClockMillis) + + #expect( + difference <= 10, + "LambdaClock and Foundation Date should be within 10ms of each other, difference was \(difference)ms" + ) + } + @Test("Instant renders as string with an epoch number") + @available(LambdaSwift 2.0, *) + func instantRendersAsStringWithEpochNumber() { + let clock = LambdaClock() + let instant = clock.now + + let expectedString = "\(instant)" + #expect(expectedString.allSatisfy { $0.isNumber }, "String should only contain numbers") + + if let expectedNumber = Int64(expectedString) { + let newInstant = LambdaClock.Instant(millisecondsSinceEpoch: expectedNumber) + #expect(instant == newInstant, "Instant should match the expected number") + } else { + Issue.record("expectedString is not a number") + } + } +} diff --git a/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift new file mode 100644 index 00000000..7a53c1ef --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift @@ -0,0 +1,137 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import Foundation +import Logging +import Testing + +@testable import AWSLambdaRuntime + +@Suite("LambdaContext ClientContext Tests") +struct LambdaContextTests { + + @Test("ClientContext with full data resolves correctly") + func clientContextWithFullDataResolves() throws { + let custom = ["key": "value"] + let environment = ["key": "value"] + let clientContext = ClientContext( + client: ClientApplication( + installationID: "test-id", + appTitle: "test-app", + appVersionName: "1.0", + appVersionCode: "100", + appPackageName: "com.test.app" + ), + custom: custom, + environment: environment + ) + + let encoder = JSONEncoder() + let clientContextData = try encoder.encode(clientContext) + + // Verify JSON encoding/decoding works correctly + let decoder = JSONDecoder() + let decodedClientContext = try decoder.decode(ClientContext.self, from: clientContextData) + + let decodedClient = try #require(decodedClientContext.client) + let originalClient = try #require(clientContext.client) + + #expect(decodedClient.installationID == originalClient.installationID) + #expect(decodedClient.appTitle == originalClient.appTitle) + #expect(decodedClient.appVersionName == originalClient.appVersionName) + #expect(decodedClient.appVersionCode == originalClient.appVersionCode) + #expect(decodedClient.appPackageName == originalClient.appPackageName) + #expect(decodedClientContext.custom == clientContext.custom) + #expect(decodedClientContext.environment == clientContext.environment) + } + + @Test("ClientContext with empty data resolves correctly") + func clientContextWithEmptyDataResolves() throws { + let emptyClientContextJSON = "{}" + let emptyClientContextData = emptyClientContextJSON.data(using: .utf8)! + + let decoder = JSONDecoder() + let decodedClientContext = try decoder.decode(ClientContext.self, from: emptyClientContextData) + + // With empty JSON, we expect nil values for optional fields + #expect(decodedClientContext.client == nil) + #expect(decodedClientContext.custom == nil) + #expect(decodedClientContext.environment == nil) + } + + @Test("ClientContext with AWS Lambda JSON payload decodes correctly") + func clientContextWithAWSLambdaJSONPayload() throws { + let jsonPayload = """ + { + "client": { + "installation_id": "example-id", + "app_title": "Example App", + "app_version_name": "1.0", + "app_version_code": "1", + "app_package_name": "com.example.app" + }, + "custom": { + "customKey": "customValue" + }, + "env": { + "platform": "Android", + "platform_version": "10" + } + } + """ + + let jsonData = jsonPayload.data(using: .utf8)! + let decoder = JSONDecoder() + let decodedClientContext = try decoder.decode(ClientContext.self, from: jsonData) + + // Verify client application data + let client = try #require(decodedClientContext.client) + #expect(client.installationID == "example-id") + #expect(client.appTitle == "Example App") + #expect(client.appVersionName == "1.0") + #expect(client.appVersionCode == "1") + #expect(client.appPackageName == "com.example.app") + + // Verify custom properties + let custom = try #require(decodedClientContext.custom) + #expect(custom["customKey"] == "customValue") + + // Verify environment settings + let environment = try #require(decodedClientContext.environment) + #expect(environment["platform"] == "Android") + #expect(environment["platform_version"] == "10") + } + + @Test("getRemainingTime returns positive duration for future deadline") + @available(LambdaSwift 2.0, *) + func getRemainingTimeReturnsPositiveDurationForFutureDeadline() { + + // Create context with deadline 30 seconds in the future + let context = LambdaContext.__forTestsOnly( + requestID: "test-request", + traceID: "test-trace", + invokedFunctionARN: "test-arn", + timeout: .seconds(30), + logger: Logger(label: "test") + ) + + // Get remaining time - should be positive since deadline is in future + let remainingTime = context.getRemainingTime() + + // Verify Duration can be negative (not absolute value) + #expect(remainingTime > .zero, "getRemainingTime() should return positive duration when deadline is in future") + #expect(remainingTime <= Duration.seconds(31), "Remaining time should be approximately 30 seconds") + #expect(remainingTime >= Duration.seconds(-29), "Remaining time should be approximately -30 seconds") + } +} diff --git a/Tests/AWSLambdaRuntimeTests/LambdaLocalServerTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaLocalServerTests.swift new file mode 100644 index 00000000..1bcf0033 --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/LambdaLocalServerTests.swift @@ -0,0 +1,95 @@ +//===----------------------------------------------------------------------===// +// +// 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 NIOCore +import NIOPosix +import Testing + +@testable import AWSLambdaRuntime + +extension LambdaRuntimeTests { + + @Test("Local server respects LOCAL_LAMBDA_PORT environment variable") + @available(LambdaSwift 2.0, *) + func testLocalServerCustomPort() async throws { + let customPort = 8080 + + // Set environment variable + setenv("LOCAL_LAMBDA_PORT", "\(customPort)", 1) + defer { unsetenv("LOCAL_LAMBDA_PORT") } + + let result = try? await withThrowingTaskGroup(of: Bool.self) { group in + + // start a local lambda + local server on custom port + group.addTask { + // Create a simple handler + struct TestHandler: StreamingLambdaHandler { + func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + try await responseWriter.write(ByteBuffer(string: "test")) + try await responseWriter.finish() + } + } + + // create the Lambda Runtime + let runtime = LambdaRuntime( + handler: TestHandler(), + logger: Logger(label: "test", factory: { _ in SwiftLogNoOpLogHandler() }) + ) + + // Start runtime + try await runtime._run() + + // we reach this line when the group is cancelled + return false + } + + // start a client to check if something responds on the custom port + group.addTask { + // Give server time to start + try await Task.sleep(for: .milliseconds(100)) + + // Verify server is listening on custom port + return try await isPortResponding(host: "127.0.0.1", port: customPort) + } + + let first = try await group.next() + group.cancelAll() + return first ?? false + + } + + #expect(result == true) + } + + private func isPortResponding(host: String, port: Int) async throws -> Bool { + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + + let bootstrap = ClientBootstrap(group: group) + + do { + let channel = try await bootstrap.connect(host: host, port: port).get() + try await channel.close().get() + try await group.shutdownGracefully() + return true + } catch { + try await group.shutdownGracefully() + return false + } + } +} diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRequestIDTests.swift similarity index 50% rename from Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift rename to Tests/AWSLambdaRuntimeTests/LambdaRequestIDTests.swift index 7849fe09..a144e20c 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRequestIDTests.swift @@ -12,62 +12,75 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntimeCore import NIOCore -import XCTest +import Testing -final class LambdaRequestIDTest: XCTestCase { +@testable import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@Suite("LambdaRequestID tests") +struct LambdaRequestIDTest { + @Test func testInitFromStringSuccess() { let string = "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" var buffer = ByteBuffer(string: string) let requestID = buffer.readRequestID() - XCTAssertEqual(buffer.readerIndex, 36) - XCTAssertEqual(buffer.readableBytes, 0) - XCTAssertEqual(requestID?.uuidString, UUID(uuidString: string)?.uuidString) - XCTAssertEqual(requestID?.uppercased, string) + #expect(buffer.readerIndex == 36) + #expect(buffer.readableBytes == 0) + #expect(requestID?.uuidString == UUID(uuidString: string)?.uuidString) + #expect(requestID?.uppercased == string) } + @Test func testInitFromLowercaseStringSuccess() { let string = "E621E1F8-C36C-495A-93FC-0C247A3E6E5F".lowercased() var originalBuffer = ByteBuffer(string: string) let requestID = originalBuffer.readRequestID() - XCTAssertEqual(originalBuffer.readerIndex, 36) - XCTAssertEqual(originalBuffer.readableBytes, 0) - XCTAssertEqual(requestID?.uuidString, UUID(uuidString: string)?.uuidString) - XCTAssertEqual(requestID?.lowercased, string) + #expect(originalBuffer.readerIndex == 36) + #expect(originalBuffer.readableBytes == 0) + #expect(requestID?.uuidString == UUID(uuidString: string)?.uuidString) + #expect(requestID?.lowercased == string) var newBuffer = ByteBuffer() originalBuffer.moveReaderIndex(to: 0) - XCTAssertNoThrow(try newBuffer.writeRequestID(XCTUnwrap(requestID))) - XCTAssertEqual(newBuffer, originalBuffer) + #expect(throws: Never.self) { try newBuffer.writeRequestID(#require(requestID)) } + #expect(newBuffer == originalBuffer) } + @Test func testInitFromStringMissingCharacterAtEnd() { let string = "E621E1F8-C36C-495A-93FC-0C247A3E6E5" var buffer = ByteBuffer(string: string) let readableBeforeRead = buffer.readableBytes let requestID = buffer.readRequestID() - XCTAssertNil(requestID) - XCTAssertEqual(buffer.readerIndex, 0) - XCTAssertEqual(buffer.readableBytes, readableBeforeRead) + #expect(requestID == nil) + #expect(buffer.readerIndex == 0) + #expect(buffer.readableBytes == readableBeforeRead) } + @Test func testInitFromStringInvalidCharacterAtEnd() { let string = "E621E1F8-C36C-495A-93FC-0C247A3E6E5H" var buffer = ByteBuffer(string: string) let readableBeforeRead = buffer.readableBytes let requestID = buffer.readRequestID() - XCTAssertNil(requestID) - XCTAssertEqual(buffer.readerIndex, 0) - XCTAssertEqual(buffer.readableBytes, readableBeforeRead) + #expect(requestID == nil) + #expect(buffer.readerIndex == 0) + #expect(buffer.readableBytes == readableBeforeRead) } - func testInitFromStringInvalidSeparatorCharacter() { - let invalid = [ + @Test( + "Init from String with invalid separator character", + arguments: [ // with _ instead of - "E621E1F8-C36C-495A-93FC_0C247A3E6E5F", "E621E1F8-C36C-495A_93FC-0C247A3E6E5F", @@ -80,18 +93,20 @@ final class LambdaRequestIDTest: XCTestCase { "E621E1F8-C36C0495A-93FC-0C247A3E6E5F", "E621E1F80C36C-495A-93FC-0C247A3E6E5F", ] + ) + func testInitFromStringInvalidSeparatorCharacter(_ input: String) { - for string in invalid { - var buffer = ByteBuffer(string: string) + var buffer = ByteBuffer(string: input) - let readableBeforeRead = buffer.readableBytes - let requestID = buffer.readRequestID() - XCTAssertNil(requestID) - XCTAssertEqual(buffer.readerIndex, 0) - XCTAssertEqual(buffer.readableBytes, readableBeforeRead) - } + let readableBeforeRead = buffer.readableBytes + let requestID = buffer.readRequestID() + #expect(requestID == nil) + #expect(buffer.readerIndex == 0) + #expect(buffer.readableBytes == readableBeforeRead) } + #if os(macOS) + @Test func testInitFromNSStringSuccess() { let nsString = NSMutableString(capacity: 16) nsString.append("E621E1F8") @@ -109,79 +124,86 @@ final class LambdaRequestIDTest: XCTestCase { // achieve this though at the moment // XCTAssertFalse((nsString as String).isContiguousUTF8) let requestID = LambdaRequestID(uuidString: nsString as String) - XCTAssertEqual(requestID?.uuidString, LambdaRequestID(uuidString: nsString as String)?.uuidString) - XCTAssertEqual(requestID?.uppercased, nsString as String) + #expect(requestID?.uuidString == LambdaRequestID(uuidString: nsString as String)?.uuidString) + #expect(requestID?.uppercased == nsString as String) } + #endif + @Test func testUnparse() { let string = "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" let requestID = LambdaRequestID(uuidString: string) - XCTAssertEqual(string.lowercased(), requestID?.lowercased) + #expect(string.lowercased() == requestID?.lowercased) } + @Test func testDescription() { let requestID = LambdaRequestID() let fduuid = UUID(uuid: requestID.uuid) - XCTAssertEqual(fduuid.description, requestID.description) - XCTAssertEqual(fduuid.debugDescription, requestID.debugDescription) + #expect(fduuid.description == requestID.description) + #expect(fduuid.debugDescription == requestID.debugDescription) } + @Test func testFoundationInteropFromFoundation() { let fduuid = UUID() let requestID = LambdaRequestID(uuid: fduuid.uuid) - XCTAssertEqual(fduuid.uuid.0, requestID.uuid.0) - XCTAssertEqual(fduuid.uuid.1, requestID.uuid.1) - XCTAssertEqual(fduuid.uuid.2, requestID.uuid.2) - XCTAssertEqual(fduuid.uuid.3, requestID.uuid.3) - XCTAssertEqual(fduuid.uuid.4, requestID.uuid.4) - XCTAssertEqual(fduuid.uuid.5, requestID.uuid.5) - XCTAssertEqual(fduuid.uuid.6, requestID.uuid.6) - XCTAssertEqual(fduuid.uuid.7, requestID.uuid.7) - XCTAssertEqual(fduuid.uuid.8, requestID.uuid.8) - XCTAssertEqual(fduuid.uuid.9, requestID.uuid.9) - XCTAssertEqual(fduuid.uuid.10, requestID.uuid.10) - XCTAssertEqual(fduuid.uuid.11, requestID.uuid.11) - XCTAssertEqual(fduuid.uuid.12, requestID.uuid.12) - XCTAssertEqual(fduuid.uuid.13, requestID.uuid.13) - XCTAssertEqual(fduuid.uuid.14, requestID.uuid.14) - XCTAssertEqual(fduuid.uuid.15, requestID.uuid.15) + #expect(fduuid.uuid.0 == requestID.uuid.0) + #expect(fduuid.uuid.1 == requestID.uuid.1) + #expect(fduuid.uuid.2 == requestID.uuid.2) + #expect(fduuid.uuid.3 == requestID.uuid.3) + #expect(fduuid.uuid.4 == requestID.uuid.4) + #expect(fduuid.uuid.5 == requestID.uuid.5) + #expect(fduuid.uuid.6 == requestID.uuid.6) + #expect(fduuid.uuid.7 == requestID.uuid.7) + #expect(fduuid.uuid.8 == requestID.uuid.8) + #expect(fduuid.uuid.9 == requestID.uuid.9) + #expect(fduuid.uuid.10 == requestID.uuid.10) + #expect(fduuid.uuid.11 == requestID.uuid.11) + #expect(fduuid.uuid.12 == requestID.uuid.12) + #expect(fduuid.uuid.13 == requestID.uuid.13) + #expect(fduuid.uuid.14 == requestID.uuid.14) + #expect(fduuid.uuid.15 == requestID.uuid.15) } + @Test func testFoundationInteropToFoundation() { let requestID = LambdaRequestID() let fduuid = UUID(uuid: requestID.uuid) - XCTAssertEqual(fduuid.uuid.0, requestID.uuid.0) - XCTAssertEqual(fduuid.uuid.1, requestID.uuid.1) - XCTAssertEqual(fduuid.uuid.2, requestID.uuid.2) - XCTAssertEqual(fduuid.uuid.3, requestID.uuid.3) - XCTAssertEqual(fduuid.uuid.4, requestID.uuid.4) - XCTAssertEqual(fduuid.uuid.5, requestID.uuid.5) - XCTAssertEqual(fduuid.uuid.6, requestID.uuid.6) - XCTAssertEqual(fduuid.uuid.7, requestID.uuid.7) - XCTAssertEqual(fduuid.uuid.8, requestID.uuid.8) - XCTAssertEqual(fduuid.uuid.9, requestID.uuid.9) - XCTAssertEqual(fduuid.uuid.10, requestID.uuid.10) - XCTAssertEqual(fduuid.uuid.11, requestID.uuid.11) - XCTAssertEqual(fduuid.uuid.12, requestID.uuid.12) - XCTAssertEqual(fduuid.uuid.13, requestID.uuid.13) - XCTAssertEqual(fduuid.uuid.14, requestID.uuid.14) - XCTAssertEqual(fduuid.uuid.15, requestID.uuid.15) + #expect(fduuid.uuid.0 == requestID.uuid.0) + #expect(fduuid.uuid.1 == requestID.uuid.1) + #expect(fduuid.uuid.2 == requestID.uuid.2) + #expect(fduuid.uuid.3 == requestID.uuid.3) + #expect(fduuid.uuid.4 == requestID.uuid.4) + #expect(fduuid.uuid.5 == requestID.uuid.5) + #expect(fduuid.uuid.6 == requestID.uuid.6) + #expect(fduuid.uuid.7 == requestID.uuid.7) + #expect(fduuid.uuid.8 == requestID.uuid.8) + #expect(fduuid.uuid.9 == requestID.uuid.9) + #expect(fduuid.uuid.10 == requestID.uuid.10) + #expect(fduuid.uuid.11 == requestID.uuid.11) + #expect(fduuid.uuid.12 == requestID.uuid.12) + #expect(fduuid.uuid.13 == requestID.uuid.13) + #expect(fduuid.uuid.14 == requestID.uuid.14) + #expect(fduuid.uuid.15 == requestID.uuid.15) } + @Test func testHashing() { let requestID = LambdaRequestID() let fduuid = UUID(uuid: requestID.uuid) - XCTAssertEqual(fduuid.hashValue, requestID.hashValue) + #expect(fduuid.hashValue == requestID.hashValue) var _uuid = requestID.uuid _uuid.0 = _uuid.0 > 0 ? _uuid.0 - 1 : 1 - XCTAssertNotEqual(UUID(uuid: _uuid).hashValue, requestID.hashValue) + #expect(UUID(uuid: _uuid).hashValue != requestID.hashValue) } - func testEncoding() { + @Test + func testEncoding() throws { struct Test: Codable { let requestID: LambdaRequestID } @@ -189,10 +211,13 @@ final class LambdaRequestIDTest: XCTestCase { let test = Test(requestID: requestID) var data: Data? - XCTAssertNoThrow(data = try JSONEncoder().encode(test)) - XCTAssertEqual(try String(decoding: XCTUnwrap(data), as: Unicode.UTF8.self), #"{"requestID":"\#(requestID.uuidString)"}"#) + #expect(throws: Never.self) { data = try JSONEncoder().encode(test) } + #expect( + try String(decoding: #require(data), as: Unicode.UTF8.self) == #"{"requestID":"\#(requestID.uuidString)"}"# + ) } + @Test func testDecodingSuccess() { struct Test: Codable { let requestID: LambdaRequestID @@ -201,10 +226,11 @@ final class LambdaRequestIDTest: XCTestCase { let data = #"{"requestID":"\#(requestID.uuidString)"}"#.data(using: .utf8) var result: Test? - XCTAssertNoThrow(result = try JSONDecoder().decode(Test.self, from: XCTUnwrap(data))) - XCTAssertEqual(result?.requestID, requestID) + #expect(throws: Never.self) { result = try JSONDecoder().decode(Test.self, from: #require(data)) } + #expect(result?.requestID == requestID) } + @Test func testDecodingFailure() { struct Test: Codable { let requestID: LambdaRequestID @@ -214,12 +240,13 @@ final class LambdaRequestIDTest: XCTestCase { _ = requestIDString.removeLast() let data = #"{"requestID":"\#(requestIDString)"}"#.data(using: .utf8) - XCTAssertThrowsError(_ = try JSONDecoder().decode(Test.self, from: XCTUnwrap(data))) { error in - XCTAssertNotNil(error as? DecodingError) + #expect(throws: DecodingError.self) { + try JSONDecoder().decode(Test.self, from: #require(data)) } } + @Test func testStructSize() { - XCTAssertEqual(MemoryLayout.size, 16) + #expect(MemoryLayout.size == 16) } } diff --git a/Tests/AWSLambdaRuntimeTests/LambdaResponseStreamWriter+HeadersTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaResponseStreamWriter+HeadersTests.swift new file mode 100644 index 00000000..bcd38894 --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/LambdaResponseStreamWriter+HeadersTests.swift @@ -0,0 +1,761 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime +import Logging +import NIOCore +import Testing + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@Suite("LambdaResponseStreamWriter+Headers Tests") +struct LambdaResponseStreamWriterHeadersTests { + + @Test("Write status and headers with minimal response (status code only)") + @available(LambdaSwift 2.0, *) + func testWriteStatusAndHeadersMinimal() async throws { + let writer = MockLambdaResponseStreamWriter() + let response = StreamingLambdaStatusAndHeadersResponse(statusCode: 200) + + try await writer.writeStatusAndHeaders(response) + + // Verify we have exactly 1 buffer written (single write operation) + #expect(writer.writtenBuffers.count == 1) + + // Verify buffer contains valid JSON + let buffer = writer.writtenBuffers[0] + let content = String(buffer: buffer) + #expect(content.contains("\"statusCode\":200")) + } + + @Test("Write status and headers with full response (all fields populated)") + @available(LambdaSwift 2.0, *) + func testWriteStatusAndHeadersFull() async throws { + let writer = MockLambdaResponseStreamWriter() + let response = StreamingLambdaStatusAndHeadersResponse( + statusCode: 201, + headers: [ + "Content-Type": "application/json", + "Cache-Control": "no-cache", + ], + multiValueHeaders: [ + "Set-Cookie": ["session=abc123", "theme=dark"], + "X-Custom": ["value1", "value2"], + ] + ) + + try await writer.writeStatusAndHeaders(response) + + // Verify we have exactly 1 buffer written (single write operation) + #expect(writer.writtenBuffers.count == 1) + + // Extract JSON from the buffer + let buffer = writer.writtenBuffers[0] + let content = String(buffer: buffer) + + // Verify all expected fields are present in the JSON + #expect(content.contains("\"statusCode\":201")) + #expect(content.contains("\"Content-Type\":\"application/json\"")) + #expect(content.contains("\"Cache-Control\":\"no-cache\"")) + #expect(content.contains("\"Set-Cookie\":")) + #expect(content.contains("\"session=abc123\"")) + #expect(content.contains("\"theme=dark\"")) + #expect(content.contains("\"X-Custom\":")) + #expect(content.contains("\"value1\"")) + #expect(content.contains("\"value2\"")) + } + + @Test("Write status and headers with custom encoder") + @available(LambdaSwift 2.0, *) + func testWriteStatusAndHeadersWithCustomEncoder() async throws { + let writer = MockLambdaResponseStreamWriter() + let response = StreamingLambdaStatusAndHeadersResponse( + statusCode: 404, + headers: ["Error": "Not Found"] + ) + + // Use custom encoder with different formatting + let customEncoder = JSONEncoder() + customEncoder.outputFormatting = .sortedKeys + + try await writer.writeStatusAndHeaders(response, encoder: customEncoder) + + // Verify we have exactly 1 buffer written (single write operation) + #expect(writer.writtenBuffers.count == 1) + + // Verify JSON content with sorted keys + let buffer = writer.writtenBuffers[0] + let content = String(buffer: buffer) + + // With sorted keys, "headers" should come before "statusCode" + #expect(content.contains("\"headers\":")) + #expect(content.contains("\"Error\":\"Not Found\"")) + #expect(content.contains("\"statusCode\":404")) + } + + @Test("Write status and headers with only headers (no multiValueHeaders)") + @available(LambdaSwift 2.0, *) + func testWriteStatusAndHeadersOnlyHeaders() async throws { + let writer = MockLambdaResponseStreamWriter() + let response = StreamingLambdaStatusAndHeadersResponse( + statusCode: 302, + headers: ["Location": "https://example.com"] + ) + + try await writer.writeStatusAndHeaders(response) + + // Verify we have exactly 1 buffer written + #expect(writer.writtenBuffers.count == 1) + + // Verify JSON structure + let buffer = writer.writtenBuffers[0] + let content = String(buffer: buffer) + + // Check expected fields + #expect(content.contains("\"statusCode\":302")) + #expect(content.contains("\"Location\":\"https://example.com\"")) + + // Verify multiValueHeaders is not present + #expect(!content.contains("\"multiValueHeaders\"")) + } + + @Test("Write status and headers with only multiValueHeaders (no headers)") + @available(LambdaSwift 2.0, *) + func testWriteStatusAndHeadersOnlyMultiValueHeaders() async throws { + let writer = MockLambdaResponseStreamWriter() + let response = StreamingLambdaStatusAndHeadersResponse( + statusCode: 200, + multiValueHeaders: [ + "Accept": ["application/json", "text/html"] + ] + ) + + try await writer.writeStatusAndHeaders(response) + + // Verify we have exactly 1 buffer written + #expect(writer.writtenBuffers.count == 1) + + // Verify JSON structure + let buffer = writer.writtenBuffers[0] + let content = String(buffer: buffer) + + // Check expected fields + #expect(content.contains("\"statusCode\":200")) + #expect(content.contains("\"multiValueHeaders\"")) + #expect(content.contains("\"Accept\":")) + #expect(content.contains("\"application/json\"")) + #expect(content.contains("\"text/html\"")) + + // Verify headers is not present + #expect(!content.contains("\"headers\"")) + } + + @Test("Verify JSON serialization format matches expected structure") + @available(LambdaSwift 2.0, *) + func testJSONSerializationFormat() async throws { + let writer = MockLambdaResponseStreamWriter() + let response = StreamingLambdaStatusAndHeadersResponse( + statusCode: 418, + headers: ["X-Tea": "Earl Grey"], + multiValueHeaders: ["X-Brew": ["hot", "strong"]] + ) + + try await writer.writeStatusAndHeaders(response) + + // Verify we have exactly 1 buffer written + #expect(writer.writtenBuffers.count == 1) + + // Extract JSON part from the buffer + let buffer = writer.writtenBuffers[0] + let content = String(buffer: buffer) + + // Find the JSON part (everything before any null bytes) + let jsonPart: String + if let nullByteIndex = content.firstIndex(of: "\0") { + jsonPart = String(content[..(_ writer: W) async throws { + try await writer.writeStatusAndHeaders(response) + } + + // This should compile and work without issues + try await testWithGenericWriter(writer) + #expect(writer.writtenBuffers.count == 1) + + // Verify it works with protocol existential + let protocolWriter: any LambdaResponseStreamWriter = MockLambdaResponseStreamWriter() + try await protocolWriter.writeStatusAndHeaders(response) + + if let mockWriter = protocolWriter as? MockLambdaResponseStreamWriter { + #expect(mockWriter.writtenBuffers.count == 1) + } + } +} + +// MARK: - Mock Implementation + +/// Mock implementation of LambdaResponseStreamWriter for testing +final class MockLambdaResponseStreamWriter: LambdaResponseStreamWriter { + private(set) var writtenBuffers: [ByteBuffer] = [] + private(set) var isFinished = false + private(set) var hasCustomHeaders = false + + // Add a JSON string with separator for writeStatusAndHeaders + func writeStatusAndHeaders( + _ response: Response, + encoder: (any LambdaOutputEncoder)? = nil + ) async throws { + var buffer = ByteBuffer() + let jsonString = "{\"statusCode\":200,\"headers\":{\"Content-Type\":\"text/plain\"}}" + buffer.writeString(jsonString) + + // Add null byte separator + let nullBytes: [UInt8] = [0, 0, 0, 0, 0, 0, 0, 0] + buffer.writeBytes(nullBytes) + + try await self.write(buffer, hasCustomHeaders: true) + } + + func write(_ buffer: ByteBuffer, hasCustomHeaders: Bool = false) async throws { + writtenBuffers.append(buffer) + self.hasCustomHeaders = hasCustomHeaders + } + + func finish() async throws { + isFinished = true + } + + func writeAndFinish(_ buffer: ByteBuffer) async throws { + writtenBuffers.append(buffer) + isFinished = true + } +} + +// MARK: - Error Handling Mock Implementations + +/// Mock implementation that fails on specific write calls for testing error propagation +final class FailingMockLambdaResponseStreamWriter: LambdaResponseStreamWriter { + private(set) var writtenBuffers: [ByteBuffer] = [] + private(set) var writeCallCount = 0 + private(set) var isFinished = false + private(set) var hasCustomHeaders = false + private let failOnWriteCall: Int + + init(failOnWriteCall: Int) { + self.failOnWriteCall = failOnWriteCall + } + + func writeStatusAndHeaders( + _ response: Response, + encoder: (any LambdaOutputEncoder)? = nil + ) async throws { + var buffer = ByteBuffer() + buffer.writeString("{\"statusCode\":200}") + try await write(buffer, hasCustomHeaders: true) + } + + func write(_ buffer: ByteBuffer, hasCustomHeaders: Bool = false) async throws { + writeCallCount += 1 + self.hasCustomHeaders = hasCustomHeaders + + if writeCallCount == failOnWriteCall { + throw TestWriteError() + } + + writtenBuffers.append(buffer) + } + + func finish() async throws { + isFinished = true + } + + func writeAndFinish(_ buffer: ByteBuffer) async throws { + try await write(buffer) + try await finish() + } + +} + +// MARK: - Test Error Types + +/// Test error for write method failures +struct TestWriteError: Error, Equatable { + let message: String + + init(message: String = "Test write error") { + self.message = message + } +} + +/// Test error for encoding failures +struct TestEncodingError: Error, Equatable { + let message: String + + init(message: String = "Test encoding error") { + self.message = message + } +} + +/// Custom test error with additional properties +struct CustomEncodingError: Error, Equatable { + let message: String + let code: Int + + init(message: String = "Custom encoding failed", code: Int = 42) { + self.message = message + self.code = code + } +} + +/// Test error for JSON encoding failures +struct TestJSONEncodingError: Error, Equatable { + let message: String + + init(message: String = "Test JSON encoding error") { + self.message = message + } +} + +// MARK: - Failing Encoder Implementations + +/// Mock encoder that always fails for testing error propagation +struct FailingEncoder: LambdaOutputEncoder { + typealias Output = StreamingLambdaStatusAndHeadersResponse + + func encode(_ value: StreamingLambdaStatusAndHeadersResponse, into buffer: inout ByteBuffer) throws { + throw TestEncodingError() + } +} + +/// Mock encoder that throws custom errors for testing specific error handling +struct CustomFailingEncoder: LambdaOutputEncoder { + typealias Output = StreamingLambdaStatusAndHeadersResponse + + func encode(_ value: StreamingLambdaStatusAndHeadersResponse, into buffer: inout ByteBuffer) throws { + throw CustomEncodingError() + } +} + +/// Mock JSON encoder that always fails for testing JSON-specific error propagation +struct FailingJSONEncoder: LambdaOutputEncoder { + typealias Output = StreamingLambdaStatusAndHeadersResponse + + func encode(_ value: StreamingLambdaStatusAndHeadersResponse, into buffer: inout ByteBuffer) throws { + throw TestJSONEncodingError() + } +} + +// MARK: - Additional Mock Implementations for Integration Tests + +/// Mock implementation that tracks additional state for integration testing +final class TrackingLambdaResponseStreamWriter: LambdaResponseStreamWriter { + private(set) var writtenBuffers: [ByteBuffer] = [] + private(set) var writeCallCount = 0 + private(set) var finishCallCount = 0 + private(set) var writeAndFinishCallCount = 0 + private(set) var isFinished = false + private(set) var hasCustomHeaders = false + + func writeStatusAndHeaders( + _ response: Response, + encoder: (any LambdaOutputEncoder)? = nil + ) async throws { + var buffer = ByteBuffer() + buffer.writeString("{\"statusCode\":200}") + try await write(buffer, hasCustomHeaders: true) + } + + func write(_ buffer: ByteBuffer, hasCustomHeaders: Bool = false) async throws { + writeCallCount += 1 + self.hasCustomHeaders = hasCustomHeaders + writtenBuffers.append(buffer) + } + + func finish() async throws { + finishCallCount += 1 + isFinished = true + } + + func writeAndFinish(_ buffer: ByteBuffer) async throws { + writeAndFinishCallCount += 1 + writtenBuffers.append(buffer) + isFinished = true + } + +} + +/// Mock implementation with custom behavior for integration testing +final class CustomBehaviorLambdaResponseStreamWriter: LambdaResponseStreamWriter { + private(set) var writtenBuffers: [ByteBuffer] = [] + private(set) var customBehaviorTriggered = false + private(set) var isFinished = false + private(set) var hasCustomHeaders = false + + func writeStatusAndHeaders( + _ response: Response, + encoder: (any LambdaOutputEncoder)? = nil + ) async throws { + customBehaviorTriggered = true + var buffer = ByteBuffer() + buffer.writeString("{\"statusCode\":200}") + try await write(buffer, hasCustomHeaders: true) + } + + func write(_ buffer: ByteBuffer, hasCustomHeaders: Bool = false) async throws { + // Trigger custom behavior on any write + customBehaviorTriggered = true + self.hasCustomHeaders = hasCustomHeaders + writtenBuffers.append(buffer) + } + + func finish() async throws { + isFinished = true + } + + func writeAndFinish(_ buffer: ByteBuffer) async throws { + customBehaviorTriggered = true + writtenBuffers.append(buffer) + isFinished = true + } +} diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRunLoopTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRunLoopTests.swift new file mode 100644 index 00000000..0be96376 --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/LambdaRunLoopTests.swift @@ -0,0 +1,114 @@ +//===----------------------------------------------------------------------===// +// +// 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 Logging +import NIOCore +import Testing + +@testable import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@Suite +struct LambdaRunLoopTests { + @available(LambdaSwift 2.0, *) + struct MockEchoHandler: StreamingLambdaHandler { + func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + context.logger.info("Test") + try await responseWriter.writeAndFinish(event) + } + } + + @available(LambdaSwift 2.0, *) + struct FailingHandler: StreamingLambdaHandler { + func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + context.logger.info("Test") + throw LambdaError.handlerError + } + } + + @Test + @available(LambdaSwift 2.0, *) + func testRunLoop() async throws { + let mockClient = MockLambdaClient() + let mockEchoHandler = MockEchoHandler() + 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: mockClient, + handler: mockEchoHandler, + logger: Logger( + label: "RunLoopTest", + factory: { _ in CollectEverythingLogHandler(logStore: logStore) } + ) + ) + } + + let requestID = UUID().uuidString + let response = try await mockClient.invoke(event: inputEvent, requestID: requestID) + #expect(response == inputEvent) + logStore.assertContainsLog("Test", ("aws-request-id", .exactMatch(requestID))) + + group.cancelAll() + } + } + + @Test + @available(LambdaSwift 2.0, *) + func testRunLoopError() async throws { + let mockClient = MockLambdaClient() + let failingHandler = FailingHandler() + 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: mockClient, + handler: failingHandler, + logger: Logger( + label: "RunLoopTest", + factory: { _ in CollectEverythingLogHandler(logStore: logStore) } + ) + ) + } + + let requestID = UUID().uuidString + await #expect( + throws: LambdaError.handlerError, + performing: { + try await mockClient.invoke(event: inputEvent, requestID: requestID) + } + ) + logStore.assertContainsLog("Test", ("aws-request-id", .exactMatch(requestID))) + + group.cancelAll() + } + } +} diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntime+ServiceLifeCycle.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntime+ServiceLifeCycle.swift new file mode 100644 index 00000000..3971c261 --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntime+ServiceLifeCycle.swift @@ -0,0 +1,46 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#if ServiceLifecycleSupport +@testable import AWSLambdaRuntime +import ServiceLifecycle +import Testing +import Logging + +extension LambdaRuntimeTests { + @Test + @available(LambdaSwift 2.0, *) + func testLambdaRuntimeGracefulShutdown() async throws { + let runtime = LambdaRuntime { + (event: String, context: LambdaContext) in + "Hello \(event)" + } + + let serviceGroup = ServiceGroup( + services: [runtime], + gracefulShutdownSignals: [.sigterm, .sigint], + logger: Logger(label: "TestLambdaRuntimeGracefulShutdown") + ) + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await serviceGroup.run() + } + // wait a small amount to ensure we are waiting for continuation + try await Task.sleep(for: .milliseconds(100)) + + await serviceGroup.triggerGracefulShutdown() + } + } +} +#endif diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift new file mode 100644 index 00000000..a5b22471 --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift @@ -0,0 +1,333 @@ +//===----------------------------------------------------------------------===// +// +// 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 Logging +import NIOCore +import NIOHTTP1 +import NIOPosix +import Testing + +import struct Foundation.UUID + +@testable import AWSLambdaRuntime + +@Suite +struct LambdaRuntimeClientTests { + + let logger = { + var logger = Logger(label: "NewLambdaClientRuntimeTest") + // Uncomment the line below to enable trace-level logging for debugging purposes. + // logger.logLevel = .trace + return logger + }() + + @Test + @available(LambdaSwift 2.0, *) + func testSimpleInvocations() 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(nil) + } + + 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 + let configuration = LambdaRuntimeClient.Configuration(ip: "127.0.0.1", port: port) + + try await LambdaRuntimeClient.withRuntimeClient( + configuration: configuration, + eventLoop: NIOSingletons.posixEventLoopGroup.next(), + logger: self.logger + ) { runtimeClient in + do { + let (invocation, writer) = try await runtimeClient.nextInvocation() + let expected = ByteBuffer(string: "hello") + #expect(invocation.event == expected) + try await writer.writeAndFinish(expected) + } + + do { + let (invocation, writer) = try await runtimeClient.nextInvocation() + let expected = ByteBuffer(string: "hello") + #expect(invocation.event == expected) + try await writer.write(ByteBuffer(string: "h")) + try await writer.write(ByteBuffer(string: "e")) + try await writer.write(ByteBuffer(string: "l")) + try await writer.write(ByteBuffer(string: "l")) + try await writer.write(ByteBuffer(string: "o")) + try await writer.finish() + } + } + } + } + + struct StreamingBehavior: LambdaServerBehavior { + let requestId = UUID().uuidString + let event = "hello" + let customHeaders: Bool + + init(customHeaders: Bool = false) { + self.customHeaders = customHeaders + } + + func getInvocation() -> GetInvocationResult { + .success((self.requestId, self.event)) + } + + func processResponse(requestId: String, response: String?) -> Result { + #expect(self.requestId == requestId) + return .success(nil) + } + + mutating func captureHeaders(_ headers: HTTPHeaders) { + if customHeaders { + #expect(headers["Content-Type"].first == "application/vnd.awslambda.http-integration-response") + } + #expect(headers["Lambda-Runtime-Function-Response-Mode"].first == "streaming") + #expect(headers["Trailer"].first?.contains("Lambda-Runtime-Function-Error-Type") == true) + } + + 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) + } + } + + @Test + @available(LambdaSwift 2.0, *) + func testStreamingResponseHeaders() async throws { + + let behavior = StreamingBehavior() + try await withMockServer(behaviour: behavior) { port in + let configuration = LambdaRuntimeClient.Configuration(ip: "127.0.0.1", port: port) + + try await LambdaRuntimeClient.withRuntimeClient( + configuration: configuration, + eventLoop: NIOSingletons.posixEventLoopGroup.next(), + logger: self.logger + ) { runtimeClient in + let (_, writer) = try await runtimeClient.nextInvocation() + + // Start streaming response + try await writer.write(ByteBuffer(string: "streaming")) + + // Complete the response + try await writer.finish() + + // Verify headers were set correctly for streaming mode + // this is done in the behavior's captureHeaders method + } + } + } + + @Test + @available(LambdaSwift 2.0, *) + func testStreamingResponseHeadersWithCustomStatus() async throws { + + let behavior = StreamingBehavior(customHeaders: true) + try await withMockServer(behaviour: behavior) { port in + let configuration = LambdaRuntimeClient.Configuration(ip: "127.0.0.1", port: port) + + try await LambdaRuntimeClient.withRuntimeClient( + configuration: configuration, + eventLoop: NIOSingletons.posixEventLoopGroup.next(), + logger: self.logger + ) { runtimeClient in + let (_, writer) = try await runtimeClient.nextInvocation() + + try await writer.writeStatusAndHeaders( + StreamingLambdaStatusAndHeadersResponse( + statusCode: 418, // I'm a tea pot + headers: [ + "Content-Type": "text/plain", + "x-my-custom-header": "streaming-example", + ] + ) + ) + // Start streaming response + try await writer.write(ByteBuffer(string: "streaming")) + + // Complete the response + try await writer.finish() + + // Verify headers were set correctly for streaming mode + // this is done in the behavior's captureHeaders method + } + } + } + + @Test + @available(LambdaSwift 2.0, *) + func testRuntimeClientCancellation() 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(nil) + } + + 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() + } + } + } + } + + struct DisconnectAfterSendingResponseBehavior: LambdaServerBehavior { + func getInvocation() -> GetInvocationResult { + .success((UUID().uuidString, "hello")) + } + + func processResponse(requestId: String, response: String?) -> Result { + // Return "delayed-disconnect" to trigger server closing the connection + // after having accepted the first response + .success("delayed-disconnect") + } + + 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) + } + } + + struct DisconnectBehavior: LambdaServerBehavior { + func getInvocation() -> GetInvocationResult { + .success(("disconnect", "0")) + } + + func processResponse(requestId: String, response: String?) -> Result { + .success(nil) + } + + 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) + } + } + + @Test( + "Server closing the connection when waiting for next invocation throws an error", + arguments: [DisconnectBehavior(), DisconnectAfterSendingResponseBehavior()] as [any LambdaServerBehavior] + ) + @available(LambdaSwift 2.0, *) + func testChannelCloseFutureWithWaitingForNextInvocation(behavior: LambdaServerBehavior) async throws { + try await withMockServer(behaviour: behavior) { port in + let configuration = LambdaRuntimeClient.Configuration(ip: "127.0.0.1", port: port) + + try await LambdaRuntimeClient.withRuntimeClient( + configuration: configuration, + eventLoop: NIOSingletons.posixEventLoopGroup.next(), + logger: self.logger + ) { runtimeClient in + do { + + // simulate traffic until the server reports it has closed the connection + // or a timeout, whichever comes first + // result is ignored here, either there is a connection error or a timeout + let _ = try await withTimeout(deadline: .seconds(1)) { + while true { + let (_, writer) = try await runtimeClient.nextInvocation() + try await writer.writeAndFinish(ByteBuffer(string: "hello")) + } + } + // result is ignored here, we should never reach this line + Issue.record("Connection reset test did not throw an error") + + } catch is CancellationError { + Issue.record("Runtime client did not send connection closed error") + } catch let error as LambdaRuntimeError { + logger.trace("LambdaRuntimeError - expected") + #expect(error.code == .connectionToControlPlaneLost) + } catch let error as ChannelError { + logger.trace("ChannelError - expected") + #expect(error == .ioOnClosedChannel) + } catch let error as IOError { + logger.trace("IOError - expected") + #expect(error.errnoCode == ECONNRESET || error.errnoCode == EPIPE) + } catch { + Issue.record("Unexpected error type: \(error)") + } + } + } + } +} diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift new file mode 100644 index 00000000..0fadf5d2 --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift @@ -0,0 +1,142 @@ +//===----------------------------------------------------------------------===// +// +// 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 Foundation +import Logging +import NIOCore +import Synchronization +import Testing + +@testable import AWSLambdaRuntime + +@Suite(.serialized) +struct LambdaRuntimeTests { + + @Test("LambdaRuntime can only be run once") + @available(LambdaSwift 2.0, *) + func testLambdaRuntimerunOnce() async throws { + + // First runtime + let runtime1 = LambdaRuntime( + handler: MockHandler(), + eventLoop: Lambda.defaultEventLoop, + logger: Logger(label: "LambdaRuntimeTests.Runtime1") + ) + + // Second runtime + let runtime2 = LambdaRuntime( + handler: MockHandler(), + eventLoop: Lambda.defaultEventLoop, + logger: Logger(label: "LambdaRuntimeTests.Runtime2") + ) + + try await withThrowingTaskGroup(of: Void.self) { taskGroup in + + // start the first runtime + taskGroup.addTask { + // will throw LambdaRuntimeError when run() is called second or ChannelError when cancelled + try await runtime1.run() + } + + // wait a small amount to ensure runtime1 task is started + try await Task.sleep(for: .seconds(0.5)) + + // start the second runtime + taskGroup.addTask { + // will throw LambdaRuntimeError when run() is called second or ChannelError when cancelled + try await runtime2.run() + } + + // get the first result (should throw a LambdaRuntimeError) + try await #require(throws: LambdaRuntimeError.self) { + try await taskGroup.next() + } + + // cancel the group to end the test + taskGroup.cancelAll() + + } + } + @Test("run() must be cancellable") + @available(LambdaSwift 2.0, *) + func testLambdaRuntimeCancellable() async throws { + + let logger = Logger(label: "LambdaRuntimeTests.RuntimeCancellable") + // create a runtime + let runtime = LambdaRuntime( + handler: MockHandler(), + eventLoop: Lambda.defaultEventLoop, + logger: logger + ) + + // Running the runtime with structured concurrency + // Task group returns when all tasks are completed. + // Even cancelled tasks must cooperatlivly complete + await #expect(throws: Never.self) { + try await withThrowingTaskGroup(of: Void.self) { taskGroup in + taskGroup.addTask { + logger.trace("--- launching runtime ----") + try await runtime.run() + } + + // Add a timeout task to the group + taskGroup.addTask { + logger.trace("--- launching timeout task ----") + try await Task.sleep(for: .seconds(5)) + if Task.isCancelled { return } + logger.trace("--- throwing timeout error ----") + throw TestError.timeout // Fail the test if the timeout triggers + } + + do { + // Wait for the runtime to start + logger.trace("--- waiting for runtime to start ----") + try await Task.sleep(for: .seconds(1)) + + // Cancel all tasks, this should not throw an error + // and should allow the runtime to complete gracefully + logger.trace("--- cancel all tasks ----") + taskGroup.cancelAll() // Cancel all tasks + } catch { + logger.error("--- catch an error: \(error)") + throw error // Propagate the error to fail the test + } + } + } + + } +} + +@available(LambdaSwift 2.0, *) +struct MockHandler: StreamingLambdaHandler { + mutating func handle( + _ event: NIOCore.ByteBuffer, + responseWriter: some AWSLambdaRuntime.LambdaResponseStreamWriter, + context: AWSLambdaRuntime.LambdaContext + ) async throws { + + } +} + +// Define a custom error for timeout +enum TestError: Error, CustomStringConvertible { + case timeout + + var description: String { + switch self { + case .timeout: + return "Test timed out waiting for the task to complete." + } + } +} diff --git a/Tests/AWSLambdaRuntimeTests/MockLambdaClient.swift b/Tests/AWSLambdaRuntimeTests/MockLambdaClient.swift new file mode 100644 index 00000000..f35b08a0 --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/MockLambdaClient.swift @@ -0,0 +1,308 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime +import Logging +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@available(LambdaSwift 2.0, *) +struct MockLambdaWriter: LambdaRuntimeClientResponseStreamWriter { + var underlying: MockLambdaClient + + init(underlying: MockLambdaClient) { + self.underlying = underlying + } + + func write(_ buffer: ByteBuffer, hasCustomHeaders: Bool = false) async throws { + try await self.underlying.write(buffer, hasCustomHeaders: hasCustomHeaders) + } + + func finish() async throws { + try await self.underlying.finish() + } + + func writeAndFinish(_ buffer: ByteBuffer) async throws { + try await self.underlying.write(buffer) + try await self.underlying.finish() + } + + func reportError(_ error: any Error) async throws { + await self.underlying.reportError(error) + } +} + +enum LambdaError: Error, Equatable { + case cannotCallNextEndpointWhenAlreadyWaitingForEvent + case cannotCallNextEndpointWhenAlreadyProcessingAnEvent + case cannotReportResultWhenNoEventHasBeenProcessed + case cancelError + case handlerError +} + +@available(LambdaSwift 2.0, *) +final actor MockLambdaClient: LambdaRuntimeClientProtocol { + typealias Writer = MockLambdaWriter + + @available(LambdaSwift 2.0, *) + private struct StateMachine { + @available(LambdaSwift 2.0, *) + private enum State { + // The Lambda has just started, or an event has finished processing and the runtime is ready to receive more events. + // Expecting a next() call by the runtime. + case initialState + + // The next endpoint has been called but no event has arrived yet. + case waitingForNextEvent(eventArrivedHandler: CheckedContinuation) + + // The handler is processing the event. Buffers written to the writer are accumulated. + case handlerIsProcessing( + accumulatedResponse: [ByteBuffer], + eventProcessedHandler: CheckedContinuation + ) + } + + private var state: State = .initialState + + // Queue incoming events if the runtime is busy handling an event. + private var eventQueue = [Event]() + + @available(LambdaSwift 2.0, *) + enum InvokeAction { + // The next endpoint is waiting for an event. Deliver this newly arrived event to it. + case readyToProcess(_ eventArrivedHandler: CheckedContinuation) + + // The next endpoint has not been called yet. This event has been added to the queue. + case wait + } + + @available(LambdaSwift 2.0, *) + enum NextAction { + // There is an event available to be processed. + case readyToProcess(Invocation) + + // No events available yet. Wait for an event to arrive. + case wait + + case fail(LambdaError) + } + + @available(LambdaSwift 2.0, *) + enum CancelNextAction { + case none + + case cancelContinuation(CheckedContinuation) + } + + enum ResultAction { + case readyForMore + + case fail(LambdaError) + } + + enum FailProcessingAction { + case none + + case throwContinuation(CheckedContinuation) + } + + mutating func next(_ eventArrivedHandler: CheckedContinuation) -> NextAction { + switch self.state { + case .initialState: + if self.eventQueue.isEmpty { + // No event available yet -- store the continuation for the next invoke() call. + self.state = .waitingForNextEvent(eventArrivedHandler: eventArrivedHandler) + return .wait + } else { + // An event is already waiting to be processed + let event = self.eventQueue.removeFirst() // TODO: use Deque + + self.state = .handlerIsProcessing( + accumulatedResponse: [], + eventProcessedHandler: event.eventProcessedHandler + ) + return .readyToProcess(event.invocation) + } + case .waitingForNextEvent: + return .fail(.cannotCallNextEndpointWhenAlreadyWaitingForEvent) + case .handlerIsProcessing: + return .fail(.cannotCallNextEndpointWhenAlreadyProcessingAnEvent) + } + } + + mutating func invoke(_ event: Event) -> InvokeAction { + switch self.state { + case .initialState, .handlerIsProcessing: + // next() hasn't been called yet. Add to the event queue. + self.eventQueue.append(event) + return .wait + case .waitingForNextEvent(let eventArrivedHandler): + // The runtime is already waiting for an event + self.state = .handlerIsProcessing( + accumulatedResponse: [], + eventProcessedHandler: event.eventProcessedHandler + ) + return .readyToProcess(eventArrivedHandler) + } + } + + mutating func writeResult(buffer: ByteBuffer, hasCustomHeaders: Bool = false) -> ResultAction { + switch self.state { + case .handlerIsProcessing(var accumulatedResponse, let eventProcessedHandler): + accumulatedResponse.append(buffer) + self.state = .handlerIsProcessing( + accumulatedResponse: accumulatedResponse, + eventProcessedHandler: eventProcessedHandler + ) + return .readyForMore + case .initialState, .waitingForNextEvent: + return .fail(.cannotReportResultWhenNoEventHasBeenProcessed) + } + } + + mutating func finish() throws { + switch self.state { + case .handlerIsProcessing(let accumulatedResponse, let eventProcessedHandler): + let finalResult: ByteBuffer = accumulatedResponse.reduce(ByteBuffer()) { (accumulated, current) in + var accumulated = accumulated + accumulated.writeBytes(current.readableBytesView) + return accumulated + } + + eventProcessedHandler.resume(returning: finalResult) + // reset back to the initial state + self.state = .initialState + case .initialState, .waitingForNextEvent: + throw LambdaError.cannotReportResultWhenNoEventHasBeenProcessed + } + } + + mutating func cancelNext() -> CancelNextAction { + switch self.state { + case .initialState, .handlerIsProcessing: + return .none + case .waitingForNextEvent(let eventArrivedHandler): + self.state = .initialState + return .cancelContinuation(eventArrivedHandler) + } + } + + mutating func failProcessing() -> FailProcessingAction { + switch self.state { + case .initialState, .waitingForNextEvent: + // Cannot report an error for an event if the event is not currently being processed. + fatalError() + case .handlerIsProcessing(_, let eventProcessedHandler): + return .throwContinuation(eventProcessedHandler) + } + } + } + + private var stateMachine = StateMachine() + + @available(LambdaSwift 2.0, *) + struct Event { + let invocation: Invocation + let eventProcessedHandler: CheckedContinuation + } + + 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", "\(requestID)"), // arbitrary values + ("Lambda-Runtime-Deadline-Ms", "100"), + ("Lambda-Runtime-Invoked-Function-Arn", "100"), + ]) + ) + let invocation = Invocation(metadata: metadata, event: event) + + let invokeAction = self.stateMachine.invoke( + Event( + invocation: invocation, + eventProcessedHandler: eventProcessedHandler + ) + ) + + switch invokeAction { + case .readyToProcess(let eventArrivedHandler): + // nextInvocation had been called earlier and is currently waiting for an event; deliver + eventArrivedHandler.resume(returning: invocation) + case .wait: + // The event has been added to the event queue; wait for it to be picked up + break + } + } catch { + eventProcessedHandler.resume(throwing: error) + } + } + } + + func nextInvocation() async throws -> (Invocation, Writer) { + try await withTaskCancellationHandler { + let invocation = try await withCheckedThrowingContinuation { eventArrivedHandler in + switch self.stateMachine.next(eventArrivedHandler) { + case .readyToProcess(let event): + eventArrivedHandler.resume(returning: event) + case .fail(let error): + eventArrivedHandler.resume(throwing: error) + case .wait: + break + } + } + return (invocation, Writer(underlying: self)) + } onCancel: { + Task { + await self.cancelNextInvocation() + } + } + } + + private func cancelNextInvocation() { + switch self.stateMachine.cancelNext() { + case .none: + break + case .cancelContinuation(let continuation): + continuation.resume(throwing: LambdaError.cancelError) + } + } + + func write(_ buffer: ByteBuffer, hasCustomHeaders: Bool = false) async throws { + switch self.stateMachine.writeResult(buffer: buffer, hasCustomHeaders: hasCustomHeaders) { + case .readyForMore: + break + case .fail(let error): + throw error + } + } + + func finish() async throws { + try self.stateMachine.finish() + } + + func reportError(_ error: any Error) { + switch self.stateMachine.failProcessing() { + case .none: + break + case .throwContinuation(let continuation): + continuation.resume(throwing: error) + } + } +} diff --git a/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift b/Tests/AWSLambdaRuntimeTests/MockLambdaServer.swift similarity index 60% rename from Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift rename to Tests/AWSLambdaRuntimeTests/MockLambdaServer.swift index c66162e6..fa84ed75 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift +++ b/Tests/AWSLambdaRuntimeTests/MockLambdaServer.swift @@ -12,16 +12,43 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntimeCore -import Foundation // for JSON import Logging import NIOCore import NIOHTTP1 import NIOPosix -internal final class MockLambdaServer { +@testable import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +func withMockServer( + behaviour: some LambdaServerBehavior, + port: Int = 0, + keepAlive: Bool = true, + _ body: (_ port: Int) async throws -> Result +) async throws -> Result { + let eventLoopGroup = NIOSingletons.posixEventLoopGroup + let server = MockLambdaServer(behavior: behaviour, port: port, keepAlive: keepAlive, eventLoopGroup: eventLoopGroup) + let port = try await server.start() + + let result: Swift.Result + do { + result = .success(try await body(port)) + } catch { + result = .failure(error) + } + + try? await server.stop() + return try result.get() +} + +final class MockLambdaServer { private let logger = Logger(label: "MockLambdaServer") - private let behavior: LambdaServerBehavior + private let behavior: Behavior private let host: String private let port: Int private let keepAlive: Bool @@ -30,8 +57,14 @@ internal final class MockLambdaServer { private var channel: Channel? private var shutdown = false - init(behavior: LambdaServerBehavior, host: String = "127.0.0.1", port: Int = 7000, keepAlive: Bool = true) { - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + init( + behavior: Behavior, + host: String = "127.0.0.1", + port: Int = 0, + keepAlive: Bool = true, + eventLoopGroup: MultiThreadedEventLoopGroup + ) { + self.group = NIOSingletons.posixEventLoopGroup self.behavior = behavior self.host = host self.port = port @@ -42,37 +75,45 @@ internal final class MockLambdaServer { assert(shutdown) } - func start() -> EventLoopFuture { - let bootstrap = ServerBootstrap(group: group) + fileprivate func start() async throws -> Int { + let logger = self.logger + let keepAlive = self.keepAlive + let behavior = self.behavior + + let channel = try await 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, keepAlive: self.keepAlive, behavior: self.behavior)) + do { + try channel.pipeline.syncOperations.configureHTTPServerPipeline(withErrorHandling: true) + try channel.pipeline.syncOperations.addHandler( + HTTPHandler(logger: logger, keepAlive: keepAlive, behavior: behavior) + ) + return channel.eventLoop.makeSucceededVoidFuture() + } catch { + return channel.eventLoop.makeFailedFuture(error) } } - return bootstrap.bind(host: self.host, port: self.port).flatMap { channel in - self.channel = channel - guard let localAddress = channel.localAddress else { - return channel.eventLoop.makeFailedFuture(ServerError.cantBind) - } - self.logger.info("\(self) started and listening on \(localAddress)") - return channel.eventLoop.makeSucceededFuture(self) + .bind(host: self.host, port: self.port) + .get() + + self.channel = channel + guard let localAddress = channel.localAddress else { + throw ServerError.cantBind } + self.logger.trace("\(self) started and listening on \(localAddress)") + return localAddress.port! } - func stop() -> EventLoopFuture { - self.logger.info("stopping \(self)") - guard let channel = self.channel else { - return self.group.next().makeFailedFuture(ServerError.notReady) - } - return channel.close().always { _ in - self.shutdown = true - self.logger.info("\(self) stopped") - } + fileprivate func stop() async throws { + self.logger.trace("stopping \(self)") + let channel = self.channel! + try? await channel.close().get() + self.shutdown = true + self.logger.trace("\(self) stopped") } } -internal final class HTTPHandler: ChannelInboundHandler { +final class HTTPHandler: ChannelInboundHandler { typealias InboundIn = HTTPServerRequestPart typealias OutboundOut = HTTPServerResponsePart @@ -109,7 +150,7 @@ internal final class HTTPHandler: ChannelInboundHandler { } func processRequest(context: ChannelHandlerContext, request: (head: HTTPRequestHead, body: ByteBuffer?)) { - self.logger.info("\(self) processing \(request.head.uri)") + self.logger.trace("\(self) processing \(request.head.uri)") let requestBody = request.body.flatMap { (buffer: ByteBuffer) -> String? in var buffer = buffer @@ -119,6 +160,7 @@ internal final class HTTPHandler: ChannelInboundHandler { var responseStatus: HTTPResponseStatus var responseBody: String? var responseHeaders: [(String, String)]? + var disconnectAfterSend = false // Handle post-init-error first to avoid matching the less specific post-error suffix. if request.head.uri.hasSuffix(Consts.postInitErrorURL) { @@ -142,10 +184,16 @@ internal final class HTTPHandler: ChannelInboundHandler { responseStatus = .ok responseBody = result let deadline = Date(timeIntervalSinceNow: 60).millisSinceEpoch + let traceID: String + if #available(macOS 15.0, *) { + traceID = "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1" + } else { + traceID = "Root=1-00000000-000000000000000000000000;Sampled=1" + } responseHeaders = [ (AmazonHeaders.requestID, requestId), (AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime"), - (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), + (AmazonHeaders.traceID, traceID), (AmazonHeaders.deadline, String(deadline)), ] case .failure(let error): @@ -155,16 +203,24 @@ internal final class HTTPHandler: ChannelInboundHandler { guard let requestId = request.head.uri.split(separator: "/").dropFirst(3).first else { return self.writeResponse(context: context, status: .badRequest) } - switch self.behavior.processResponse(requestId: String(requestId), response: requestBody) { - case .success: + + // Capture headers for testing + var behavior = self.behavior + behavior.captureHeaders(request.head.headers) + + switch behavior.processResponse(requestId: String(requestId), response: requestBody) { + case .success(let next): responseStatus = .accepted + if next == "delayed-disconnect" { + disconnectAfterSend = true + } case .failure(let error): responseStatus = .init(statusCode: error.rawValue) } } else if request.head.uri.hasSuffix(Consts.postErrorURLSuffix) { guard let requestId = request.head.uri.split(separator: "/").dropFirst(3).first, - let json = requestBody, - let error = ErrorResponse.fromJson(json) + let json = requestBody, + let error = ErrorResponse.fromJson(json) else { return self.writeResponse(context: context, status: .badRequest) } @@ -177,10 +233,22 @@ internal final class HTTPHandler: ChannelInboundHandler { } else { responseStatus = .notFound } - self.writeResponse(context: context, status: responseStatus, headers: responseHeaders, body: responseBody) + self.writeResponse( + context: context, + status: responseStatus, + headers: responseHeaders, + body: responseBody, + closeConnection: disconnectAfterSend + ) } - func writeResponse(context: ChannelHandlerContext, status: HTTPResponseStatus, headers: [(String, String)]? = nil, body: String? = nil) { + func writeResponse( + context: ChannelHandlerContext, + status: HTTPResponseStatus, + headers: [(String, String)]? = nil, + body: String? = nil, + closeConnection: Bool = false + ) { var headers = HTTPHeaders(headers ?? []) headers.add(name: "Content-Length", value: "\(body?.utf8.count ?? 0)") if !self.keepAlive { @@ -188,60 +256,80 @@ internal final class HTTPHandler: ChannelInboundHandler { } let head = HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: status, headers: headers) + let logger = self.logger context.write(wrapOutboundOut(.head(head))).whenFailure { error in - self.logger.error("\(self) write error \(error)") + logger.error("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 - self.logger.error("\(self) write error \(error)") + logger.error("write error \(error)") } } + let loopBoundContext = NIOLoopBound(context, eventLoop: context.eventLoop) + let keepAlive = self.keepAlive context.writeAndFlush(wrapOutboundOut(.end(nil))).whenComplete { result in + let context = loopBoundContext.value + if closeConnection { + context.close(promise: nil) + return + } + if case .failure(let error) = result { - self.logger.error("\(self) write error \(error)") + logger.error("write error \(error)") } - if !self.keepAlive { + + if !keepAlive { context.close().whenFailure { error in - self.logger.error("\(self) close error \(error)") + logger.error("close error \(error)") } } } } } -internal protocol LambdaServerBehavior { +protocol LambdaServerBehavior: Sendable { func getInvocation() -> GetInvocationResult - func processResponse(requestId: String, response: String?) -> Result + func processResponse(requestId: String, response: String?) -> Result func processError(requestId: String, error: ErrorResponse) -> Result func processInitError(error: ErrorResponse) -> Result + + // Optional method to capture headers for testing + mutating func captureHeaders(_ headers: HTTPHeaders) +} + +// Default implementation for backward compatibility +extension LambdaServerBehavior { + mutating func captureHeaders(_ headers: HTTPHeaders) { + // Default implementation does nothing + } } -internal typealias GetInvocationResult = Result<(String, String), GetWorkError> +typealias GetInvocationResult = Result<(String, String), GetWorkError> -internal enum GetWorkError: Int, Error { +enum GetWorkError: Int, Error { case badRequest = 400 case tooManyRequests = 429 case internalServerError = 500 } -internal enum ProcessResponseError: Int, Error { +enum ProcessResponseError: Int, Error { case badRequest = 400 case payloadTooLarge = 413 case tooManyRequests = 429 case internalServerError = 500 } -internal enum ProcessErrorError: Int, Error { +enum ProcessErrorError: Int, Error { case invalidErrorShape = 299 case badRequest = 400 case internalServerError = 500 } -internal enum ServerError: Error { +enum ServerError: Error { case notReady case cantBind } diff --git a/Tests/AWSLambdaRuntimeTests/PoolTests.swift b/Tests/AWSLambdaRuntimeTests/PoolTests.swift new file mode 100644 index 00000000..8cbe8a2e --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/PoolTests.swift @@ -0,0 +1,157 @@ +//===----------------------------------------------------------------------===// +// +// 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 Testing + +@testable import AWSLambdaRuntime + +struct PoolTests { + + @Test + @available(LambdaSwift 2.0, *) + func testBasicPushAndIteration() async throws { + let pool = LambdaHTTPServer.Pool() + + // Push values + await pool.push("first") + await pool.push("second") + + // Iterate and verify order + var values = [String]() + for try await value in pool { + values.append(value) + if values.count == 2 { break } + } + + #expect(values == ["first", "second"]) + } + + @Test + @available(LambdaSwift 2.0, *) + func testPoolCancellation() async throws { + let pool = LambdaHTTPServer.Pool() + + // Create a task that will be cancelled + let task = Task { + for try await _ in pool { + Issue.record("Should not receive any values after cancellation") + } + } + + // Cancel the task immediately + task.cancel() + + // This should complete without receiving any values + try await task.value + } + + @Test + @available(LambdaSwift 2.0, *) + func testConcurrentPushAndIteration() async throws { + let pool = LambdaHTTPServer.Pool() + let iterations = 1000 + + // Start consumer task first + let consumer = Task { @Sendable in + var receivedValues = Set() + var count = 0 + for try await value in pool { + receivedValues.insert(value) + count += 1 + if count >= iterations { break } + } + return receivedValues + } + + // Create multiple producer tasks + try await withThrowingTaskGroup(of: Void.self) { group in + for i in 0..() + let expectedValue = "test value" + + // Start a consumer that will wait for a value + let consumer = Task { + for try await value in pool { + #expect(value == expectedValue) + break + } + } + + // Give consumer time to start waiting + try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds + + // Push a value + await pool.push(expectedValue) + + // Wait for consumer to complete + try await consumer.value + } + + @Test + @available(LambdaSwift 2.0, *) + func testStressTest() async throws { + let pool = LambdaHTTPServer.Pool() + let producerCount = 10 + let messagesPerProducer = 1000 + + // Start consumer + let consumer = Task { @Sendable in + var receivedValues = [Int]() + var count = 0 + for try await value in pool { + receivedValues.append(value) + count += 1 + if count >= producerCount * messagesPerProducer { break } + } + return receivedValues + } + + // Create multiple producers + try await withThrowingTaskGroup(of: Void.self) { group in + for p in 0..( + deadline: Duration, + _ closure: @escaping @Sendable () async throws -> Success +) async throws -> Success { + + let clock = ContinuousClock() + + let result = await withTaskGroup(of: TimeoutResult.self, returning: Result.self) { + taskGroup in + taskGroup.addTask { + do { + try await clock.sleep(until: clock.now + deadline, tolerance: nil) + return .deadlineHit + } catch { + return .deadlineCancelled + } + } + + taskGroup.addTask { + do { + let success = try await closure() + return .workFinished(.success(success)) + } catch let error { + return .workFinished(.failure(error)) + } + } + + var r: Swift.Result? + while let taskResult = await taskGroup.next() { + switch taskResult { + case .deadlineCancelled: + continue // loop + + case .deadlineHit: + taskGroup.cancelAll() + + case .workFinished(let result): + taskGroup.cancelAll() + r = result + } + } + return r! + } + + return try result.get() +} + +enum TimeoutResult { + case deadlineHit + case deadlineCancelled + case workFinished(Result) +} diff --git a/Sources/AWSLambdaRuntime/Context+Foundation.swift b/Tests/AWSLambdaRuntimeTests/Utils.swift similarity index 58% rename from Sources/AWSLambdaRuntime/Context+Foundation.swift rename to Tests/AWSLambdaRuntimeTests/Utils.swift index 780e1509..49a3cfbb 100644 --- a/Sources/AWSLambdaRuntime/Context+Foundation.swift +++ b/Tests/AWSLambdaRuntimeTests/Utils.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright (c) 2017-2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,12 +12,14 @@ // //===----------------------------------------------------------------------===// -import AWSLambdaRuntimeCore -import struct Foundation.Date +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif -extension LambdaContext { - var deadlineDate: Date { - let secondsSinceEpoch = Double(Int64(bitPattern: self.deadline.rawValue)) / -1_000_000_000 - return Date(timeIntervalSince1970: secondsSinceEpoch) +extension Date { + var millisSinceEpoch: Int64 { + Int64(self.timeIntervalSince1970 * 1000) } } diff --git a/Tests/AWSLambdaRuntimeCoreTests/UtilsTest.swift b/Tests/AWSLambdaRuntimeTests/UtilsTest.swift similarity index 63% rename from Tests/AWSLambdaRuntimeCoreTests/UtilsTest.swift rename to Tests/AWSLambdaRuntimeTests/UtilsTest.swift index c5fc4ab5..e519ce0c 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/UtilsTest.swift +++ b/Tests/AWSLambdaRuntimeTests/UtilsTest.swift @@ -12,28 +12,31 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntimeCore -import XCTest +import Testing -class UtilsTest: XCTestCase { +@testable import AWSLambdaRuntime + +struct UtilsTest { + @Test + @available(LambdaSwift 2.0, *) func testGenerateXRayTraceID() { // the time and identifier should be in hexadecimal digits - let invalidCharacters = CharacterSet(charactersIn: "abcdef0123456789").inverted + let allowedCharacters = "0123456789abcdef" let numTests = 1000 var values = Set() - for _ in 0 ..< numTests { + for _ in 0.. String { - event - } - } - - let uuid = UUID().uuidString - let result = try await Lambda.test(MyLambda.self, with: uuid) - XCTAssertEqual(result, uuid) - } - - func testCodableClosure() async throws { - struct Request: Codable { - let name: String - } - - struct Response: Codable { - let message: String - } - - struct MyLambda: SimpleLambdaHandler { - func handle(_ event: Request, context: LambdaContext) async throws -> Response { - Response(message: "echo" + event.name) - } - } - - let request = Request(name: UUID().uuidString) - let response = try await Lambda.test(MyLambda.self, with: request) - XCTAssertEqual(response.message, "echo" + request.name) - } - - func testCodableVoidClosure() async throws { - struct Request: Codable { - let name: String - } - - struct MyLambda: SimpleLambdaHandler { - // DIRTY HACK: To verify the handler was actually invoked, we change a global variable. - static var VoidLambdaHandlerInvokeCount: Int = 0 - - func handle(_ event: Request, context: LambdaContext) async throws { - Self.VoidLambdaHandlerInvokeCount += 1 - } - } - - let request = Request(name: UUID().uuidString) - MyLambda.VoidLambdaHandlerInvokeCount = 0 - try await Lambda.test(MyLambda.self, with: request) - XCTAssertEqual(MyLambda.VoidLambdaHandlerInvokeCount, 1) - } - - func testInvocationFailure() async throws { - struct MyError: Error {} - - struct MyLambda: SimpleLambdaHandler { - func handle(_ event: String, context: LambdaContext) async throws { - throw MyError() - } - } - - do { - try await Lambda.test(MyLambda.self, with: UUID().uuidString) - XCTFail("expected to throw") - } catch { - XCTAssert(error is MyError) - } - } - - func testAsyncLongRunning() async throws { - struct MyLambda: SimpleLambdaHandler { - func handle(_ event: String, context: LambdaContext) async throws -> String { - try await Task.sleep(nanoseconds: 500 * 1000 * 1000) - return event - } - } - - let uuid = UUID().uuidString - let result = try await Lambda.test(MyLambda.self, with: uuid) - XCTAssertEqual(result, uuid) - } -} diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index ec97cef2..00000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -ARG swift_version=5.7 -ARG base_image=swift:$swift_version-amazonlinux2 -FROM $base_image -# needed to do again after FROM due to docker limitation -ARG swift_version - -# dependencies -RUN yum install -y wget perl-Digest-SHA -RUN yum install -y lsof dnsutils netcat-openbsd net-tools curl jq # used by integration tests - -# tools -RUN mkdir -p $HOME/.tools -RUN echo 'export PATH="$HOME/.tools:$PATH"' >> $HOME/.profile - -# swiftformat (until part of the toolchain) - -ARG swiftformat_version=0.50.1 -RUN git clone --branch $swiftformat_version --depth 1 https://github.com/nicklockwood/SwiftFormat $HOME/.tools/swift-format -RUN cd $HOME/.tools/swift-format && swift build -c release -RUN ln -s $HOME/.tools/swift-format/.build/release/swiftformat $HOME/.tools/swiftformat diff --git a/docker/docker-compose.al2.510.yaml b/docker/docker-compose.al2.510.yaml deleted file mode 100644 index a897f987..00000000 --- a/docker/docker-compose.al2.510.yaml +++ /dev/null @@ -1,18 +0,0 @@ -version: "3" - -services: - - runtime-setup: - image: swift-aws-lambda:al2-5.10 - build: - args: - base_image: "swiftlang/swift:nightly-5.10-amazonlinux2" - - test: - image: swift-aws-lambda:al2-5.10 - - test-examples: - image: swift-aws-lambda:al2-5.10 - - shell: - image: swift-aws-lambda:al2-5.10 diff --git a/docker/docker-compose.al2.57.yaml b/docker/docker-compose.al2.57.yaml deleted file mode 100644 index 1b19f1f0..00000000 --- a/docker/docker-compose.al2.57.yaml +++ /dev/null @@ -1,18 +0,0 @@ -version: "3" - -services: - - runtime-setup: - image: swift-aws-lambda:al2-5.7 - build: - args: - swift_version: "5.7" - - test: - image: swift-aws-lambda:al2-5.7 - - test-examples: - image: swift-aws-lambda:al2-5.7 - - shell: - image: swift-aws-lambda:al2-5.7 diff --git a/docker/docker-compose.al2.58.yaml b/docker/docker-compose.al2.58.yaml deleted file mode 100644 index 6127c65c..00000000 --- a/docker/docker-compose.al2.58.yaml +++ /dev/null @@ -1,18 +0,0 @@ -version: "3" - -services: - - runtime-setup: - image: swift-aws-lambda:al2-5.8 - build: - args: - swift_version: "5.8" - - test: - image: swift-aws-lambda:al2-5.8 - - test-examples: - image: swift-aws-lambda:al2-5.8 - - shell: - image: swift-aws-lambda:al2-5.8 diff --git a/docker/docker-compose.al2.59.yaml b/docker/docker-compose.al2.59.yaml deleted file mode 100644 index edea9327..00000000 --- a/docker/docker-compose.al2.59.yaml +++ /dev/null @@ -1,18 +0,0 @@ -version: "3" - -services: - - runtime-setup: - image: swift-aws-lambda:al2-5.9 - build: - args: - swift_version: "5.9" - - test: - image: swift-aws-lambda:al2-5.9 - - test-examples: - image: swift-aws-lambda:al2-5.9 - - shell: - image: swift-aws-lambda:al2-5.9 diff --git a/docker/docker-compose.al2.main.yaml b/docker/docker-compose.al2.main.yaml deleted file mode 100644 index b2f890c1..00000000 --- a/docker/docker-compose.al2.main.yaml +++ /dev/null @@ -1,18 +0,0 @@ -version: "3" - -services: - - runtime-setup: - image: swift-aws-lambda:al2-main - build: - args: - base_image: "swiftlang/swift:nightly-main-amazonlinux2" - - test: - image: swift-aws-lambda:al2-main - - test-examples: - image: swift-aws-lambda:al2-main - - shell: - image: swift-aws-lambda:al2-main diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml deleted file mode 100644 index 32507dcf..00000000 --- a/docker/docker-compose.yaml +++ /dev/null @@ -1,51 +0,0 @@ -# this file is not designed to be run directly -# instead, use the docker-compose.. files -# eg docker-compose -f docker/docker-compose.yaml -f docker/docker-compose.al2.57.yaml run test -version: "3" - -services: - - runtime-setup: - image: swift-aws-lambda:default - build: - context: . - dockerfile: Dockerfile - - common: &common - image: swift-aws-lambda:default - depends_on: [runtime-setup] - volumes: - - ~/.ssh:/root/.ssh - - ..:/code:z - working_dir: /code - cap_drop: - - CAP_NET_RAW - - CAP_NET_BIND_SERVICE - - soundness: - <<: *common - command: /bin/bash -cl "./scripts/soundness.sh" - - test: - <<: *common - command: /bin/bash -cl "swift test -Xswiftc -warnings-as-errors $${SANITIZER_ARG-}" - - test-examples: - <<: *common - command: >- - /bin/bash -clx " - LAMBDA_USE_LOCAL_DEPS=true swift build --package-path Examples/Benchmark && - LAMBDA_USE_LOCAL_DEPS=true swift build --package-path Examples/Deployment && - LAMBDA_USE_LOCAL_DEPS=true swift build --package-path Examples/Echo && - LAMBDA_USE_LOCAL_DEPS=true swift build --package-path Examples/ErrorHandling && - LAMBDA_USE_LOCAL_DEPS=true swift build --package-path Examples/Foundation && - LAMBDA_USE_LOCAL_DEPS=true swift build --package-path Examples/JSON && - LAMBDA_USE_LOCAL_DEPS=true swift build --package-path Examples/LocalDebugging/MyLambda && - LAMBDA_USE_LOCAL_DEPS=true swift test --package-path Examples/Testing - " - - # util - - shell: - <<: *common - entrypoint: /bin/bash -l diff --git a/projects.md b/projects.md new file mode 100644 index 00000000..d6566d64 --- /dev/null +++ b/projects.md @@ -0,0 +1,13 @@ +# Projects using Swift AWS Lambda Runtime library + +Here you will find a list of code repositories that has in common the usage of the **Swift AWS Lambda Runtime** library. + +Provide a link to your code repository and a short description about your example. + +- [GlobantPlus](https://github.com/fitomad/TechTalk-AWS-Lamba-Swift/). An imaginary streaming services with a tvOS application that interacts with the AWS API Gateway, AWS SQS services and a set of Lambdas that cover different aspects. Repository's documentation available in Spanish🇪🇸 and English🇺🇸. +- [DocUploader](https://github.com/SwiftPackageIndex/DocUploader). DocUploader is a component of the Swift Package Index site. It receives zipped DocC documentation archives which can be quite large via an S3 inbox and proceeds to unzip and upload the documentation files to S3. +- [Vapor's PennyBot](https://github.com/vapor/penny-bot). A collections of lambdas that handle events from GitHub for automatically creating releases for Vapor, handling new sponsors, automatically answering questions on Discord and providing 'pennies' - internet karma - to community members who help others or contribute to Vapor. +- [Breeze](https://github.com/swift-serverless/Breeze) A Serverless API Template Generator for Server-Side Swift. It supports template generation, creating swift package and deployment scripts for: + - Serverless REST API based on AWS APIGateway, Lambda, DynamoDB + - GitHub Webhook + - Webhook diff --git a/readme.md b/readme.md index 402ac8d1..d3626d1a 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,17 @@ -# Swift AWS Lambda Runtime + +You can read [the Swift AWS Lambda Runtime documentation](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/2.0.0/documentation/awslambdaruntime) on the Swift Package Index. + +This guide contains the following sections: + +- [The Swift AWS Lambda Runtime](#the-swift-aws-lambda-runtime) +- [Pre-requisites](#pre-requisites) +- [Getting started](#getting-started) +- [Developing your Swift Lambda functions](#developing-your-swift-lambda-functions) +- [Testing Locally](#testing-locally) +- [Deploying your Swift Lambda functions](#deploying-your-swift-lambda-functions) +- [Swift AWS Lambda Runtime - Design Principles](#swift-aws-lambda-runtime---design-principles) + +## The Swift AWS Lambda Runtime Many modern systems have client components like iOS, macOS or watchOS applications as well as server components that those clients interact with. Serverless functions are often the easiest and most efficient way for client application developers to extend their applications into the cloud. @@ -10,859 +23,529 @@ Combine this with Swift's developer friendliness, expressiveness, and emphasis o 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://fabianfett.dev/getting-started-with-swift-aws-lambda-runtime) 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.7 - - import PackageDescription - - let package = Package( - name: "MyLambda", - products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), - ], - dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), - ], - targets: [ - .executableTarget(name: "MyLambda", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - ]), - ] - ) - ``` - -Next, create a `MyLambda.swift` and implement your Lambda. Note that the file can not be named `main.swift` or you will encounter the following error: `'main' attribute cannot be used in a module that contains top-level code`. +## Pre-requisites -### Using async function +- Ensure you have the Swift 6.x toolchain installed. You can [install Swift toolchains](https://www.swift.org/install/macos/) from Swift.org - The simplest way to use `AWSLambdaRuntime` is to use the `SimpleLambdaHandler` protocol and pass in an async function, for example: +- When developing on macOS, be sure you use macOS 15 (Sequoia) or a more recent macOS version. - ```swift - // Import the module - import AWSLambdaRuntime - - @main - struct MyLambda: SimpleLambdaHandler { - // in this example we are receiving and responding with strings - func handle(_ name: String, context: LambdaContext) async throws -> String { - "Hello, \(name)" - } - } - ``` +- To build and archive your Lambda function, you need to [install docker](https://docs.docker.com/desktop/install/mac-install/). - More commonly, the event would be a JSON, which is modeled using `Codable`, for example: +- To deploy the Lambda function and invoke it, you must have [an AWS account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-creating.html) and [install and configure the `aws` command line](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). - ```swift - // Import the module - import AWSLambdaRuntime +- Some examples are using [AWS SAM](https://aws.amazon.com/serverless/sam/). Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) before deploying these examples. - // Request, uses Codable for transparent JSON encoding - struct Request: Codable { - let name: String - } +## Getting started - // Response, uses Codable for transparent JSON encoding - struct Response: Codable { - let message: String - } +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. - @main - struct MyLambda: SimpleLambdaHandler { - // In this example we are receiving and responding with `Codable`. - func handle(_ request: Request, context: LambdaContext) async throws -> Response { - Response(message: "Hello, \(request.name)") - } - } - ``` +We also wrote a comprehensive [deployment guide](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/main/documentation/awslambdaruntime/deployment). - 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 a `SQS` message: +Or, if you're impatient to start with runtime v2, try these six steps: - First, add a dependency on the event packages: +The `Examples/_MyFirstFunction` contains a script that goes through the steps described in this section. - ```swift - // swift-tools-version:5.7 - - import PackageDescription - - let package = Package( - name: "MyLambda", - products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), - ], - dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), - .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", branch: "main"), - ], - targets: [ - .executableTarget(name: "MyLambda", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), - ]), - ] - ) - ``` - - Then in your Lambda: +If you are really impatient, just type: - ```swift - // Import the modules - import AWSLambdaRuntime - import AWSLambdaEvents - - @main - struct MyLambda: SimpleLambdaHandler { - // In this example we are receiving a SQS Event, with no response (Void). - func handle(_ event: SQSEvent, context: LambdaContext) async throws { - ... - } - } - ``` - - In some cases, the Lambda needs to do work on initialization. - In such cases, use the `LambdaHandler` instead of the `SimpleLambdaHandler` which has an additional initialization method. For example: +```bash +cd Examples/_MyFirstFunction +./create_and_deploy_function.sh +``` - ```swift - import AWSLambdaRuntime +Otherwise, continue reading. - @main - struct MyLambda: LambdaHandler { - init(context: LambdaInitializationContext) async throws { - ... - } +1. Create a new Swift executable project - func handle(_ event: String, context: LambdaContext) async throws -> Void { - ... - } - } - ``` +```bash +mkdir MyLambda && cd MyLambda +swift package init --type executable +``` - Modeling Lambda functions as async functions 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 hang, 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. +2. Prepare your `Package.swift` file -### Using EventLoopLambdaHandler + 2.1 Add the Swift AWS Lambda Runtime as a dependency - Performance sensitive Lambda functions may choose to use a more complex API which allows user code to run on the same thread as the networking handlers. Swift AWS Lambda Runtime uses [SwiftNIO](https://github.com/apple/swift-nio) as its underlying networking engine which means the APIs are based on [SwiftNIO](https://github.com/apple/swift-nio) concurrency primitives like the `EventLoop` and `EventLoopFuture`. For example: + ```bash + swift package add-dependency https://github.com/swift-server/swift-aws-lambda-runtime.git --from 2.0.0 + swift package add-target-dependency AWSLambdaRuntime MyLambda --package swift-aws-lambda-runtime --from 1.0.0 + ``` - ```swift - // Import the modules - import AWSLambdaRuntime - import AWSLambdaEvents - import NIOCore - - @main - struct Handler: EventLoopLambdaHandler { - typealias Event = SNSEvent.Message // Event / Request type - typealias Output = Void // Output / Response type - - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture(Self()) - } - - // `EventLoopLambdaHandler` does not offload the Lambda processing to a separate thread - // while the closure-based handlers do. - func handle(_ event: Event, context: LambdaContext) -> EventLoopFuture { - ... - context.eventLoop.makeSucceededFuture(Void()) - } - } - ``` - - Beyond the small cognitive complexity of using the `EventLoopFuture` based APIs, note these APIs should be used with extra care. An `EventLoopLambdaHandler` will execute the user code on the same `EventLoop` (thread) as the library, making processing faster but requiring the user code to never call blocking APIs as it might prevent the underlying process from functioning. - -## 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. - -Swift AWS Lambda Runtime includes a SwiftPM plugin designed to help with the creation of the zip archive. -To build and package your Lambda, run the following command: - - ```shell - swift package archive - ``` - -The `archive` command can be customized using the following parameters - -* `--output-path` A valid file system path where a folder with the archive operation result will be placed. This folder will contains the following elements: - * A file link named `bootstrap` - * An executable file - * A **Zip** file ready to be upload to AWS -* `--verbose` A number that sets the command output detail level between the following values: - * `0` (Silent) - * `1` (Output) - * `2` (Debug) -* `--swift-version` Swift language version used to define the Amazon Linux 2 Docker image. For example "5.7.3" -* `--base-docker-image` An Amazon Linux 2 docker image name available in your system. -* `--disable-docker-image-update` If flag is set, docker image will not be updated and local image will be used. - -Both `--swift-version` and `--base-docker-image` are mutually exclusive - -Here's an example - -```zsh -swift package archive --output-path /Users/JohnAppleseed/Desktop --verbose 2 -``` + 2.2 (Optional - only on macOS) Add `platforms` after `name` -This command execution will generate a folder at `/Users/JohnAppleseed/Desktop` with the lambda zipped and ready to upload it and set the command detail output level to `2` (debug) + ``` + platforms: [.macOS(.v15)], + ``` - on macOS, the archiving plugin uses docker to build the Lambda for Amazon Linux 2, and as such requires to communicate with Docker over the localhost network. - At the moment, SwiftPM does not allow plugin communication over network, and as such the invocation requires breaking from the SwiftPM plugin sandbox. This limitation would be removed in the future. - - + 2.3 Your `Package.swift` file must look like this -```shell - swift package --disable-sandbox archive - ``` + ```swift + // swift-tools-version: 6.0 -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/). The [Examples Directory](/Examples) includes complete sample build and deployment scripts that utilize these tools. + import PackageDescription -Note the examples mentioned above use dynamic linking, therefore bundle the required Swift libraries in the Zip package along side the executable. You may choose to link the Lambda function statically (using `-static-stdlib`) which could improve performance but requires additional linker flags. + let package = Package( + name: "MyLambda", + platforms: [.macOS(.v15)], + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "2.0.0"), + ], + targets: [ + .executableTarget( + name: "MyLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + ] + ), + ] + ) + ``` -To build the Lambda function for Amazon Linux 2, use the Docker image published by Swift.org on [Swift toolchains and Docker images for Amazon Linux 2](https://swift.org/download/), as demonstrated in the examples. +3. Scaffold a minimal Lambda function -## Architecture +The runtime comes with a plugin to generate the code of a simple AWS Lambda function: -The library defines four protocols for the implementation of a Lambda Handler. From low-level to more convenient: +```bash +swift package lambda-init --allow-writing-to-package-directory +``` -### ByteBufferLambdaHandler +Your `Sources/main.swift` file must look like this. -An `EventLoopFuture` based processing protocol for a Lambda that takes a `ByteBuffer` and returns a `ByteBuffer?` asynchronously. +```swift +import AWSLambdaRuntime -`ByteBufferLambdaHandler` is the lowest level protocol designed to power the higher level `EventLoopLambdaHandler` and `LambdaHandler` 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. +// in this example we are receiving and responding with strings -```swift -public protocol ByteBufferLambdaHandler { - /// Create a Lambda handler for the runtime. - /// - /// Use this to initialize all your resources that you want to cache between invocations. This could be database - /// connections and HTTP clients for example. It is encouraged to use the given `EventLoop`'s conformance - /// to `EventLoopGroup` when initializing NIO dependencies. This will improve overall performance, as it - /// minimizes thread hopping. - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture - - /// The Lambda handling method. - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - context: Runtime ``LambdaContext``. - /// - event: The event or input 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(_ buffer: ByteBuffer, context: LambdaContext) -> EventLoopFuture +let runtime = LambdaRuntime { + (event: String, context: LambdaContext) in + return String(event.reversed()) } -``` -### EventLoopLambdaHandler - -`EventLoopLambdaHandler` is a strongly typed, `EventLoopFuture` based asynchronous processing protocol for a Lambda that takes a user defined `Event` and returns a user defined `Output`. +try await runtime.run() +``` -`EventLoopLambdaHandler` provides `ByteBuffer` -> `Event` decoding and `Output` -> `ByteBuffer?` encoding for `Codable` and `String`. +4. Build & archive the package -`EventLoopLambdaHandler` 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 it designed for performance sensitive applications that use `Codable` or `String` based Lambda functions. +The runtime comes with a plugin to compile on Amazon Linux and create a ZIP archive: -```swift -public protocol EventLoopLambdaHandler { - /// The lambda functions input. In most cases this should be `Codable`. If your event originates from an - /// AWS service, have a look at [AWSLambdaEvents](https://github.com/swift-server/swift-aws-lambda-events), - /// which provides a number of commonly used AWS Event implementations. - associatedtype Event - /// The lambda functions output. Can be `Void`. - associatedtype Output - - /// Create a Lambda handler for the runtime. - /// - /// Use this to initialize all your resources that you want to cache between invocations. This could be database - /// connections and HTTP clients for example. It is encouraged to use the given `EventLoop`'s conformance - /// to `EventLoopGroup` when initializing NIO dependencies. This will improve overall performance, as it - /// minimizes thread hopping. - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture - - /// The Lambda handling method. - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - context: Runtime ``LambdaContext``. - /// - event: Event of type `Event` 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 ``Output`` or an `Error`. - func handle(_ event: Event, context: LambdaContext) -> EventLoopFuture - - /// Encode a response of type ``Output`` to `ByteBuffer`. - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - parameters: - /// - value: Response of type ``Output``. - /// - buffer: A `ByteBuffer` to encode into, will be overwritten. - /// - /// - Returns: A `ByteBuffer` with the encoded version of the `value`. - func encode(value: Output, into buffer: inout ByteBuffer) throws - - /// Decode a `ByteBuffer` to a request or event of type ``Event``. - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - /// - parameters: - /// - buffer: The `ByteBuffer` to decode. - /// - /// - Returns: A request or event of type ``Event``. - func decode(buffer: ByteBuffer) throws -> Event -} +```bash +swift package archive --allow-network-connections docker ``` -### LambdaHandler +If there is no error, the ZIP archive is ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip` -`LambdaHandler` is a strongly typed, completion handler based asynchronous processing protocol for a Lambda that takes a user defined `Event` and returns a user defined `Output`. +5. Deploy to AWS -`LambdaHandler` provides `ByteBuffer` -> `Event` decoding and `Output` -> `ByteBuffer` encoding for `Codable` and `String`. +There are multiple ways to deploy to AWS ([SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html), [Terraform](https://developer.hashicorp.com/terraform/tutorials/aws-get-started), [AWS Cloud Development Kit (CDK)](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html), [AWS Console](https://docs.aws.amazon.com/lambda/latest/dg/getting-started.html)) that are covered later in this doc. -`LambdaHandler` offloads the user provided Lambda execution to an async task making processing safer but slightly slower. +Here is how to deploy using the `aws` command line. -```swift -public protocol LambdaHandler { - /// The lambda function's input. In most cases this should be `Codable`. If your event originates from an - /// AWS service, have a look at [AWSLambdaEvents](https://github.com/swift-server/swift-aws-lambda-events), - /// which provides a number of commonly used AWS Event implementations. - associatedtype Event - /// The lambda function's output. Can be `Void`. - associatedtype Output - - /// The Lambda initialization method. - /// Use this method to initialize resources that will be used in every request. - /// - /// Examples for this can be HTTP or database clients. - /// - parameters: - /// - context: Runtime ``LambdaInitializationContext``. - init(context: LambdaInitializationContext) async throws - - /// The Lambda handling method. - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - event: Event of type `Event` representing the event or request. - /// - context: Runtime ``LambdaContext``. - /// - /// - Returns: A Lambda result ot type `Output`. - func handle(_ event: Event, context: LambdaContext) async throws -> Output - - /// Encode a response of type ``Output`` to `ByteBuffer`. - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - parameters: - /// - value: Response of type ``Output``. - /// - buffer: A `ByteBuffer` to encode into, will be overwritten. - /// - /// - Returns: A `ByteBuffer` with the encoded version of the `value`. - func encode(value: Output, into buffer: inout ByteBuffer) throws - - /// Decode a `ByteBuffer` to a request or event of type ``Event``. - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - /// - parameters: - /// - buffer: The `ByteBuffer` to decode. - /// - /// - Returns: A request or event of type ``Event``. - func decode(buffer: ByteBuffer) throws -> Event -} +```bash +aws lambda create-function \ +--function-name MyLambda \ +--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip \ +--runtime provided.al2 \ +--handler provided \ +--architectures arm64 \ +--role arn:aws:iam:::role/lambda_basic_execution ``` -### SimpleLambdaHandler +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`. -`SimpleLambdaHandler` is a strongly typed, completion handler based asynchronous processing protocol for a Lambda that takes a user defined `Event` and returns a user defined `Output`. +Be sure to replace with your actual AWS account ID (for example: 012345678901). -`SimpleLambdaHandler` provides `ByteBuffer` -> `Event` decoding and `Output` -> `ByteBuffer` encoding for `Codable` and `String`. +> [!IMPORTANT] +> Before creating a function, you need to have a `lambda_basic_execution` IAM role in your AWS account. +> +> You can create this role in two ways: +> 1. Using AWS Console +> 2. Running the commands in the `create_lambda_execution_role()` function in [`Examples/_MyFirstFunction/create_iam_role.sh`](https://github.com/swift-server/swift-aws-lambda-runtime/blob/8dff649920ab0c66bb039d15ae48d9d5764db71a/Examples/_MyFirstFunction/create_and_deploy_function.sh#L40C1-L40C31) -`SimpleLambdaHandler` is the same as `LambdaHandler`, but does not require explicit initialization . +6. Invoke your Lambda function -```swift -public protocol SimpleLambdaHandler { - /// The lambda function's input. In most cases this should be `Codable`. If your event originates from an - /// AWS service, have a look at [AWSLambdaEvents](https://github.com/swift-server/swift-aws-lambda-events), - /// which provides a number of commonly used AWS Event implementations. - associatedtype Event - /// The lambda function's output. Can be `Void`. - associatedtype Output - - init() - - /// The Lambda handling method. - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - event: Event of type `Event` representing the event or request. - /// - context: Runtime ``LambdaContext``. - /// - /// - Returns: A Lambda result ot type `Output`. - func handle(_ event: Event, context: LambdaContext) async throws -> Output - - /// Encode a response of type ``Output`` to `ByteBuffer`. - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - parameters: - /// - value: Response of type ``Output``. - /// - buffer: A `ByteBuffer` to encode into, will be overwritten. - /// - /// - Returns: A `ByteBuffer` with the encoded version of the `value`. - func encode(value: Output, into buffer: inout ByteBuffer) throws - - /// Decode a `ByteBuffer` to a request or event of type ``Event``. - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - /// - parameters: - /// - buffer: The `ByteBuffer` to decode. - /// - /// - Returns: A request or event of type ``Event``. - func decode(buffer: ByteBuffer) throws -> Event -} +```bash +aws lambda invoke \ +--function-name MyLambda \ +--payload $(echo \"Hello World\" | base64) \ +out.txt && cat out.txt && rm out.txt ``` -### Context - -When calling the user provided Lambda function, the library provides a `LambdaContext` class that provides metadata about the execution context, as well as utilities for logging and allocating buffers. +This should print -```swift -public struct LambdaContext: CustomDebugStringConvertible, Sendable { - /// The request ID, which identifies the request that triggered the function invocation. - public var requestID: String { - self.storage.requestID - } - - /// The AWS X-Ray tracing header. - public var traceID: String { - self.storage.traceID - } - - /// The ARN of the Lambda function, version, or alias that's specified in the invocation. - public var invokedFunctionARN: String { - self.storage.invokedFunctionARN - } - - /// The timestamp that the function times out. - public var deadline: DispatchWallTime { - self.storage.deadline - } - - /// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider. - public var cognitoIdentity: String? { - self.storage.cognitoIdentity - } - - /// For invocations from the AWS Mobile SDK, data about the client application and device. - public var clientContext: String? { - self.storage.clientContext - } - - /// `Logger` to log with. - /// - /// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable. - public var logger: Logger { - self.storage.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 var eventLoop: EventLoop { - self.storage.eventLoop - } - - /// `ByteBufferAllocator` to allocate `ByteBuffer`. - /// This is useful when implementing ``EventLoopLambdaHandler``. - public var allocator: ByteBufferAllocator { - self.storage.allocator - } -} ``` - -Similarally, the library provides a context if and when initializing the Lambda. - -```swift -public struct LambdaInitializationContext: Sendable { - /// `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. - /// - /// - 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`. - public let allocator: ByteBufferAllocator - - /// ``LambdaTerminator`` to register shutdown operations. - public let terminator: LambdaTerminator +{ + "StatusCode": 200, + "ExecutedVersion": "$LATEST" } +"dlroW olleH" ``` -### 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: +## Developing your Swift Lambda functions -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 `Event` 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 `Event` event to the user provided handler. In the case of `LambdaHandler` 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 `Output` 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 `Output` 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. +### Receive and respond with JSON objects -The library encapsulates the workflow via the internal `LambdaRuntimeClient` and `LambdaRunner` structs respectively. +Typically, your Lambda functions will receive an input parameter expressed as JSON and will respond with some other JSON. The Swift AWS Lambda runtime automatically takes care of encoding and decoding JSON objects when your Lambda function handler accepts `Decodable` and returns `Encodable` conforming types. -### Lifecycle Management +Here is an example of a minimal function that accepts a JSON object as input and responds with another JSON object. -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. +```swift +import AWSLambdaRuntime -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. +// the data structure to represent the input parameter +struct HelloRequest: Decodable { + let name: String + let age: Int +} -### Integration with AWS Platform Events +// the data structure to represent the output response +struct HelloResponse: Encodable { + let greetings: String +} -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 events 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. +// the Lambda runtime +let runtime = LambdaRuntime { + (event: HelloRequest, context: LambdaContext) in -## Performance + HelloResponse( + greetings: "Hello \(event.name). You look \(event.age > 30 ? "younger" : "older") than your age." + ) +} -Lambda functions performance is usually measured across two axes: +// start the loop +try await runtime.run() +``` -- **Cold start times**: The time it takes for a Lambda function to startup, ask for an invocation and process the first invocation. +You can learn how to deploy and invoke this function in [the Hello JSON example README file](Examples/HelloJSON/README.md). -- **Warm invocation times**: The time it takes for a Lambda function to process an invocation after the Lambda has been invoked at least once. +### Lambda Streaming Response -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. +You can configure your Lambda function to stream response payloads back to clients. Response streaming can benefit latency sensitive applications by improving time to first byte (TTFB) performance. This is because you can send partial responses back to the client as they become available. Additionally, you can use response streaming to build functions that return larger payloads. Response stream payloads have a soft limit of 200 MB as compared to the 6 MB limit for buffered responses. Streaming a response also means that your function doesn’t need to fit the entire response in memory. For very large responses, this can reduce the amount of memory you need to configure for your function. -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. +Streaming responses incurs a cost. For more information, see [AWS Lambda Pricing](https://aws.amazon.com/lambda/pricing/). -## Security +You can stream responses through [Lambda function URLs](https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html), the AWS SDK, or using the Lambda [InvokeWithResponseStream](https://docs.aws.amazon.com/lambda/latest/dg/API_InvokeWithResponseStream.html) API. In this example, we create an authenticated Lambda function URL. -Please see [SECURITY.md](SECURITY.md) for details on the security process. +#### Simple Streaming Example -## Project status +Here is an example of a minimal function that streams 10 numbers with an interval of one second for each number. -This is a community-driven open-source project actively seeking contributions. -There are several areas which need additional attention, including but not limited to: +```swift +import AWSLambdaRuntime +import NIOCore + +struct SendNumbersWithPause: StreamingLambdaHandler { + func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + for i in 1...10 { + // Send partial data + try await responseWriter.write(ByteBuffer(string: "\(i)\n")) + // Perform some long asynchronous work + try await Task.sleep(for: .milliseconds(1000)) + } + // All data has been sent. Close off the response stream. + try await responseWriter.finish() + } +} -* Further performance tuning -* Additional documentation and best practices -* Additional examples +let runtime = LambdaRuntime.init(handler: SendNumbersWithPause()) +try await runtime.run() +``` ---- -# Version 0.x (previous version) documentation ---- +#### Streaming with HTTP Headers and Status Code -## Getting started +When streaming responses, you can also set HTTP status codes and headers before sending the response body. This is particularly useful when your Lambda function is invoked through API Gateway or Lambda function URLs, allowing you to control the HTTP response metadata. -If you have never used AWS Lambda or Docker before, check out this [getting started guide](https://fabianfett.de/getting-started-with-swift-aws-lambda-runtime) which helps you with every step from zero to a running Lambda. +```swift +import AWSLambdaRuntime +import NIOCore + +struct StreamingWithHeaders: StreamingLambdaHandler { + func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + // Set HTTP status code and headers before streaming the body + let response = StreamingLambdaStatusAndHeadersResponse( + statusCode: 200, + headers: [ + "Content-Type": "text/plain", + "Cache-Control": "no-cache" + ] + ) + try await responseWriter.writeStatusAndHeaders(response) + + // Now stream the response body + for i in 1...5 { + try await responseWriter.write(ByteBuffer(string: "Chunk \(i)\n")) + try await Task.sleep(for: .milliseconds(500)) + } + + try await responseWriter.finish() + } +} -First, create a SwiftPM project and pull Swift AWS Lambda Runtime as dependency into your project +let runtime = LambdaRuntime.init(handler: StreamingWithHeaders()) +try await runtime.run() +``` - ```swift - // swift-tools-version:5.6 +The `writeStatusAndHeaders` method allows you to: +- Set HTTP status codes (200, 404, 500, etc.) +- Add custom HTTP headers for content type, caching, CORS, etc. +- Control response metadata before streaming begins +- Maintain compatibility with API Gateway and Lambda function URLs - import PackageDescription +You can learn how to deploy and invoke this function in [the streaming example README file](Examples/Streaming/README.md). - 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"), - ]), - ] - ) - ``` +### Lambda Streaming Response with JSON Input -Next, create a `main.swift` and implement your Lambda. +The Swift AWS Lambda Runtime also provides a convenient interface that combines the benefits of JSON input decoding with response streaming capabilities. This is ideal when you want to receive strongly-typed JSON events while maintaining the ability to stream responses and execute background work. -### Using Closures +Here is an example of a function that receives a JSON event and streams multiple responses: -The simplest way to use `AWSLambdaRuntime` is to pass in a closure, for example: +```swift +import AWSLambdaRuntime +import NIOCore + +// Define your input event structure +struct StreamingRequest: Decodable { + let count: Int + let message: String + let delayMs: Int? +} - ```swift - // Import the module - import AWSLambdaRuntime +// Use the new streaming handler with JSON decoding +let runtime = LambdaRuntime { (event: StreamingRequest, responseWriter, context: LambdaContext) in + context.logger.info("Received request to send \(event.count) messages") + + // Stream the messages + for i in 1...event.count { + let response = "Message \(i)/\(event.count): \(event.message)\n" + try await responseWriter.write(ByteBuffer(string: response)) + + // Optional delay between messages + if let delay = event.delayMs, delay > 0 { + try await Task.sleep(for: .milliseconds(delay)) + } + } + + // Finish the stream + try await responseWriter.finish() + + // Optional: Execute background work after response is sent + context.logger.info("Background work: processing completed") +} - // in this example we are receiving and responding with strings - Lambda.run { (context, name: String, callback: @escaping (Result) -> Void) in - callback(.success("Hello, \(name)")) - } - ``` +try await runtime.run() +``` - More commonly, the event would be a JSON, which is modeled using `Codable`, for example: +This interface provides: +- **Type-safe JSON input**: Automatic decoding of JSON events into Swift structs +- **Streaming responses**: Full control over when and how to stream data back to clients +- **Background work support**: Ability to execute code after the response stream is finished +- **Familiar API**: Uses the same closure-based pattern as regular Lambda handlers - ```swift - // Import the module - import AWSLambdaRuntime +You can learn how to deploy and invoke this function in [the streaming codable example README file](Examples/StreamingFromEvent/README.md). - // Request, uses Codable for transparent JSON encoding - private struct Request: Codable { - let name: String - } +### Integration with AWS Services - // Response, uses Codable for transparent JSON encoding - private struct Response: Codable { - let message: String - } + Most Lambda functions are triggered by events originating in other AWS services such as `Amazon SNS`, `Amazon SQS` or `AWS APIGateway`. - // 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)"))) - } - ``` + 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. - 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: +> [!IMPORTANT] +> This library has no dependencies on the AWS Lambda Events library. It is safe to use AWS Lambda v1.x with this Lambda Runtime v2. -First, add a dependency on the event packages: + Here is an example Lambda function invoked when the AWS APIGateway receives an HTTP request. ```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"), - ]), - ] - ) - ``` +import AWSLambdaEvents +import AWSLambdaRuntime +let runtime = LambdaRuntime { + (event: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response in - ```swift - // Import the modules - import AWSLambdaRuntime - import AWSLambdaEvents + APIGatewayV2Response(statusCode: .ok, body: "Hello World!") +} - // 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())) - } - ``` +try await runtime.run() +``` - 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 hang, 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. + You can learn how to deploy and invoke this function in [the API Gateway example README file](Examples/APIGateway/README.md). -### Using EventLoopLambdaHandler +### Integration with Swift Service LifeCycle - Performance sensitive Lambda functions may choose to use a more complex API which allows user code to run on the same thread as the networking handlers. Swift AWS Lambda Runtime uses [SwiftNIO](https://github.com/apple/swift-nio) as its underlying networking engine which means the APIs are based on [SwiftNIO](https://github.com/apple/swift-nio) concurrency primitives like the `EventLoop` and `EventLoopFuture`. For example: +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. - ```swift - // Import the modules - import AWSLambdaRuntime - import AWSLambdaEvents - import NIO +### Use Lambda Background Tasks - // Our Lambda handler, conforms to EventLoopLambdaHandler - struct Handler: EventLoopLambdaHandler { - typealias In = SNS.Message // Request type - typealias Out = Void // Response type +Background tasks allow code to execute asynchronously after the main response has been returned, enabling additional processing without affecting response latency. This approach is ideal for scenarios like logging, data updates, or notifications that can be deferred. The code leverages Lambda's "Response Streaming" feature, which is effective for balancing real-time user responsiveness with the ability to perform extended tasks post-response. For more information about Lambda background tasks, see [this AWS blog post](https://aws.amazon.com/blogs/compute/running-code-after-returning-a-response-from-an-aws-lambda-function/). - // In this example we are receiving an SNS Message, with no response (Void). - func handle(context: Lambda.Context, event: In) -> EventLoopFuture { - ... - context.eventLoop.makeSucceededFuture(Void()) - } - } - Lambda.run(Handler()) - ``` +Here is an example of a minimal function that waits 10 seconds after it returned a response but before the handler returns. +```swift +import AWSLambdaRuntime +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +struct BackgroundProcessingHandler: LambdaWithBackgroundProcessingHandler { + struct Input: Decodable { + let message: String + } + + struct Greeting: Encodable { + let echoedMessage: String + } + + typealias Event = Input + typealias Output = Greeting + + func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + // Return result to the Lambda control plane + context.logger.debug("BackgroundProcessingHandler - message received") + try await outputWriter.write(Greeting(echoedMessage: event.message)) + + // Perform some background work, e.g: + context.logger.debug("BackgroundProcessingHandler - response sent. Performing background tasks.") + try await Task.sleep(for: .seconds(10)) + + // Exit the function. All asynchronous work has been executed before exiting the scope of this function. + // Follows structured concurrency principles. + context.logger.debug("BackgroundProcessingHandler - Background tasks completed. Returning") + return + } +} - Beyond the small cognitive complexity of using the `EventLoopFuture` based APIs, note these APIs should be used with extra care. An `EventLoopLambdaHandler` will execute the user code on the same `EventLoop` (thread) as the library, making processing faster but requiring the user code to never call blocking APIs as it might prevent the underlying process from functioning. +let adapter = LambdaCodableAdapter(handler: BackgroundProcessingHandler()) +let runtime = LambdaRuntime.init(handler: adapter) +try await runtime.run() +``` -## Deploying to AWS Lambda +You can learn how to deploy and invoke this function in [the background tasks example README file](Examples/BackgroundTasks/README.md). -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. +## Testing Locally -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/). The [Examples Directory](/Examples) includes complete sample build and deployment scripts that utilize these tools. +Before deploying your code to AWS Lambda, you can test it locally by running the executable target on your local machine. It will look like this on CLI: -Note the examples mentioned above use dynamic linking, therefore bundle the required Swift libraries in the Zip package along side the executable. You may choose to link the Lambda function statically (using `-static-stdlib`) which could improve performance but requires additional linker flags. +```sh +swift run +``` -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/), as demonstrated in the examples. +When not running inside a Lambda execution environment, it starts a local HTTP server listening on port 7000. You can invoke your local Lambda function by sending an HTTP POST request to `http://127.0.0.1:7000/invoke`. + +The request must include the JSON payload expected as an `event` by your function. You can create a text file with the JSON payload documented by AWS or captured from a trace. In this example, we used [the APIGatewayv2 JSON payload from the documentation](https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html#apigateway-example-event), saved as `events/create-session.json` text file. + +Then we use curl to invoke the local endpoint with the test JSON payload. + +```sh +curl -v --header "Content-Type:\ application/json" --data @events/create-session.json http://127.0.0.1:7000/invoke +* Trying 127.0.0.1:7000... +* Connected to 127.0.0.1 (127.0.0.1) port 7000 +> POST /invoke HTTP/1.1 +> Host: 127.0.0.1:7000 +> User-Agent: curl/8.4.0 +> Accept: */* +> Content-Type:\ application/json +> Content-Length: 1160 +> +< HTTP/1.1 200 OK +< content-length: 247 +< +* Connection #0 to host 127.0.0.1 left intact +{"statusCode":200,"isBase64Encoded":false,"body":"...","headers":{"Access-Control-Allow-Origin":"*","Content-Type":"application\/json; charset=utf-8","Access-Control-Allow-Headers":"*"}} +``` +### Modifying the local server URI -## Architecture +By default, when using the local Lambda server during your tests, it listens on `http://127.0.0.1:7000/invoke`. -The library defines three protocols for the implementation of a Lambda Handler. From low-level to more convenient: +Some testing tools, such as the [AWS Lambda runtime interface emulator](https://docs.aws.amazon.com/lambda/latest/dg/images-test.html), require a different endpoint, the port might be used, or you may want to bind a specific IP address. -### ByteBufferLambdaHandler +In these cases, you can use three environment variables to control the local server: -An `EventLoopFuture` based processing protocol for a Lambda that takes a `ByteBuffer` and returns a `ByteBuffer?` asynchronously. +- Set `LOCAL_LAMBDA_HOST` to configure the local server to listen on a different TCP address. +- Set `LOCAL_LAMBDA_PORT` to configure the local server to listen on a different TCP port. +- Set `LOCAL_LAMBDA_INVOCATION_ENDPOINT` to force the local server to listen on a different endpoint. -`ByteBufferLambdaHandler` is the lowest level protocol designed to power the higher level `EventLoopLambdaHandler` and `LambdaHandler` 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. +Example: -```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 -} +```sh +LOCAL_LAMBDA_PORT=8080 LOCAL_LAMBDA_INVOCATION_ENDPOINT=/2015-03-31/functions/function/invocations swift run ``` -### EventLoopLambdaHandler +## Deploying your Swift Lambda functions -`EventLoopLambdaHandler` is a strongly typed, `EventLoopFuture` based asynchronous processing protocol for a Lambda that takes a user defined `In` and returns a user defined `Out`. +There is a full deployment guide available in [the documentation](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/main/documentation/awslambdaruntime/deployment). -`EventLoopLambdaHandler` extends `ByteBufferLambdaHandler`, providing `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer?` encoding for `Codable` and `String`. +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. -`EventLoopLambdaHandler` 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 it designed for performance sensitive applications that use `Codable` or `String` based Lambda functions. +To take full advantage of the cloud, we recommend using Infrastructure as Code (IaC) tools like the [AWS Serverless Application Model (SAM)](https://aws.amazon.com/serverless/sam/) or [AWS Cloud Development Kit (CDK)](https://aws.amazon.com/cdk/). These tools allow you to define your infrastructure and deployment process as code, which can be version-controlled and automated. -```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 -} -``` +Alternatively, you might also consider using popular third-party tools like [Serverless Framework](https://www.serverless.com/), [Terraform](https://www.terraform.io/), or [Pulumi](https://www.pulumi.com/) to deploy Lambda functions and create and manage AWS infrastructure. -### LambdaHandler +Here is a short example that shows how to deploy using SAM. -`LambdaHandler` 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`. +**Prerequisites** : Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) -`LambdaHandler` extends `ByteBufferLambdaHandler`, performing `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer` encoding for `Codable` and `String`. +When using SAM, you describe your deployment in a YAML text file. +The [API Gateway example directory](Examples/APIGateway/template.yaml) contains a file named `template.yaml` that you can use as a starting point. -`LambdaHandler` offloads the user provided Lambda execution to a `DispatchQueue` making processing safer but slower. +To deploy your Lambda function and create the infrastructure, type the following `sam` command. -```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) -} +```bash +sam deploy \ +--resolve-s3 \ +--template-file template.yaml \ +--stack-name APIGatewayLambda \ +--capabilities CAPABILITY_IAM ``` -### 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` 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. +At the end of the deployment, the script lists the API Gateway endpoint. +The output is similar to this one. -```swift -public typealias CodableClosure = (Lambda.Context, In, @escaping (Result) -> Void) -> Void ``` - -```swift -public typealias StringClosure = (Lambda.Context, String, @escaping (Result) -> Void) -> Void +----------------------------------------------------------------------------------------------------------------------------- +Outputs +----------------------------------------------------------------------------------------------------------------------------- +Key APIGatewayEndpoint +Description API Gateway endpoint URL" +Value https://a5q74es3k2.execute-api.us-east-1.amazonaws.com +----------------------------------------------------------------------------------------------------------------------------- ``` -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. +Please refer to the full deployment guide available in [the documentation](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/main/documentation/awslambdaruntime) for more details. -### Context +## Swift AWS Lambda Runtime - Design Principles -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. +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. -```swift -public final class Context { - /// The request ID, which identifies the request that triggered the function invocation. - public let requestID: String +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). - /// The AWS X-Ray tracing header. - public let traceID: String +### Key Design Principles - /// The ARN of the Lambda function, version, or alias that's specified in the invocation. - public let invokedFunctionARN: String +The v2 API prioritizes the following principles: - /// The timestamp that the function times out - public let deadline: DispatchWallTime +- Readability and Maintainability: Extensive use of `async`/`await` improves code clarity and simplifies maintenance. - /// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider. - public let cognitoIdentity: String? +- Developer Control: Developers own the `main()` function and have the flexibility to inject dependencies into the `LambdaRuntime`. This allows you to manage service lifecycles efficiently using [Swift Service Lifecycle](https://github.com/swift-server/swift-service-lifecycle) for structured concurrency. - /// For invocations from the AWS Mobile SDK, data about the client application and device. - public let clientContext: String? +- Simplified Codable Support: The `LambdaCodableAdapter` struct eliminates the need for verbose boilerplate code when encoding and decoding events and responses. - /// `Logger` to log with - /// - /// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable. - public let logger: Logger +### New Capabilities - /// 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 +The v2 API introduces two new features: - /// `ByteBufferAllocator` to allocate `ByteBuffer` - /// This is useful when implementing `EventLoopLambdaHandler` - public let allocator: ByteBufferAllocator -} -``` +[Response Streaming](https://aws.amazon.com/blogs/compute/introducing-aws-lambda-response-streaming/): This functionality is ideal for handling large responses that need to be sent incrementally.   + +[Background Work](https://aws.amazon.com/blogs/compute/running-code-after-returning-a-response-from-an-aws-lambda-function/): Schedule tasks to run after returning a response to the AWS Lambda control plane. + +These new capabilities provide greater flexibility and control when building serverless functions in Swift with the swift-aws-lambda-runtime library. diff --git a/scripts/check-doc.sh b/scripts/check-doc.sh new file mode 100755 index 00000000..ab27bd60 --- /dev/null +++ b/scripts/check-doc.sh @@ -0,0 +1,72 @@ +#!/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 +## +##===----------------------------------------------------------------------===## +##===----------------------------------------------------------------------===## +## +## This source file is part of the Swift.org open source project +## +## Copyright (c) 2024 Apple Inc. and the Swift project authors +## Licensed under Apache License v2.0 with Runtime Library Exception +## +## See https://swift.org/LICENSE.txt for license information +## See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +## +##===----------------------------------------------------------------------===## + +set -euo pipefail + +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { error "$@"; exit 1; } + +if [ ! -f .spi.yml ]; then + log "No '.spi.yml' found, no documentation targets to check." + exit 0 +fi + +if ! command -v yq &> /dev/null; then + fatal "yq could not be found. Please install yq to proceed." +fi + +package_files=$(find . -maxdepth 1 -name 'Package*.swift') +if [ -z "$package_files" ]; then + fatal "Package.swift not found. Please ensure you are running this script from the root of a Swift package." +fi + +# yq 3.1.0-3 doesn't have filter, otherwise we could replace the grep call with "filter(.identity == "swift-docc-plugin") | keys | .[]" +hasDoccPlugin=$(swift package dump-package | yq -r '.dependencies[].sourceControl' | grep -e "\"identity\": \"swift-docc-plugin\"" || true) +if [[ -n $hasDoccPlugin ]] +then + log "swift-docc-plugin already exists" +else + log "Appending swift-docc-plugin" + for package_file in $package_files; do + log "Editing $package_file..." + cat <> "$package_file" + +package.dependencies.append( + .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0") +) +EOF + done +fi + +log "Checking documentation targets..." +for target in $(yq -r '.builder.configs[].documentation_targets[]' .spi.yml); do + log "Checking target $target..." + # shellcheck disable=SC2086 # We explicitly want to explode "$ADDITIONAL_DOCC_ARGUMENTS" into multiple arguments. + swift package plugin generate-documentation --target "$target" --warnings-as-errors --analyze $ADDITIONAL_DOCC_ARGUMENTS +done + +log "✅ Found no documentation issues." \ No newline at end of file diff --git a/scripts/check-format-linux.sh b/scripts/check-format-linux.sh new file mode 100755 index 00000000..57393937 --- /dev/null +++ b/scripts/check-format-linux.sh @@ -0,0 +1,53 @@ +#!/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 +## +##===----------------------------------------------------------------------===## +##===----------------------------------------------------------------------===## +## +## This source file is part of the Swift.org open source project +## +## Copyright (c) 2020 Apple Inc. and the Swift project authors +## Licensed under Apache License v2.0 with Runtime Library Exception +## +## See https://swift.org/LICENSE.txt for license information +## See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +## +##===----------------------------------------------------------------------===## + +set +x +set -euo pipefail + +SWIFT_IMAGE=swift:latest +CHECK_FORMAT_SCRIPT=https://raw.githubusercontent.com/swiftlang/github-workflows/refs/heads/main/.github/workflows/scripts/check-swift-format.sh + +echo "Downloading check-swift-format.sh" +curl -s ${CHECK_FORMAT_SCRIPT} > format.sh && chmod u+x format.sh + +echo "Running check-swift-format.sh" +/usr/local/bin/docker run --rm -v "$(pwd):/workspace" -w /workspace ${SWIFT_IMAGE} bash -clx "./format.sh" + +echo "Cleaning up" +rm format.sh + +YAML_LINT=https://raw.githubusercontent.com/swiftlang/github-workflows/refs/heads/main/.github/workflows/configs/yamllint.yml +YAML_IMAGE=ubuntu:latest + +echo "Downloading yamllint.yml" +curl -s ${YAML_LINT} > yamllint.yml + +echo "Running yamllint" +/usr/local/bin/docker run --rm -v "$(pwd):/workspace" -w /workspace ${YAML_IMAGE} bash -clx "apt-get -qq update && apt-get -qq -y install yamllint && yamllint --strict --config-file /workspace/yamllint.yml .github" + +echo "Cleaning up" +rm yamllint.yml + diff --git a/scripts/check-format.sh b/scripts/check-format.sh new file mode 100755 index 00000000..51fd80ac --- /dev/null +++ b/scripts/check-format.sh @@ -0,0 +1,58 @@ +#!/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 +## +##===----------------------------------------------------------------------===## +##===----------------------------------------------------------------------===## +## +## This source file is part of the Swift.org open source project +## +## Copyright (c) 2020 Apple Inc. and the Swift project authors +## Licensed under Apache License v2.0 with Runtime Library Exception +## +## See https://swift.org/LICENSE.txt for license information +## See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +## +##===----------------------------------------------------------------------===## + +set -euo pipefail + +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { error "$@"; exit 1; } + + +if [[ -f .swiftformatignore ]]; then + log "Found swiftformatignore file..." + + log "Running swift format format..." + tr '\n' '\0' < .swiftformatignore| xargs -0 -I% printf '":(exclude)%" '| xargs git ls-files -z '*.swift' | xargs -0 swift format format --parallel --in-place + + log "Running swift format lint..." + + tr '\n' '\0' < .swiftformatignore | xargs -0 -I% printf '":(exclude)%" '| xargs git ls-files -z '*.swift' | xargs -0 swift format lint --strict --parallel +else + log "Running swift format format..." + git ls-files -z '*.swift' | xargs -0 swift format format --parallel --in-place + + log "Running swift format lint..." + + git ls-files -z '*.swift' | xargs -0 swift format lint --strict --parallel +fi + + + +log "Checking for modified files..." + +GIT_PAGER='' git diff --exit-code '*.swift' + +log "✅ Found no formatting issues." diff --git a/scripts/check_no_api_breakages.sh b/scripts/check_no_api_breakages.sh deleted file mode 100755 index 436f722d..00000000 --- a/scripts/check_no_api_breakages.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright (c) 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 -## -##===----------------------------------------------------------------------===## - -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftNIO open source project -## -## Copyright (c) 2017-2020 Apple Inc. and the SwiftNIO project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftNIO project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -set -eu - -function usage() { - echo >&2 "Usage: $0 REPO-GITHUB-URL NEW-VERSION OLD-VERSIONS..." - echo >&2 - echo >&2 "This script requires a Swift 5.6+ toolchain." - echo >&2 - echo >&2 "Examples:" - echo >&2 - echo >&2 "Check between main and tag 2.1.1 of swift-nio:" - echo >&2 " $0 https://github.com/apple/swift-nio main 2.1.1" - echo >&2 - echo >&2 "Check between HEAD and commit 64cf63d7 using the provided toolchain:" - echo >&2 " xcrun --toolchain org.swift.5120190702a $0 ../some-local-repo HEAD 64cf63d7" -} - -if [[ $# -lt 3 ]]; then - usage - exit 1 -fi - -tmpdir=$(mktemp -d /tmp/.check-api_XXXXXX) -repo_url=$1 -new_tag=$2 -shift 2 - -repodir="$tmpdir/repo" -git clone "$repo_url" "$repodir" -git -C "$repodir" fetch -q origin '+refs/pull/*:refs/remotes/origin/pr/*' -cd "$repodir" -git checkout -q "$new_tag" - -for old_tag in "$@"; do - echo "Checking public API breakages from $old_tag to $new_tag" - - swift package diagnose-api-breaking-changes "$old_tag" -done - -echo done diff --git a/scripts/extract_aws_credentials.sh b/scripts/extract_aws_credentials.sh new file mode 100755 index 00000000..039f44f3 --- /dev/null +++ b/scripts/extract_aws_credentials.sh @@ -0,0 +1,122 @@ +#!/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 +## +##===----------------------------------------------------------------------===## + +# Extract AWS credentials from ~/.aws/credentials and ~/.aws/config (default profile) +# and set environment variables + +set -e + +# Default profile name +PROFILE="default" + +# Check if a different profile is specified as argument +if [ $# -eq 1 ]; then + PROFILE="$1" +fi + +# AWS credentials file path +CREDENTIALS_FILE="$HOME/.aws/credentials" +CONFIG_FILE="$HOME/.aws/config" + +# Check if credentials file exists +if [ ! -f "$CREDENTIALS_FILE" ]; then + echo "Error: AWS credentials file not found at $CREDENTIALS_FILE" + exit 1 +fi + +# Function to extract value from AWS config files +extract_value() { + local file="$1" + local profile="$2" + local key="$3" + + # Use awk to extract the value for the specified profile and key + awk -v profile="[$profile]" -v key="$key" ' + BEGIN { in_profile = 0 } + $0 == profile { in_profile = 1; next } + /^\[/ && $0 != profile { in_profile = 0 } + in_profile && $0 ~ "^" key " *= *" { + gsub("^" key " *= *", "") + gsub(/^[ \t]+|[ \t]+$/, "") # trim whitespace + print $0 + exit + } + ' "$file" +} + +# Extract credentials +AWS_ACCESS_KEY_ID=$(extract_value "$CREDENTIALS_FILE" "$PROFILE" "aws_access_key_id") +AWS_SECRET_ACCESS_KEY=$(extract_value "$CREDENTIALS_FILE" "$PROFILE" "aws_secret_access_key") +AWS_SESSION_TOKEN=$(extract_value "$CREDENTIALS_FILE" "$PROFILE" "aws_session_token") + +# Extract region from config file (try both credentials and config files) +AWS_REGION=$(extract_value "$CREDENTIALS_FILE" "$PROFILE" "region") +if [ -z "$AWS_REGION" ] && [ -f "$CONFIG_FILE" ]; then + # Try config file with profile prefix for non-default profiles + if [ "$PROFILE" = "default" ]; then + AWS_REGION=$(extract_value "$CONFIG_FILE" "$PROFILE" "region") + else + AWS_REGION=$(extract_value "$CONFIG_FILE" "profile $PROFILE" "region") + fi +fi + +# Validate required credentials +if [ -z "$AWS_ACCESS_KEY_ID" ]; then + echo "Error: aws_access_key_id not found for profile '$PROFILE'" + exit 1 +fi + +if [ -z "$AWS_SECRET_ACCESS_KEY" ]; then + echo "Error: aws_secret_access_key not found for profile '$PROFILE'" + exit 1 +fi + +# Set default region if not found +if [ -z "$AWS_REGION" ]; then + AWS_REGION="us-east-1" + echo "Warning: No region found for profile '$PROFILE', defaulting to us-east-1" +fi + +# Export environment variables +export AWS_REGION="$AWS_REGION" +export AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" +export AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" + +# Only export session token if it exists (for temporary credentials) +if [ -n "$AWS_SESSION_TOKEN" ]; then + export AWS_SESSION_TOKEN="$AWS_SESSION_TOKEN" +fi + +# Print confirmation (without sensitive values) +echo "AWS credentials loaded for profile: $PROFILE" +echo "AWS_REGION: $AWS_REGION" +echo "AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:0:4}****" +echo "AWS_SECRET_ACCESS_KEY: ****" +if [ -n "$AWS_SESSION_TOKEN" ]; then + echo "AWS_SESSION_TOKEN: ****" +fi + +# Optional: Print export commands for manual sourcing +echo "" +echo "To use these credentials in your current shell, run:" +echo "source $(basename "$0")" +echo "" +echo "Or copy and paste these export commands:" +echo "export AWS_REGION='$AWS_REGION'" +echo "export AWS_ACCESS_KEY_ID='$AWS_ACCESS_KEY_ID'" +echo "export AWS_SECRET_ACCESS_KEY='$AWS_SECRET_ACCESS_KEY'" +if [ -n "$AWS_SESSION_TOKEN" ]; then + echo "export AWS_SESSION_TOKEN='$AWS_SESSION_TOKEN'" +fi \ No newline at end of file diff --git a/scripts/generate_contributors_list.sh b/scripts/generate_contributors_list.sh index d745e21e..9d4014d8 100755 --- a/scripts/generate_contributors_list.sh +++ b/scripts/generate_contributors_list.sh @@ -3,7 +3,7 @@ ## ## This source file is part of the SwiftAWSLambdaRuntime open source project ## -## Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors +## Copyright (c) 2017-2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information @@ -36,4 +36,4 @@ cat > "$here/../CONTRIBUTORS.txt" <<- EOF **Updating this list** Please do not edit this file manually. It is generated using \`./scripts/generate_contributors_list.sh\`. If a name is misspelled or appearing multiple times: add an entry in \`./.mailmap\` -EOF +EOF \ No newline at end of file diff --git a/scripts/linux_performance_setup.sh b/scripts/linux_performance_setup.sh index 7c11cbbd..f02ac66b 100755 --- a/scripts/linux_performance_setup.sh +++ b/scripts/linux_performance_setup.sh @@ -20,14 +20,14 @@ apt-get install -y vim htop strace linux-tools-common linux-tools-generic libc6- echo 0 > /proc/sys/kernel/kptr_restrict -cd /usr/bin +pushd /usr/bin || exit 1 rm -rf perf ln -s /usr/lib/linux-tools/*/perf perf -cd - +popd || exit 1 -cd /opt +pushd /opt || exit 1 git clone https://github.com/brendangregg/FlameGraph.git -cd - +popd || exit 1 # build the code in relase mode with debug symbols # swift build -c release -Xswiftc -g diff --git a/scripts/performance_test.sh b/scripts/performance_test.sh index d46f2bef..700b6810 100755 --- a/scripts/performance_test.sh +++ b/scripts/performance_test.sh @@ -3,7 +3,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) 2017-2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information @@ -13,69 +13,91 @@ ## ##===----------------------------------------------------------------------===## -set -eu +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { error "$@"; exit 1; } export HOST=127.0.0.1 -export PORT=3000 +export PORT=7000 export AWS_LAMBDA_RUNTIME_API="$HOST:$PORT" -export LOG_LEVEL=warning # important, otherwise log becomes a bottleneck +export LOG_LEVEL=error # important, otherwise log becomes a bottleneck + +DATE_CMD="date" +# using gdate on darwin for nanoseconds +# gdate is installed by coreutils on macOS +if [[ $(uname -s) == "Darwin" ]]; then + if ! command -v gdate &> /dev/null; then + # shellcheck disable=SC2006 # we explicitly want to use backticks here + fatal "gdate could not be found. Please \`brew install coreutils\` to proceed." + fi + DATE_CMD="gdate" +fi +echo "⏱️ using $DATE_CMD to count time" -# using gdate on mdarwin for nanoseconds -if [[ $(uname -s) == "Linux" ]]; then - shopt -s expand_aliases - alias gdate="date" +if ! command -v "$DATE_CMD" &> /dev/null; then + fatal "$DATE_CMD could not be found. Please install $DATE_CMD to proceed." fi +echo "🏗️ Building library and test functions" swift build -c release -Xswiftc -g -swift build --package-path Examples/Echo -c release -Xswiftc -g -swift build --package-path Examples/JSON -c release -Xswiftc -g +LAMBDA_USE_LOCAL_DEPS=../.. swift build --package-path Examples/HelloWorld -c release -Xswiftc -g +LAMBDA_USE_LOCAL_DEPS=../.. swift build --package-path Examples/HelloJSON -c release -Xswiftc -g cleanup() { - kill -9 $server_pid + pkill -9 MockServer && echo "killed previous mock server" # ignore-unacceptable-language } -trap "cleanup" ERR +# start a mock server +start_mockserver() { + if [ $# -ne 2 ]; then + fatal "Usage: $0 " + fi + MODE=$1 + INVOCATIONS=$2 + pkill -9 MockServer && echo "killed previous mock server" && sleep 1 # ignore-unacceptable-language + echo "👨‍🔧 starting server in $MODE mode for $INVOCATIONS invocations" + (MAX_INVOCATIONS="$INVOCATIONS" MODE="$MODE" ./.build/release/MockServer) & + server_pid=$! + sleep 1 + kill -0 $server_pid # check server is alive # ignore-unacceptable-language +} -cold_iterations=1000 -warm_iterations=10000 +cold_iterations=100 +warm_iterations=1000 results=() #------------------ # string #------------------ -export MODE=string +MODE=string -# start (fork) mock server -pkill -9 MockServer && echo "killed previous servers" && sleep 1 -echo "starting server in $MODE mode" -(./.build/release/MockServer) & -server_pid=$! -sleep 1 -kill -0 $server_pid # check server is alive +# Start mock server +start_mockserver "$MODE" "$cold_iterations" # cold start -echo "running $MODE mode cold test" +echo "🚀❄️ running $MODE mode $cold_iterations cold test" cold=() -export MAX_REQUESTS=1 -for (( i=0; i<$cold_iterations; i++ )); do - start=$(gdate +%s%N) - ./Examples/Echo/.build/release/MyLambda - end=$(gdate +%s%N) - cold+=( $(($end-$start)) ) +for (( i=0; i Checking for unacceptable language... " -# This greps for unacceptable terminology. The square bracket[s] are so that -# "git grep" doesn't find the lines that greps :). -unacceptable_terms=( - -e blacklis[t] - -e whitelis[t] - -e slav[e] - -e sanit[y] -) -if git grep --color=never -i "${unacceptable_terms[@]}" -- . ":(exclude)CODE_OF_CONDUCT.md" > /dev/null; then - printf "\033[0;31mUnacceptable language found.\033[0m\n" - git grep -i "${unacceptable_terms[@]}" - exit 1 -fi -printf "\033[0;32mokay.\033[0m\n" - -printf "=> Checking format... " -FIRST_OUT="$(git status --porcelain)" -swiftformat . > /dev/null 2>&1 -SECOND_OUT="$(git status --porcelain)" -if [[ "$FIRST_OUT" != "$SECOND_OUT" ]]; then - printf "\033[0;31mformatting issues!\033[0m\n" - git --no-pager diff - exit 1 -else - printf "\033[0;32mokay.\033[0m\n" -fi - -printf "=> Checking license headers\n" -tmp=$(mktemp /tmp/.swift-aws-lambda-soundness_XXXXXX) - -for language in swift-or-c bash dtrace; do - printf " * $language... " - declare -a matching_files - declare -a exceptions - expections=( ) - matching_files=( -name '*' ) - case "$language" in - swift-or-c) - exceptions=( -name Package.swift -o -name 'Package@*.swift' ) - matching_files=( -name '*.swift' -o -name '*.c' -o -name '*.h' ) - cat > "$tmp" <<"EOF" -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) YEARS 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 -// -//===----------------------------------------------------------------------===// -EOF - ;; - bash) - matching_files=( -name '*.sh' ) - cat > "$tmp" <<"EOF" -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright (c) YEARS 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 -## -##===----------------------------------------------------------------------===## -EOF - ;; - dtrace) - matching_files=( -name '*.d' ) - cat > "$tmp" <<"EOF" -#!/usr/sbin/dtrace -q -s -/*===----------------------------------------------------------------------===* - * - * This source file is part of the SwiftAWSLambdaRuntime open source project - * - * Copyright (c) YEARS 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 - * - *===----------------------------------------------------------------------===*/ -EOF - ;; - *) - echo >&2 "ERROR: unknown language '$language'" - ;; - esac - - expected_lines=$(cat "$tmp" | wc -l) - expected_sha=$(cat "$tmp" | shasum) - - ( - cd "$here/.." - find . \ - \( \! -path '*/.build/*' -a \ - \( \! -path '*/.git/*' \) -a \ - \( \! -path '*/Documentation.docc/*' \) -a \ - \( "${matching_files[@]}" \) -a \ - \( \! \( "${exceptions[@]}" \) \) \) | while read line; do - if [[ "$(cat "$line" | replace_acceptable_years | head -n $expected_lines | shasum)" != "$expected_sha" ]]; then - printf "\033[0;31mmissing headers in file '$line'!\033[0m\n" - diff -u <(cat "$line" | replace_acceptable_years | head -n $expected_lines) "$tmp" - exit 1 - fi - done - printf "\033[0;32mokay.\033[0m\n" - ) -done - -rm "$tmp" 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