diff --git a/.swiftformatignore b/.swiftformatignore new file mode 100644 index 00000000..ea8271df --- /dev/null +++ b/.swiftformatignore @@ -0,0 +1 @@ +Plugins/AWSLambdaInitializer/Template.swift diff --git a/Examples/_MyFirstFunction/create_function.sh b/Examples/_MyFirstFunction/create_function.sh new file mode 100755 index 00000000..71fd4ad7 --- /dev/null +++ b/Examples/_MyFirstFunction/create_function.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +# check if docker is installed +which docker > /dev/null +if [[ $? != 0 ]]; then + echo "Docker is not installed. Please install Docker and try again." + exit 1 +fi + +# 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' or asked interactively. +" + +read -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 + +echo "⚑️ Create your Swift command line project" +swift package init --type executable --name MyLambda + +echo "πŸ“¦ Add the AWS Lambda Swift runtime to your project" +swift package add-dependency https://github.com/swift-server/swift-aws-lambda-runtime.git --branch main +swift package add-dependency https://github.com/swift-server/swift-aws-lambda-events.git --branch main +swift package add-target-dependency AWSLambdaRuntime MyLambda --package swift-aws-lambda-runtime +swift package add-target-dependency AWSLambdaEvents MyLambda --package swift-aws-lambda-events + +echo "πŸ“ Write the Swift code" +swift package lambda-init --allow-writing-to-package-directory + +echo "πŸ“¦ Compile and package the function for deployment" +swift package archive --allow-network-connections docker + +echo "πŸš€ Deploy to AWS Lambda" + + diff --git a/Package.swift b/Package.swift index 69f5ff36..ff9475ed 100644 --- a/Package.swift +++ b/Package.swift @@ -16,15 +16,21 @@ let package = Package( .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 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 // requires Linux or at least macOS v15 .plugin(name: "AWSLambdaPackager", targets: ["AWSLambdaPackager"]), + // plugin to deploy a Lambda function + .plugin(name: "AWSLambdadeployer", targets: ["AWSLambdaDeployer"]), + .executable(name: "AWSLambdaDeployerHelper", targets: ["AWSLambdaDeployerHelper"]), // for testing only .library(name: "AWSLambdaTesting", targets: ["AWSLambdaTesting"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", from: "2.72.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), + .package(url: "https://github.com/apple/swift-crypto.git", from: "3.9.1"), ], targets: [ .target( @@ -45,6 +51,19 @@ let package = Package( ], swiftSettings: [.swiftLanguageMode(.v5)] ), + .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.") + ] + ) + ), .plugin( name: "AWSLambdaPackager", capability: .command( @@ -61,6 +80,34 @@ let package = Package( ] ) ), + .plugin( + name: "AWSLambdaDeployer", + capability: .command( + intent: .custom( + verb: "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: "AWSLambdaDeployerHelper") + ] + ), + .executableTarget( + name: "AWSLambdaDeployerHelper", + dependencies: [ + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "Crypto", package: "swift-crypto"), + ], + swiftSettings: [.swiftLanguageMode(.v6)] + ), .testTarget( name: "AWSLambdaRuntimeCoreTests", dependencies: [ diff --git a/Plugins/AWSLambdaInitializer/Plugin.swift b/Plugins/AWSLambdaInitializer/Plugin.swift new file mode 100644 index 00000000..21713c0f --- /dev/null +++ b/Plugins/AWSLambdaInitializer/Plugin.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// 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 Foundation +import PackagePlugin + +@main +@available(macOS 15.0, *) +struct AWSLambdaPackager: CommandPlugin { + + let destFileName = "Sources/main.swift" + + func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws { + let configuration = try Configuration(context: context, arguments: arguments) + + if configuration.help { + self.displayHelpMessage() + return + } + + let destFileURL = context.package.directoryURL.appendingPathComponent(destFileName) + do { + try functionWithUrlTemplate.write(to: destFileURL, atomically: true, encoding: .utf8) + + if configuration.verboseLogging { + Diagnostics.progress("βœ… Lambda function written to \(destFileName)") + Diagnostics.progress("πŸ“¦ You can now package with: 'swift package archive'") + } + + } catch { + Diagnostics.error("πŸ›‘Failed to create the Lambda function file: \(error)") + } + } + + private func displayHelpMessage() { + print( + """ + OVERVIEW: A SwiftPM plugin to scaffold a HelloWorld Lambda function. + + USAGE: swift package lambda-init + [--help] [--verbose] + [--allow-writing-to-package-directory] + + OPTIONS: + --allow-writing-to-package-directory Don't ask for permissions to write files. + --verbose Produce verbose output for debugging. + --help Show help information. + """ + ) + } + +} + +private struct Configuration: CustomStringConvertible { + public let help: Bool + public let verboseLogging: Bool + + public init( + context: PluginContext, + arguments: [String] + ) throws { + var argumentExtractor = ArgumentExtractor(arguments) + let verboseArgument = argumentExtractor.extractFlag(named: "verbose") > 0 + let helpArgument = argumentExtractor.extractFlag(named: "help") > 0 + + // help required ? + self.help = helpArgument + + // verbose logging required ? + self.verboseLogging = verboseArgument + } + + var description: String { + """ + { + verboseLogging: \(self.verboseLogging) + } + """ + } +} diff --git a/Plugins/AWSLambdaInitializer/Template.swift b/Plugins/AWSLambdaInitializer/Template.swift new file mode 100644 index 00000000..133e5033 --- /dev/null +++ b/Plugins/AWSLambdaInitializer/Template.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// 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 Foundation + +let functionWithUrlTemplate = #""" + 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() + """# diff --git a/readme.md b/readme.md index ca8097e8..181be9ce 100644 --- a/readme.md +++ b/readme.md @@ -18,6 +18,17 @@ ## TL;DR +The `Examples/_MyFirstFunction` contains a script that goes through the steps described in this section. + +If you are really impatient, just type: + +```bash +cd Examples/_MyFirstFunction +./create_and_deploy_function.sh +``` + +Otherwise, continue reading. + 1. Create a new Swift executable project ```bash @@ -64,7 +75,15 @@ swift package init --type executable ) ``` -3. Edit `Sources/main.swift` file and replace the content with this code +3. Scaffold a minimal Lambda function + +The runtime comes with a plugin to generate the code of a simple AWS Lambda function: + +```bash +swift package lambda-init --allow-writing-to-package-directory +``` + +Your `Sources/main.swift` file must look like this. ```swift import AWSLambdaRuntime @@ -81,12 +100,13 @@ try await runtime.run() 4. Build & archive the package +The runtime comes with a plugin to compile on Amazon Linux and create a ZIP archive: + ```bash -swift build swift package archive --allow-network-connections docker ``` -If there is no error, there is a ZIP file ready to deploy. +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` 5. Deploy to AWS