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/workflows/examples_matrix.yml b/.github/workflows/examples_matrix.yml new file mode 100644 index 00000000..fb9cca41 --- /dev/null +++ b/.github/workflows/examples_matrix.yml @@ -0,0 +1,80 @@ +name: ExamplesMatrix + +on: + workflow_call: + inputs: + name: + type: string + description: "The name of the workflow used for the concurrency group." + required: true + # examples: + # type: sequence + # description: "The examples to run." + # required: 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 + description: "Container image for the matrix job. Defaults to matching latest Swift Ubuntu image." + default: "swift:amazonlinux2" + +## We are cancelling previously triggered workflow runs +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.name }} + cancel-in-progress: true + +jobs: + linux: + name: Example/${{ matrix.examples }} on Linux ${{ matrix.swift.swift_version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # This should be passed as an argument in input. Can we pass arrays as argument ? + examples: ["HelloWorld", "APIGateway", "S3_AWSSDK", "S3_Soto"] + # examples: ${{ inputs.examples }} + + # We are using only one Swift version + swift: + - image: ${{ inputs.matrix_linux_swift_container_image }} + swift_version: "6.0.1-amazonlinux2" + container: + image: ${{ matrix.swift.image }} + steps: + # 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: + SWIFT_VERSION: ${{ matrix.swift.swift_version }} + COMMAND: ${{ inputs.matrix_linux_command }} + EXAMPLE: ${{ matrix.examples }} + run: | + ./scripts/integration_tests.sh diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index f7522664..ad616cdc 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,29 +1,39 @@ name: PR on: - pull_request: - types: [opened, reopened, synchronize] + pull_request: + types: [opened, reopened, synchronize] jobs: - soundness: - name: Soundness - uses: apple/swift-nio/.github/workflows/soundness.yml@main - with: - license_header_check_project_name: "SwiftAWSLambdaRuntime" - shell_check_enabled: false - api_breakage_check_container_image: "swift:6.0-noble" - docs_check_container_image: "swift:6.0-noble" + soundness: + name: Soundness + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main + with: + license_header_check_project_name: "SwiftAWSLambdaRuntime" + shell_check_enabled: false + api_breakage_check_container_image: "swift:6.0-noble" + docs_check_container_image: "swift:6.0-noble" + format_check_container_image: "swiftlang/swift:nightly-6.0-jammy" - unit-tests: - name: Unit tests - uses: apple/swift-nio/.github/workflows/unit_tests.yml@main - with: - linux_5_8_enabled: false - linux_5_9_enabled: false - linux_5_10_enabled: false - linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error" - linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error" + unit-tests: + name: Unit tests + uses: apple/swift-nio/.github/workflows/unit_tests.yml@main + with: + linux_5_8_enabled: false + linux_5_9_enabled: false + linux_5_10_enabled: false + linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error" + linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error" - swift-6-language-mode: - name: Swift 6 Language Mode - uses: apple/swift-nio/.github/workflows/swift_6_language_mode.yml@main + integration-tests: + name: Integration Tests + uses: ./.github/workflows/examples_matrix.yml + with: + # We should pass the list of examples here, but we can't pass an array as argument + # examples: [ "HelloWorld", "APIGateway" ] + name: "Integration tests" + matrix_linux_command: "LAMBDA_USE_LOCAL_DEPS=../.. swift build" + + swift-6-language-mode: + name: Swift 6 Language Mode + uses: apple/swift-nio/.github/workflows/swift_6_language_mode.yml@main diff --git a/Examples/APIGateway/Package.swift b/Examples/APIGateway/Package.swift index a2df5b73..ebaac99a 100644 --- a/Examples/APIGateway/Package.swift +++ b/Examples/APIGateway/Package.swift @@ -3,7 +3,6 @@ import PackageDescription // needed for CI to test the local version of the library -import class Foundation.ProcessInfo import struct Foundation.URL #if os(macOS) @@ -16,17 +15,16 @@ let package = Package( name: "swift-aws-lambda-runtime-example", platforms: platforms, products: [ - .executable(name: "APIGAtewayLambda", targets: ["APIGAtewayLambda"]) + .executable(name: "APIGatewayLambda", targets: ["APIGatewayLambda"]) ], dependencies: [ - // dependency on swift-aws-lambda-runtime is added dynamically below - // .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main") - - .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", branch: "main") + // during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", branch: "main"), ], targets: [ .executableTarget( - name: "APIGAtewayLambda", + name: "APIGatewayLambda", dependencies: [ .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), @@ -36,20 +34,29 @@ let package = Package( ] ) -if let localDepsPath = ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"], +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], localDepsPath != "", let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), - let _ = v.isDirectory + 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) ] - -} else { - print("[INFO] LAMBDA_USE_LOCAL_DEPS is not pointing to your local swift-aws-lambda-runtime code") - print("[INFO] This project will compile against the main branch of the Lambda Runtime on GitHub") - package.dependencies += [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main") - ] } diff --git a/Examples/APIGateway/README.md b/Examples/APIGateway/README.md index f534f9ba..be4c8b8c 100644 --- a/Examples/APIGateway/README.md +++ b/Examples/APIGateway/README.md @@ -26,7 +26,7 @@ swift package archive --allow-network-access 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` +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip` ## Deploy @@ -40,9 +40,9 @@ To actually deploy your Lambda function and create the infrastructure, type the ```bash sam deploy \ -----resolve-s3 \ +--resolve-s3 \ --template-file template.yaml \ ---stack-name MyLambda \ +--stack-name APIGatewayLambda \ --capabilities CAPABILITY_IAM ``` @@ -53,8 +53,8 @@ The output is similar to this one. ----------------------------------------------------------------------------------------------------------------------------- Outputs ----------------------------------------------------------------------------------------------------------------------------- -Key APIGAtewayEndpoint -Description API Gateway endpoint UR" +Key APIGatewayEndpoint +Description API Gateway endpoint URL" Value https://a5q74es3k2.execute-api.us-east-1.amazonaws.com ----------------------------------------------------------------------------------------------------------------------------- ``` diff --git a/Examples/APIGateway/template.yaml b/Examples/APIGateway/template.yaml index 700c09b0..0d7a6af4 100644 --- a/Examples/APIGateway/template.yaml +++ b/Examples/APIGateway/template.yaml @@ -4,12 +4,12 @@ Description: SAM Template for QuoteService Resources: # Lambda function - APIGAtewayLambda: + APIGatewayLambda: Type: AWS::Serverless::Function Properties: - CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGAtewayLambda/APIGAtewayLambda.zip + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip Timeout: 60 - Handler: swift.bootstrap + Handler: swift.bootstrap # ignored by the Swift runtime Runtime: provided.al2 MemorySize: 512 Architectures: @@ -19,13 +19,13 @@ Resources: # 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 + LOG_LEVEL: debug Events: HttpApiEvent: - Type: HttpApi + Type: HttpApi Outputs: # print API Gateway endpoint - APIGAtewayEndpoint: + APIGatewayEndpoint: Description: API Gateway endpoint UR" Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" diff --git a/Examples/HelloWorld/Package.swift b/Examples/HelloWorld/Package.swift index 6468fb88..adb4b1f9 100644 --- a/Examples/HelloWorld/Package.swift +++ b/Examples/HelloWorld/Package.swift @@ -3,7 +3,6 @@ import PackageDescription // needed for CI to test the local version of the library -import class Foundation.ProcessInfo import struct Foundation.URL #if os(macOS) @@ -19,8 +18,8 @@ let package = Package( .executable(name: "MyLambda", targets: ["MyLambda"]) ], dependencies: [ - // dependency on swift-aws-lambda-runtime is added dynamically below - // .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main") + // during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main") ], targets: [ .executableTarget( @@ -33,20 +32,29 @@ let package = Package( ] ) -if let localDepsPath = ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"], +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], localDepsPath != "", let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), - let _ = v.isDirectory + 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) ] - -} else { - print("[INFO] LAMBDA_USE_LOCAL_DEPS is not pointing to your local swift-aws-lambda-runtime code") - print("[INFO] This project will compile against the main branch of the Lambda Runtime on GitHub") - package.dependencies += [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main") - ] } 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..5417fbdd --- /dev/null +++ b/Examples/S3_AWSSDK/Package.swift @@ -0,0 +1,63 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +#if os(macOS) +let platforms: [PackageDescription.SupportedPlatform]? = [.macOS(.v15)] +#else +let platforms: [PackageDescription.SupportedPlatform]? = nil +#endif + +let package = Package( + name: "AWSSDKExample", + platforms: platforms, + 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", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events", branch: "main"), + .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..58a7b83d --- /dev/null +++ b/Examples/S3_AWSSDK/README.md @@ -0,0 +1,89 @@ +# 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-access 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 +``` \ 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..46e29ec8 --- /dev/null +++ b/Examples/S3_AWSSDK/template.yaml @@ -0,0 +1,43 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for AWS SDK Example + +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: 512 + 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..4b4eead1 --- /dev/null +++ b/Examples/S3_Soto/Package.swift @@ -0,0 +1,64 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +#if os(macOS) +let platforms: [PackageDescription.SupportedPlatform]? = [.macOS(.v15)] +#else +let platforms: [PackageDescription.SupportedPlatform]? = nil +#endif + +let package = Package( + name: "SotoExample", + platforms: platforms, + 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", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events", branch: "main"), + ], + 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..c9c0e4a1 --- /dev/null +++ b/Examples/S3_Soto/README.md @@ -0,0 +1,89 @@ +# 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-access 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 +``` \ 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..bfc04d1e --- /dev/null +++ b/Examples/S3_Soto/template.yaml @@ -0,0 +1,43 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for AWS SDK Example + +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: 512 + 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/Package.swift b/Package.swift index 8748710f..b20625e2 100644 --- a/Package.swift +++ b/Package.swift @@ -61,16 +61,14 @@ let package = Package( .byName(name: "AWSLambdaRuntimeCore"), .product(name: "NIOTestUtils", package: "swift-nio"), .product(name: "NIOFoundationCompat", package: "swift-nio"), - ], - swiftSettings: [.swiftLanguageMode(.v5)] + ] ), .testTarget( name: "AWSLambdaRuntimeTests", dependencies: [ .byName(name: "AWSLambdaRuntimeCore"), .byName(name: "AWSLambdaRuntime"), - ], - swiftSettings: [.swiftLanguageMode(.v5)] + ] ), // testing helper .target( diff --git a/Plugins/AWSLambdaPackager/PluginUtils.swift b/Plugins/AWSLambdaPackager/PluginUtils.swift index 11827200..e12e09dd 100644 --- a/Plugins/AWSLambdaPackager/PluginUtils.swift +++ b/Plugins/AWSLambdaPackager/PluginUtils.swift @@ -32,7 +32,7 @@ struct Utils { let fd = dup(1) let stdout = fdopen(fd, "rw") - defer { fclose(stdout!) } + 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. 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/scripts/integration_tests.sh b/scripts/integration_tests.sh new file mode 100755 index 00000000..232d743d --- /dev/null +++ b/scripts/integration_tests.sh @@ -0,0 +1,34 @@ +#!/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 -euo pipefail + +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { error "$@"; exit 1; } + +test -n "${SWIFT_VERSION:-}" || fatal "SWIFT_VERSION unset" +test -n "${COMMAND:-}" || fatal "COMMAND unset" +test -n "${EXAMPLE:-}" || fatal "EXAMPLE unset" +swift_version="$SWIFT_VERSION" +command="$COMMAND" +example="$EXAMPLE" + +pushd Examples/"$example" > /dev/null + +log "Running command with Swift $SWIFT_VERSION" +eval "$command" + +popd