From 2b5405a87de3ded2a7977d74b5a2d9ccf44e4bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Thu, 7 Nov 2024 10:00:05 +0100 Subject: [PATCH 01/27] WIP : init plugin --- .swiftformatignore | 1 + Examples/_MyFirstFunction/create_function.sh | 39 +++++++++ Package.swift | 47 ++++++++++ Plugins/AWSLambdaInitializer/Plugin.swift | 91 ++++++++++++++++++++ Plugins/AWSLambdaInitializer/Template.swift | 35 ++++++++ readme.md | 26 +++++- 6 files changed, 236 insertions(+), 3 deletions(-) create mode 100644 .swiftformatignore create mode 100755 Examples/_MyFirstFunction/create_function.sh create mode 100644 Plugins/AWSLambdaInitializer/Plugin.swift create mode 100644 Plugins/AWSLambdaInitializer/Template.swift diff --git a/.swiftformatignore b/.swiftformatignore new file mode 100644 index 000000000..ea8271df6 --- /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 000000000..71fd4ad7e --- /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 69f5ff36c..ff9475ed5 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 000000000..21713c0f1 --- /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 000000000..133e50334 --- /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 ca8097e86..181be9ce7 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 From cc08a9a5c2019fda0cac7dee9bb9fe79943ebea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Thu, 7 Nov 2024 10:00:17 +0100 Subject: [PATCH 02/27] WIP deploy plugin --- Plugins/AWSLambdaDeployer/Plugin.swift | 79 +++++ Plugins/AWSLambdaDeployer/PluginUtils.swift | 156 ++++++++++ .../AWSCredentials.swift | 60 ++++ .../AWSLambdaDeployerHelper/AWSSigner.swift | 269 ++++++++++++++++++ Sources/AWSLambdaDeployerHelper/main.swift | 1 + 5 files changed, 565 insertions(+) create mode 100644 Plugins/AWSLambdaDeployer/Plugin.swift create mode 100644 Plugins/AWSLambdaDeployer/PluginUtils.swift create mode 100644 Sources/AWSLambdaDeployerHelper/AWSCredentials.swift create mode 100644 Sources/AWSLambdaDeployerHelper/AWSSigner.swift create mode 100644 Sources/AWSLambdaDeployerHelper/main.swift diff --git a/Plugins/AWSLambdaDeployer/Plugin.swift b/Plugins/AWSLambdaDeployer/Plugin.swift new file mode 100644 index 000000000..977b1a2d0 --- /dev/null +++ b/Plugins/AWSLambdaDeployer/Plugin.swift @@ -0,0 +1,79 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2023 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 AWSLambdaDeployer: CommandPlugin { + + + func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws { + let configuration = try Configuration(context: context, arguments: arguments) + + if configuration.help { + self.displayHelpMessage() + return + } + + let tool = try context.tool(named: "AWSLambdaDeployerHelper") + try Utils.execute(executable: tool.url, arguments: [], logLevel: .debug) + } + + private func displayHelpMessage() { + print( + """ + OVERVIEW: A SwiftPM plugin to deploy a Lambda function. + + USAGE: swift package lambda-deploy + [--with-url] + [--help] [--verbose] + + OPTIONS: + --with-url Add an URL to access the Lambda function + --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/AWSLambdaDeployer/PluginUtils.swift b/Plugins/AWSLambdaDeployer/PluginUtils.swift new file mode 100644 index 000000000..52d1b2beb --- /dev/null +++ b/Plugins/AWSLambdaDeployer/PluginUtils.swift @@ -0,0 +1,156 @@ +//===----------------------------------------------------------------------===// +// +// 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: " "))") + } + + 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 workingDirectory = customWorkingDirectory { + process.currentDirectoryURL = URL(fileURLWithPath: workingDirectory.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/Sources/AWSLambdaDeployerHelper/AWSCredentials.swift b/Sources/AWSLambdaDeployerHelper/AWSCredentials.swift new file mode 100644 index 000000000..aec469b10 --- /dev/null +++ b/Sources/AWSLambdaDeployerHelper/AWSCredentials.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2023 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 +// +//===----------------------------------------------------------------------===// + +// +// credentials.swift +// aws-sign +// +// Created by Adam Fowler on 29/08/2019. +// +import class Foundation.ProcessInfo + +/// Protocol for providing credential details for accessing AWS services +public protocol Credential { + var accessKeyId: String {get} + var secretAccessKey: String {get} + var sessionToken: String? {get} +} + +/// basic version of Credential where you supply the credentials +public struct StaticCredential: Credential { + public let accessKeyId: String + public let secretAccessKey: String + public let sessionToken: String? + + public init(accessKeyId: String, secretAccessKey: String, sessionToken: String? = nil) { + self.accessKeyId = accessKeyId + self.secretAccessKey = secretAccessKey + self.sessionToken = sessionToken + } +} + +/// environment variable version of credential that uses system environment variables to get credential details +public struct EnvironmentCredential: Credential { + public let accessKeyId: String + public let secretAccessKey: String + public let sessionToken: String? + + public init?() { + guard let accessKeyId = ProcessInfo.processInfo.environment["AWS_ACCESS_KEY_ID"] else { + return nil + } + guard let secretAccessKey = ProcessInfo.processInfo.environment["AWS_SECRET_ACCESS_KEY"] else { + return nil + } + self.accessKeyId = accessKeyId + self.secretAccessKey = secretAccessKey + self.sessionToken = ProcessInfo.processInfo.environment["AWS_SESSION_TOKEN"] + } +} diff --git a/Sources/AWSLambdaDeployerHelper/AWSSigner.swift b/Sources/AWSLambdaDeployerHelper/AWSSigner.swift new file mode 100644 index 000000000..41774b93a --- /dev/null +++ b/Sources/AWSLambdaDeployerHelper/AWSSigner.swift @@ -0,0 +1,269 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2023 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 +// +//===----------------------------------------------------------------------===// + +// +// signer.swift +// AWSSigner +// +// Created by Adam Fowler on 2019/08/29. +// Amazon Web Services V4 Signer +// AWS documentation about signing requests is here https://docs.aws.amazon.com/general/latest/gr/signing_aws_api_requests.html +// + +import struct Foundation.CharacterSet +import struct Foundation.Data +import struct Foundation.Date +import class Foundation.DateFormatter +import struct Foundation.Locale +import struct Foundation.TimeZone +import struct Foundation.URL +import Crypto +import NIO +import NIOHTTP1 + +/// Amazon Web Services V4 Signer +public struct AWSSigner { + /// security credentials for accessing AWS services + public let credentials: Credential + /// service signing name. In general this is the same as the service name + public let name: String + /// AWS region you are working in + public let region: String + + static let hashedEmptyBody = SHA256.hash(data: [UInt8]()).hexDigest() + + static private let timeStampDateFormatter: DateFormatter = createTimeStampDateFormatter() + + /// Initialise the Signer class with AWS credentials + public init(credentials: Credential, name: String, region: String) { + self.credentials = credentials + self.name = name + self.region = region + } + + /// Enum for holding your body data + public enum BodyData { + case string(String) + case data(Data) + case byteBuffer(ByteBuffer) + } + + /// Generate signed headers, for a HTTP request + public func signHeaders(url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: BodyData? = nil, date: Date = Date()) -> HTTPHeaders { + let bodyHash = AWSSigner.hashedPayload(body) + let dateString = AWSSigner.timestamp(date) + var headers = headers + // add date, host, sha256 and if available security token headers + headers.add(name: "X-Amz-Date", value: dateString) + headers.add(name: "host", value: url.host ?? "") + headers.add(name: "x-amz-content-sha256", value: bodyHash) + if let sessionToken = credentials.sessionToken { + headers.add(name: "x-amz-security-token", value: sessionToken) + } + + // construct signing data. Do this after adding the headers as it uses data from the headers + let signingData = AWSSigner.SigningData(url: url, method: method, headers: headers, body: body, bodyHash: bodyHash, date: dateString, signer: self) + + // construct authorization string + let authorization = "AWS4-HMAC-SHA256 " + + "Credential=\(credentials.accessKeyId)/\(signingData.date)/\(region)/\(name)/aws4_request, " + + "SignedHeaders=\(signingData.signedHeaders), " + + "Signature=\(signature(signingData: signingData))" + + // add Authorization header + headers.add(name: "Authorization", value: authorization) + + return headers + } + + /// Generate a signed URL, for a HTTP request + public func signURL(url: URL, method: HTTPMethod = .GET, body: BodyData? = nil, date: Date = Date(), expires: Int = 86400) -> URL { + let headers = HTTPHeaders([("host", url.host ?? "")]) + // Create signing data + var signingData = AWSSigner.SigningData(url: url, method: method, headers: headers, body: body, date: AWSSigner.timestamp(date), signer: self) + // Construct query string. Start with original query strings and append all the signing info. + var query = url.query ?? "" + if query.count > 0 { + query += "&" + } + query += "X-Amz-Algorithm=AWS4-HMAC-SHA256" + query += "&X-Amz-Credential=\(credentials.accessKeyId)/\(signingData.date)/\(region)/\(name)/aws4_request" + query += "&X-Amz-Date=\(signingData.datetime)" + query += "&X-Amz-Expires=\(expires)" + query += "&X-Amz-SignedHeaders=\(signingData.signedHeaders)" + if let sessionToken = credentials.sessionToken { + query += "&X-Amz-Security-Token=\(sessionToken.uriEncode())" + } + // Split the string and sort to ensure the order of query strings is the same as AWS + query = query.split(separator: "&") + .sorted() + .joined(separator: "&") + .queryEncode() + + // update unsignedURL in the signingData so when the canonical request is constructed it includes all the signing query items + signingData.unsignedURL = URL(string: url.absoluteString.split(separator: "?")[0]+"?"+query)! // NEED TO DEAL WITH SITUATION WHERE THIS FAILS + query += "&X-Amz-Signature=\(signature(signingData: signingData))" + + // Add signature to query items and build a new Request + let signedURL = URL(string: url.absoluteString.split(separator: "?")[0]+"?"+query)! + + return signedURL + } + + /// structure used to store data used throughout the signing process + struct SigningData { + let url : URL + let method : HTTPMethod + let hashedPayload : String + let datetime : String + let headersToSign: [String: String] + let signedHeaders : String + var unsignedURL : URL + + var date : String { return String(datetime.prefix(8))} + + init(url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: BodyData? = nil, bodyHash: String? = nil, date: String, signer: AWSSigner) { + if url.path == "" { + //URL has to have trailing slash + self.url = url.appendingPathComponent("/") + } else { + self.url = url + } + self.method = method + self.datetime = date + self.unsignedURL = self.url + + if let hash = bodyHash { + self.hashedPayload = hash + } else if signer.name == "s3" { + self.hashedPayload = "UNSIGNED-PAYLOAD" + } else { + self.hashedPayload = AWSSigner.hashedPayload(body) + } + + let headersNotToSign: Set = [ + "Authorization" + ] + var headersToSign: [String: String] = [:] + var signedHeadersArray: [String] = [] + for header in headers { + if headersNotToSign.contains(header.name) { + continue + } + headersToSign[header.name] = header.value + signedHeadersArray.append(header.name.lowercased()) + } + self.headersToSign = headersToSign + self.signedHeaders = signedHeadersArray.sorted().joined(separator: ";") + } + } + + // Stage 3 Calculating signature as in https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html + func signature(signingData: SigningData) -> String { + let kDate = HMAC.authenticationCode(for: Data(signingData.date.utf8), using: SymmetricKey(data: Array("AWS4\(credentials.secretAccessKey)".utf8))) + let kRegion = HMAC.authenticationCode(for: Data(region.utf8), using: SymmetricKey(data: kDate)) + let kService = HMAC.authenticationCode(for: Data(name.utf8), using: SymmetricKey(data: kRegion)) + let kSigning = HMAC.authenticationCode(for: Data("aws4_request".utf8), using: SymmetricKey(data: kService)) + let kSignature = HMAC.authenticationCode(for: stringToSign(signingData: signingData), using: SymmetricKey(data: kSigning)) + return kSignature.hexDigest() + } + + /// Stage 2 Create the string to sign as in https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html + func stringToSign(signingData: SigningData) -> Data { + let stringToSign = "AWS4-HMAC-SHA256\n" + + "\(signingData.datetime)\n" + + "\(signingData.date)/\(region)/\(name)/aws4_request\n" + + SHA256.hash(data: canonicalRequest(signingData: signingData)).hexDigest() + return Data(stringToSign.utf8) + } + + /// Stage 1 Create the canonical request as in https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html + func canonicalRequest(signingData: SigningData) -> Data { + let canonicalHeaders = signingData.headersToSign.map { return "\($0.key.lowercased()):\($0.value.trimmingCharacters(in: CharacterSet.whitespaces))" } + .sorted() + .joined(separator: "\n") + let canonicalRequest = "\(signingData.method.rawValue)\n" + + "\(signingData.unsignedURL.path.uriEncodeWithSlash())\n" + + "\(signingData.unsignedURL.query ?? "")\n" + // should really uriEncode all the query string values + "\(canonicalHeaders)\n\n" + + "\(signingData.signedHeaders)\n" + + signingData.hashedPayload + return Data(canonicalRequest.utf8) + } + + /// Create a SHA256 hash of the Requests body + static func hashedPayload(_ payload: BodyData?) -> String { + guard let payload = payload else { return hashedEmptyBody } + let hash : String? + switch payload { + case .string(let string): + hash = SHA256.hash(data: Data(string.utf8)).hexDigest() + case .data(let data): + hash = SHA256.hash(data: data).hexDigest() + case .byteBuffer(let byteBuffer): + let byteBufferView = byteBuffer.readableBytesView + hash = byteBufferView.withContiguousStorageIfAvailable { bytes in + return SHA256.hash(data: bytes).hexDigest() + } + } + if let hash = hash { + return hash + } else { + return hashedEmptyBody + } + } + + /// return a hexEncoded string buffer from an array of bytes + static func hexEncoded(_ buffer: [UInt8]) -> String { + return buffer.map{String(format: "%02x", $0)}.joined(separator: "") + } + /// create timestamp dateformatter + static private func createTimeStampDateFormatter() -> DateFormatter { + let formatter = DateFormatter() + formatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'" + formatter.timeZone = TimeZone(abbreviation: "UTC") + formatter.locale = Locale(identifier: "en_US_POSIX") + return formatter + } + + /// return a timestamp formatted for signing requests + static func timestamp(_ date: Date) -> String { + return timeStampDateFormatter.string(from: date) + } +} + +extension String { + func queryEncode() -> String { + return addingPercentEncoding(withAllowedCharacters: String.queryAllowedCharacters) ?? self + } + + func uriEncode() -> String { + return addingPercentEncoding(withAllowedCharacters: String.uriAllowedCharacters) ?? self + } + + func uriEncodeWithSlash() -> String { + return addingPercentEncoding(withAllowedCharacters: String.uriAllowedWithSlashCharacters) ?? self + } + + static let uriAllowedWithSlashCharacters = CharacterSet(charactersIn:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~/") + static let uriAllowedCharacters = CharacterSet(charactersIn:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~") + static let queryAllowedCharacters = CharacterSet(charactersIn:"/;+").inverted +} + +public extension Sequence where Element == UInt8 { + /// return a hexEncoded string buffer from an array of bytes + func hexDigest() -> String { + return self.map{String(format: "%02x", $0)}.joined(separator: "") + } +} diff --git a/Sources/AWSLambdaDeployerHelper/main.swift b/Sources/AWSLambdaDeployerHelper/main.swift new file mode 100644 index 000000000..c28454c65 --- /dev/null +++ b/Sources/AWSLambdaDeployerHelper/main.swift @@ -0,0 +1 @@ +print("Deployer") \ No newline at end of file From 9f7b0433856ba522109960e541d05ab812e4e54e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 13 Nov 2024 15:34:30 +0100 Subject: [PATCH 03/27] change plugin invocation name for consistency --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index ff9475ed5..624d37bb4 100644 --- a/Package.swift +++ b/Package.swift @@ -68,7 +68,7 @@ let package = Package( name: "AWSLambdaPackager", capability: .command( intent: .custom( - verb: "archive", + verb: "lambda-build", description: "Archive the Lambda binary and prepare it for uploading to AWS. Requires docker on macOS or non Amazonlinux 2 distributions." ), From bd4634555ce11a7d967d4f54290cf951451ff57e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 16 Nov 2024 13:40:05 +0100 Subject: [PATCH 04/27] add implementation of AWS v4 Signer from aws-signer-v4 and HMAC+SHA256 from CryptoSwift --- Package.swift | 25 +- .../AWSLambdaPluginHelper/Extensions.swift | 48 +++ .../Vendored/crypto/Array+Extensions.swift | 155 +++++++ .../Vendored/crypto/Authenticator.swift | 20 + .../Vendored/crypto/BatchedCollections.swift | 81 ++++ .../Vendored/crypto/Bit.swift | 26 ++ .../crypto/Collections+Extensions.swift | 75 ++++ .../Vendored/crypto/Digest.swift | 92 +++++ .../Vendored/crypto/DigestType.swift | 32 ++ .../Vendored/crypto/Generics.swift | 57 +++ .../Vendored/crypto/HMAC.swift | 139 +++++++ .../Vendored/crypto/Int+Extension.swift | 47 +++ .../Vendored/crypto/NoPadding.swift | 41 ++ .../Vendored/crypto/Padding.swift | 79 ++++ .../Vendored/crypto/SHA1.swift | 176 ++++++++ .../Vendored/crypto/SHA2.swift | 386 ++++++++++++++++++ .../Vendored/crypto/SHA3.swift | 317 ++++++++++++++ .../Vendored/crypto/UInt16+Extension.swift | 51 +++ .../Vendored/crypto/UInt32+Extension.swift | 65 +++ .../Vendored/crypto/UInt64+Extension.swift | 58 +++ .../Vendored/crypto/UInt8+Extension.swift | 88 ++++ .../Vendored/crypto/Updatable.swift | 121 ++++++ .../Vendored/crypto/Utils.swift | 131 ++++++ .../Vendored/crypto/ZeroPadding.swift | 54 +++ .../Vendored/signer}/AWSCredentials.swift | 2 +- .../Vendored/signer}/AWSSigner.swift | 21 +- .../main.swift | 0 .../AWSSignerTests.swift | 57 +++ .../CryptoTests.swift | 78 ++++ 29 files changed, 2514 insertions(+), 8 deletions(-) create mode 100644 Sources/AWSLambdaPluginHelper/Extensions.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Array+Extensions.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Authenticator.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/BatchedCollections.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Bit.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Collections+Extensions.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Digest.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/DigestType.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Generics.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Int+Extension.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/NoPadding.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA1.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA3.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt16+Extension.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt32+Extension.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt64+Extension.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt8+Extension.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Updatable.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Utils.swift create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/ZeroPadding.swift rename Sources/{AWSLambdaDeployerHelper => AWSLambdaPluginHelper/Vendored/signer}/AWSCredentials.swift (96%) rename Sources/{AWSLambdaDeployerHelper => AWSLambdaPluginHelper/Vendored/signer}/AWSSigner.swift (92%) rename Sources/{AWSLambdaDeployerHelper => AWSLambdaPluginHelper}/main.swift (100%) create mode 100644 Tests/AWSLambdaPluginHelperTests/AWSSignerTests.swift create mode 100644 Tests/AWSLambdaPluginHelperTests/CryptoTests.swift diff --git a/Package.swift b/Package.swift index 5c5056232..f34b38eb7 100644 --- a/Package.swift +++ b/Package.swift @@ -14,23 +14,30 @@ let package = Package( 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 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"]), + .plugin(name: "AWSLambdaDeployer", targets: ["AWSLambdaDeployer"]), + + // an executable that implements the business logic for the plugins + .executable(name: "AWSLambdaPluginHelper", targets: ["AWSLambdaPluginHelper"]), + // for testing only .library(name: "AWSLambdaTesting", targets: ["AWSLambdaTesting"]), ], dependencies: [ .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"), - .package(url: "https://github.com/apple/swift-crypto.git", from: "3.9.1"), + // .package(url: "https://github.com/apple/swift-crypto.git", from: "3.9.1"), ], targets: [ .target( @@ -96,15 +103,14 @@ let package = Package( ] ), dependencies: [ - .target(name: "AWSLambdaDeployerHelper") + .target(name: "AWSLambdaPluginHelper") ] ), .executableTarget( - name: "AWSLambdaDeployerHelper", + name: "AWSLambdaPluginHelper", dependencies: [ .product(name: "NIOHTTP1", package: "swift-nio"), .product(name: "NIOCore", package: "swift-nio"), - .product(name: "Crypto", package: "swift-crypto"), ], swiftSettings: [.swiftLanguageMode(.v6)] ), @@ -148,5 +154,12 @@ let package = Package( ], swiftSettings: [.swiftLanguageMode(.v5)] ), + .testTarget( + name: "AWSLambdaPluginHelperTests", + dependencies: [ + .byName(name: "AWSLambdaPluginHelper") + ] + ), + ] ) diff --git a/Sources/AWSLambdaPluginHelper/Extensions.swift b/Sources/AWSLambdaPluginHelper/Extensions.swift new file mode 100644 index 000000000..671c4cec6 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Extensions.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 +// +//===----------------------------------------------------------------------===// + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// extension Array where Element == UInt8 { +// public var base64: String { +// Data(self).base64EncodedString() +// } +// } + +extension Data { + var bytes: [UInt8] { + return [UInt8](self) + } +} + +extension String { + public var array: [UInt8] { + Array(self.utf8) + } +} + +extension HMAC { + public static func authenticate(for data: [UInt8], using key: [UInt8], variant: HMAC.Variant = .sha2(.sha256)) throws -> [UInt8] { + let authenticator = HMAC(key: key, variant: variant) + return try authenticator.authenticate(data) + } + public static func authenticate(for data: Data, using key: [UInt8], variant: HMAC.Variant = .sha2(.sha256)) throws -> [UInt8] { + let authenticator = HMAC(key: key, variant: variant) + return try authenticator.authenticate(data.bytes) + } +} \ No newline at end of file diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Array+Extensions.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Array+Extensions.swift new file mode 100644 index 000000000..fd0631673 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Array+Extensions.swift @@ -0,0 +1,155 @@ +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +extension Array { + @inlinable + init(reserveCapacity: Int) { + self = Array() + self.reserveCapacity(reserveCapacity) + } + + @inlinable + var slice: ArraySlice { + self[self.startIndex ..< self.endIndex] + } + + @inlinable + subscript (safe index: Index) -> Element? { + return indices.contains(index) ? self[index] : nil + } +} + +extension Array where Element == UInt8 { + public init(hex: String) { + self.init(reserveCapacity: hex.unicodeScalars.lazy.underestimatedCount) + var buffer: UInt8? + var skip = hex.hasPrefix("0x") ? 2 : 0 + for char in hex.unicodeScalars.lazy { + guard skip == 0 else { + skip -= 1 + continue + } + guard char.value >= 48 && char.value <= 102 else { + removeAll() + return + } + let v: UInt8 + let c: UInt8 = UInt8(char.value) + switch c { + case let c where c <= 57: + v = c - 48 + case let c where c >= 65 && c <= 70: + v = c - 55 + case let c where c >= 97: + v = c - 87 + default: + removeAll() + return + } + if let b = buffer { + append(b << 4 | v) + buffer = nil + } else { + buffer = v + } + } + if let b = buffer { + append(b) + } + } + + public func toHexString() -> String { + `lazy`.reduce(into: "") { + var s = String($1, radix: 16) + if s.count == 1 { + s = "0" + s + } + $0 += s + } + } +} + +extension Array where Element == UInt8 { + /// split in chunks with given chunk size + @available(*, deprecated) + public func chunks(size chunksize: Int) -> Array> { + var words = Array>() + words.reserveCapacity(count / chunksize) + for idx in stride(from: chunksize, through: count, by: chunksize) { + words.append(Array(self[idx - chunksize ..< idx])) // slow for large table + } + let remainder = suffix(count % chunksize) + if !remainder.isEmpty { + words.append(Array(remainder)) + } + return words + } + + // public func md5() -> [Element] { + // Digest.md5(self) + // } + + // public func sha1() -> [Element] { + // Digest.sha1(self) + // } + + // public func sha224() -> [Element] { + // Digest.sha224(self) + // } + + public func sha256() -> [Element] { + Digest.sha256(self) + } + + public func sha384() -> [Element] { + Digest.sha384(self) + } + + public func sha512() -> [Element] { + Digest.sha512(self) + } + + public func sha2(_ variant: SHA2.Variant) -> [Element] { + Digest.sha2(self, variant: variant) + } + + public func sha3(_ variant: SHA3.Variant) -> [Element] { + Digest.sha3(self, variant: variant) + } + + // public func crc32(seed: UInt32? = nil, reflect: Bool = true) -> UInt32 { + // Checksum.crc32(self, seed: seed, reflect: reflect) + // } + + // public func crc32c(seed: UInt32? = nil, reflect: Bool = true) -> UInt32 { + // Checksum.crc32c(self, seed: seed, reflect: reflect) + // } + + // public func crc16(seed: UInt16? = nil) -> UInt16 { + // Checksum.crc16(self, seed: seed) + // } + + // public func encrypt(cipher: Cipher) throws -> [Element] { + // try cipher.encrypt(self.slice) + // } + + // public func decrypt(cipher: Cipher) throws -> [Element] { + // try cipher.decrypt(self.slice) + // } + + public func authenticate(with authenticator: A) throws -> [Element] { + try authenticator.authenticate(self) + } +} \ No newline at end of file diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Authenticator.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Authenticator.swift new file mode 100644 index 000000000..afabf51bc --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Authenticator.swift @@ -0,0 +1,20 @@ +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +/// Message authentication code. +public protocol Authenticator { + /// Calculate Message Authentication Code (MAC) for message. + func authenticate(_ bytes: Array) throws -> Array +} \ No newline at end of file diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/BatchedCollections.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/BatchedCollections.swift new file mode 100644 index 000000000..43efb56a3 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/BatchedCollections.swift @@ -0,0 +1,81 @@ +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +@usableFromInline +struct BatchedCollectionIndex { + let range: Range +} + +extension BatchedCollectionIndex: Comparable { + @usableFromInline + static func == (lhs: BatchedCollectionIndex, rhs: BatchedCollectionIndex) -> Bool { + lhs.range.lowerBound == rhs.range.lowerBound + } + + @usableFromInline + static func < (lhs: BatchedCollectionIndex, rhs: BatchedCollectionIndex) -> Bool { + lhs.range.lowerBound < rhs.range.lowerBound + } +} + +protocol BatchedCollectionType: Collection { + associatedtype Base: Collection +} + +@usableFromInline +struct BatchedCollection: Collection { + let base: Base + let size: Int + + @usableFromInline + init(base: Base, size: Int) { + self.base = base + self.size = size + } + + @usableFromInline + typealias Index = BatchedCollectionIndex + + private func nextBreak(after idx: Base.Index) -> Base.Index { + self.base.index(idx, offsetBy: self.size, limitedBy: self.base.endIndex) ?? self.base.endIndex + } + + @usableFromInline + var startIndex: Index { + Index(range: self.base.startIndex.. Index { + Index(range: idx.range.upperBound.. Base.SubSequence { + self.base[idx.range] + } +} + +extension Collection { + @inlinable + func batched(by size: Int) -> BatchedCollection { + BatchedCollection(base: self, size: size) + } +} \ No newline at end of file diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Bit.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Bit.swift new file mode 100644 index 000000000..3520b0dd4 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Bit.swift @@ -0,0 +1,26 @@ +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +public enum Bit: Int { + case zero + case one +} + +extension Bit { + @inlinable + func inverted() -> Bit { + self == .zero ? .one : .zero + } +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Collections+Extensions.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Collections+Extensions.swift new file mode 100644 index 000000000..84d68d176 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Collections+Extensions.swift @@ -0,0 +1,75 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// +extension Collection where Self.Element == UInt8, Self.Index == Int { + // Big endian order + @inlinable + func toUInt32Array() -> Array { + guard !isEmpty else { + return [] + } + + let c = strideCount(from: startIndex, to: endIndex, by: 4) + return Array(unsafeUninitializedCapacity: c) { buf, count in + var counter = 0 + for idx in stride(from: startIndex, to: endIndex, by: 4) { + let val = UInt32(bytes: self, fromIndex: idx).bigEndian + buf[counter] = val + counter += 1 + } + count = counter + assert(counter == c) + } + } + + // Big endian order + @inlinable + func toUInt64Array() -> Array { + guard !isEmpty else { + return [] + } + + let c = strideCount(from: startIndex, to: endIndex, by: 8) + return Array(unsafeUninitializedCapacity: c) { buf, count in + var counter = 0 + for idx in stride(from: startIndex, to: endIndex, by: 8) { + let val = UInt64(bytes: self, fromIndex: idx).bigEndian + buf[counter] = val + counter += 1 + } + count = counter + assert(counter == c) + } + } +} + +@usableFromInline +func strideCount(from: Int, to: Int, by: Int) -> Int { + let count = to - from + return count / by + (count % by > 0 ? 1 : 0) +} \ No newline at end of file diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Digest.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Digest.swift new file mode 100644 index 000000000..491f6fdbd --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Digest.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +@available(*, renamed: "Digest") +public typealias Hash = Digest + +/// Hash functions to calculate Digest. +public struct Digest { + /// Calculate MD5 Digest + /// - parameter bytes: input message + /// - returns: Digest bytes + // public static func md5(_ bytes: Array) -> Array { + // MD5().calculate(for: bytes) + // } + + /// Calculate SHA1 Digest + /// - parameter bytes: input message + /// - returns: Digest bytes + public static func sha1(_ bytes: Array) -> Array { + SHA1().calculate(for: bytes) + } + + /// Calculate SHA2-224 Digest + /// - parameter bytes: input message + /// - returns: Digest bytes + public static func sha224(_ bytes: Array) -> Array { + self.sha2(bytes, variant: .sha224) + } + + /// Calculate SHA2-256 Digest + /// - parameter bytes: input message + /// - returns: Digest bytes + public static func sha256(_ bytes: Array) -> Array { + self.sha2(bytes, variant: .sha256) + } + + /// Calculate SHA2-384 Digest + /// - parameter bytes: input message + /// - returns: Digest bytes + public static func sha384(_ bytes: Array) -> Array { + self.sha2(bytes, variant: .sha384) + } + + /// Calculate SHA2-512 Digest + /// - parameter bytes: input message + /// - returns: Digest bytes + public static func sha512(_ bytes: Array) -> Array { + self.sha2(bytes, variant: .sha512) + } + + /// Calculate SHA2 Digest + /// - parameter bytes: input message + /// - parameter variant: SHA-2 variant + /// - returns: Digest bytes + public static func sha2(_ bytes: Array, variant: SHA2.Variant) -> Array { + SHA2(variant: variant).calculate(for: bytes) + } + + /// Calculate SHA3 Digest + /// - parameter bytes: input message + /// - parameter variant: SHA-3 variant + /// - returns: Digest bytes + public static func sha3(_ bytes: Array, variant: SHA3.Variant) -> Array { + SHA3(variant: variant).calculate(for: bytes) + } +} \ No newline at end of file diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/DigestType.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/DigestType.swift new file mode 100644 index 000000000..13f27c1f0 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/DigestType.swift @@ -0,0 +1,32 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +internal protocol DigestType { + func calculate(for bytes: Array) -> Array +} \ No newline at end of file diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Generics.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Generics.swift new file mode 100644 index 000000000..2ba535d54 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Generics.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 +// +//===----------------------------------------------------------------------===// + +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +/// Array of bytes. Caution: don't use directly because generic is slow. +/// +/// - parameter value: integer value +/// - parameter length: length of output array. By default size of value type +/// +/// - returns: Array of bytes +@_specialize(where T == Int) +@_specialize(where T == UInt) +@_specialize(where T == UInt8) +@_specialize(where T == UInt16) +@_specialize(where T == UInt32) +@_specialize(where T == UInt64) +@inlinable +func arrayOfBytes(value: T, length totalBytes: Int = MemoryLayout.size) -> Array { + let valuePointer = UnsafeMutablePointer.allocate(capacity: 1) + valuePointer.pointee = value + + let bytesPointer = UnsafeMutablePointer(OpaquePointer(valuePointer)) + var bytes = Array(repeating: 0, count: totalBytes) + for j in 0...size, totalBytes) { + bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee + } + + valuePointer.deinitialize(count: 1) + valuePointer.deallocate() + + return bytes +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift new file mode 100644 index 000000000..b257eea45 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift @@ -0,0 +1,139 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +public final class HMAC: Authenticator { + public enum Error: Swift.Error { + case authenticateError + case invalidInput + } + + public enum Variant { + // case md5 + case sha1 + case sha2(SHA2.Variant) + case sha3(SHA3.Variant) + + @available(*, deprecated, message: "Use sha2(variant) instead.") + case sha256, sha384, sha512 + + var digestLength: Int { + switch self { + case .sha1: + return SHA1.digestLength + case .sha256: + return SHA2.Variant.sha256.digestLength + case .sha384: + return SHA2.Variant.sha384.digestLength + case .sha512: + return SHA2.Variant.sha512.digestLength + case .sha2(let variant): + return variant.digestLength + case .sha3(let variant): + return variant.digestLength + // case .md5: + // return MD5.digestLength + } + } + + func calculateHash(_ bytes: Array) -> Array { + switch self { + case .sha1: + return Digest.sha1(bytes) + case .sha256: + return Digest.sha256(bytes) + case .sha384: + return Digest.sha384(bytes) + case .sha512: + return Digest.sha512(bytes) + case .sha2(let variant): + return Digest.sha2(bytes, variant: variant) + case .sha3(let variant): + return Digest.sha3(bytes, variant: variant) + // case .md5: + // return Digest.md5(bytes) + } + } + + func blockSize() -> Int { + switch self { + // case .md5: + // return MD5.blockSize + case .sha1: + return SHA1.blockSize + case .sha256: + return SHA2.Variant.sha256.blockSize + case .sha384: + return SHA2.Variant.sha384.blockSize + case .sha512: + return SHA2.Variant.sha512.blockSize + case .sha2(let variant): + return variant.blockSize + case .sha3(let variant): + return variant.blockSize + } + } + } + + var key: Array + let variant: Variant + + // public init(key: Array, variant: HMAC.Variant = .md5) { + public init(key: Array, variant: HMAC.Variant = .sha2(.sha256)) { + self.variant = variant + self.key = key + + if key.count > variant.blockSize() { + let hash = variant.calculateHash(key) + self.key = hash + } + + if key.count < variant.blockSize() { + self.key = ZeroPadding().add(to: key, blockSize: variant.blockSize()) + } + } + + // MARK: Authenticator + + public func authenticate(_ bytes: Array) throws -> Array { + var opad = Array(repeating: 0x5c, count: variant.blockSize()) + for idx in self.key.indices { + opad[idx] = self.key[idx] ^ opad[idx] + } + var ipad = Array(repeating: 0x36, count: variant.blockSize()) + for idx in self.key.indices { + ipad[idx] = self.key[idx] ^ ipad[idx] + } + + let ipadAndMessageHash = self.variant.calculateHash(ipad + bytes) + let result = self.variant.calculateHash(opad + ipadAndMessageHash) + + // return Array(result[0..<10]) // 80 bits + return result + } +} \ No newline at end of file diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Int+Extension.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Int+Extension.swift new file mode 100644 index 000000000..b73a19cfa --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Int+Extension.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// +// CryptoSwift +// +// Created by Marcin Krzyzanowski on 12/08/14. +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(ucrt) +import ucrt +#endif + +extension FixedWidthInteger { + @inlinable + func bytes(totalBytes: Int = MemoryLayout.size) -> Array { + arrayOfBytes(value: self.littleEndian, length: totalBytes) + // TODO: adjust bytes order + // var value = self.littleEndian + // return withUnsafeBytes(of: &value, Array.init).reversed() + } +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/NoPadding.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/NoPadding.swift new file mode 100644 index 000000000..fa7f007ab --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/NoPadding.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +struct NoPadding: PaddingProtocol { + init() { + } + + func add(to data: Array, blockSize _: Int) -> Array { + data + } + + func remove(from data: Array, blockSize _: Int?) -> Array { + data + } +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift new file mode 100644 index 000000000..74e70e8d7 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.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 +// +//===----------------------------------------------------------------------===// + +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +public protocol PaddingProtocol { + func add(to: Array, blockSize: Int) -> Array + func remove(from: Array, blockSize: Int?) -> Array +} + +public enum Padding: PaddingProtocol { + case noPadding, zeroPadding //, pkcs7, pkcs5, eme_pkcs1v15, emsa_pkcs1v15, iso78164, iso10126 + + public func add(to: Array, blockSize: Int) -> Array { + switch self { + case .noPadding: + return to // NoPadding().add(to: to, blockSize: blockSize) + case .zeroPadding: + return ZeroPadding().add(to: to, blockSize: blockSize) + // case .pkcs7: + // return PKCS7.Padding().add(to: to, blockSize: blockSize) + // case .pkcs5: + // return PKCS5.Padding().add(to: to, blockSize: blockSize) + // case .eme_pkcs1v15: + // return EMEPKCS1v15Padding().add(to: to, blockSize: blockSize) + // case .emsa_pkcs1v15: + // return EMSAPKCS1v15Padding().add(to: to, blockSize: blockSize) + // case .iso78164: + // return ISO78164Padding().add(to: to, blockSize: blockSize) + // case .iso10126: + // return ISO10126Padding().add(to: to, blockSize: blockSize) + } + } + + public func remove(from: Array, blockSize: Int?) -> Array { + switch self { + case .noPadding: + return from //NoPadding().remove(from: from, blockSize: blockSize) + case .zeroPadding: + return ZeroPadding().remove(from: from, blockSize: blockSize) + // case .pkcs7: + // return PKCS7.Padding().remove(from: from, blockSize: blockSize) + // case .pkcs5: + // return PKCS5.Padding().remove(from: from, blockSize: blockSize) + // case .eme_pkcs1v15: + // return EMEPKCS1v15Padding().remove(from: from, blockSize: blockSize) + // case .emsa_pkcs1v15: + // return EMSAPKCS1v15Padding().remove(from: from, blockSize: blockSize) + // case .iso78164: + // return ISO78164Padding().remove(from: from, blockSize: blockSize) + // case .iso10126: + // return ISO10126Padding().remove(from: from, blockSize: blockSize) + } + } +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA1.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA1.swift new file mode 100644 index 000000000..46f57746e --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA1.swift @@ -0,0 +1,176 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +public final class SHA1: DigestType { + + @usableFromInline + static let digestLength: Int = 20 // 160 / 8 + + @usableFromInline + static let blockSize: Int = 64 + + @usableFromInline + static let hashInitialValue: ContiguousArray = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0] + + @usableFromInline + var accumulated = Array() + + @usableFromInline + var processedBytesTotalCount: Int = 0 + + @usableFromInline + var accumulatedHash: ContiguousArray = SHA1.hashInitialValue + + public init() { + } + + @inlinable + public func calculate(for bytes: Array) -> Array { + do { + return try update(withBytes: bytes.slice, isLast: true) + } catch { + return [] + } + } + + public func callAsFunction(_ bytes: Array) -> Array { + calculate(for: bytes) + } + + @usableFromInline + func process(block chunk: ArraySlice, currentHash hh: inout ContiguousArray) { + // break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15, big-endian + // Extend the sixteen 32-bit words into eighty 32-bit words: + let M = UnsafeMutablePointer.allocate(capacity: 80) + M.initialize(repeating: 0, count: 80) + defer { + M.deinitialize(count: 80) + M.deallocate() + } + + for x in 0..<80 { + switch x { + case 0...15: + let start = chunk.startIndex.advanced(by: x * 4) // * MemoryLayout.size + M[x] = UInt32(bytes: chunk, fromIndex: start) + default: + M[x] = rotateLeft(M[x - 3] ^ M[x - 8] ^ M[x - 14] ^ M[x - 16], by: 1) + } + } + + var A = hh[0] + var B = hh[1] + var C = hh[2] + var D = hh[3] + var E = hh[4] + + // Main loop + for j in 0...79 { + var f: UInt32 = 0 + var k: UInt32 = 0 + + switch j { + case 0...19: + f = (B & C) | ((~B) & D) + k = 0x5a827999 + case 20...39: + f = B ^ C ^ D + k = 0x6ed9eba1 + case 40...59: + f = (B & C) | (B & D) | (C & D) + k = 0x8f1bbcdc + case 60...79: + f = B ^ C ^ D + k = 0xca62c1d6 + default: + break + } + + let temp = rotateLeft(A, by: 5) &+ f &+ E &+ M[j] &+ k + E = D + D = C + C = rotateLeft(B, by: 30) + B = A + A = temp + } + + hh[0] = hh[0] &+ A + hh[1] = hh[1] &+ B + hh[2] = hh[2] &+ C + hh[3] = hh[3] &+ D + hh[4] = hh[4] &+ E + } +} + +extension SHA1: Updatable { + @discardableResult @inlinable + public func update(withBytes bytes: ArraySlice, isLast: Bool = false) throws -> Array { + self.accumulated += bytes + + if isLast { + let lengthInBits = (processedBytesTotalCount + self.accumulated.count) * 8 + let lengthBytes = lengthInBits.bytes(totalBytes: 64 / 8) // A 64-bit representation of b + + // Step 1. Append padding + bitPadding(to: &self.accumulated, blockSize: SHA1.blockSize, allowance: 64 / 8) + + // Step 2. Append Length a 64-bit representation of lengthInBits + self.accumulated += lengthBytes + } + + var processedBytes = 0 + for chunk in self.accumulated.batched(by: SHA1.blockSize) { + if isLast || (self.accumulated.count - processedBytes) >= SHA1.blockSize { + self.process(block: chunk, currentHash: &self.accumulatedHash) + processedBytes += chunk.count + } + } + self.accumulated.removeFirst(processedBytes) + self.processedBytesTotalCount += processedBytes + + // output current hash + var result = Array(repeating: 0, count: SHA1.digestLength) + var pos = 0 + for idx in 0..> 24) & 0xff) + result[pos + 1] = UInt8((h >> 16) & 0xff) + result[pos + 2] = UInt8((h >> 8) & 0xff) + result[pos + 3] = UInt8(h & 0xff) + pos += 4 + } + + // reset hash value for instance + if isLast { + self.accumulatedHash = SHA1.hashInitialValue + } + + return result + } +} \ No newline at end of file diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift new file mode 100644 index 000000000..9d4e4aca9 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift @@ -0,0 +1,386 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +// TODO: generic for process32/64 (UInt32/UInt64) +// + +public final class SHA2: DigestType { + @usableFromInline + let variant: Variant + + @usableFromInline + let size: Int + + @usableFromInline + let blockSize: Int + + @usableFromInline + let digestLength: Int + + private let k: Array + + @usableFromInline + var accumulated = Array() + + @usableFromInline + var processedBytesTotalCount: Int = 0 + + @usableFromInline + var accumulatedHash32 = Array() + + @usableFromInline + var accumulatedHash64 = Array() + + @frozen + public enum Variant: RawRepresentable { + case sha224, sha256, sha384, sha512 + + public var digestLength: Int { + self.rawValue / 8 + } + + public var blockSize: Int { + switch self { + case .sha224, .sha256: + return 64 + case .sha384, .sha512: + return 128 + } + } + + public typealias RawValue = Int + public var rawValue: RawValue { + switch self { + case .sha224: + return 224 + case .sha256: + return 256 + case .sha384: + return 384 + case .sha512: + return 512 + } + } + + public init?(rawValue: RawValue) { + switch rawValue { + case 224: + self = .sha224 + case 256: + self = .sha256 + case 384: + self = .sha384 + case 512: + self = .sha512 + default: + return nil + } + } + + @usableFromInline + var h: Array { + switch self { + case .sha224: + return [0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939, 0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4] + case .sha256: + return [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19] + case .sha384: + return [0xcbbb9d5dc1059ed8, 0x629a292a367cd507, 0x9159015a3070dd17, 0x152fecd8f70e5939, 0x67332667ffc00b31, 0x8eb44a8768581511, 0xdb0c2e0d64f98fa7, 0x47b5481dbefa4fa4] + case .sha512: + return [0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, 0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179] + } + } + + @usableFromInline + var finalLength: Int { + switch self { + case .sha224: + return 7 + case .sha384: + return 6 + default: + return Int.max + } + } + } + + public init(variant: SHA2.Variant) { + self.variant = variant + switch self.variant { + case .sha224, .sha256: + self.accumulatedHash32 = variant.h.map { UInt32($0) } // FIXME: UInt64 for process64 + self.blockSize = variant.blockSize + self.size = variant.rawValue + self.digestLength = variant.digestLength + self.k = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + ] + case .sha384, .sha512: + self.accumulatedHash64 = variant.h + self.blockSize = variant.blockSize + self.size = variant.rawValue + self.digestLength = variant.digestLength + self.k = [ + 0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc, 0x3956c25bf348b538, + 0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118, 0xd807aa98a3030242, 0x12835b0145706fbe, + 0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2, 0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235, + 0xc19bf174cf692694, 0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65, + 0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5, 0x983e5152ee66dfab, + 0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4, 0xc6e00bf33da88fc2, 0xd5a79147930aa725, + 0x06ca6351e003826f, 0x142929670a0e6e70, 0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed, + 0x53380d139d95b3df, 0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b, + 0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30, 0xd192e819d6ef5218, + 0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8, 0x19a4c116b8d2d0c8, 0x1e376c085141ab53, + 0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8, 0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373, + 0x682e6ff3d6b2b8a3, 0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec, + 0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b, 0xca273eceea26619c, + 0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178, 0x06f067aa72176fba, 0x0a637dc5a2c898a6, + 0x113f9804bef90dae, 0x1b710b35131c471b, 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc, + 0x431d67c49c100d4c, 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817 + ] + } + } + + @inlinable + public func calculate(for bytes: Array) -> Array { + do { + return try update(withBytes: bytes.slice, isLast: true) + } catch { + return [] + } + } + + public func callAsFunction(_ bytes: Array) -> Array { + calculate(for: bytes) + } + + @usableFromInline + func process64(block chunk: ArraySlice, currentHash hh: inout Array) { + // break chunk into sixteen 64-bit words M[j], 0 ≤ j ≤ 15, big-endian + // Extend the sixteen 64-bit words into eighty 64-bit words: + let M = UnsafeMutablePointer.allocate(capacity: self.k.count) + M.initialize(repeating: 0, count: self.k.count) + defer { + M.deinitialize(count: self.k.count) + M.deallocate() + } + for x in 0...size + M[x] = UInt64(bytes: chunk, fromIndex: start) + default: + let s0 = rotateRight(M[x - 15], by: 1) ^ rotateRight(M[x - 15], by: 8) ^ (M[x - 15] >> 7) + let s1 = rotateRight(M[x - 2], by: 19) ^ rotateRight(M[x - 2], by: 61) ^ (M[x - 2] >> 6) + M[x] = M[x - 16] &+ s0 &+ M[x - 7] &+ s1 + } + } + + var A = hh[0] + var B = hh[1] + var C = hh[2] + var D = hh[3] + var E = hh[4] + var F = hh[5] + var G = hh[6] + var H = hh[7] + + // Main loop + for j in 0.., currentHash hh: inout Array) { + // break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15, big-endian + // Extend the sixteen 32-bit words into sixty-four 32-bit words: + let M = UnsafeMutablePointer.allocate(capacity: self.k.count) + M.initialize(repeating: 0, count: self.k.count) + defer { + M.deinitialize(count: self.k.count) + M.deallocate() + } + + for x in 0...size + M[x] = UInt32(bytes: chunk, fromIndex: start) + default: + let s0 = rotateRight(M[x - 15], by: 7) ^ rotateRight(M[x - 15], by: 18) ^ (M[x - 15] >> 3) + let s1 = rotateRight(M[x - 2], by: 17) ^ rotateRight(M[x - 2], by: 19) ^ (M[x - 2] >> 10) + M[x] = M[x - 16] &+ s0 &+ M[x - 7] &+ s1 + } + } + + var A = hh[0] + var B = hh[1] + var C = hh[2] + var D = hh[3] + var E = hh[4] + var F = hh[5] + var G = hh[6] + var H = hh[7] + + // Main loop + for j in 0.., isLast: Bool = false) throws -> Array { + self.accumulated += bytes + + if isLast { + let lengthInBits = (processedBytesTotalCount + self.accumulated.count) * 8 + let lengthBytes = lengthInBits.bytes(totalBytes: self.blockSize / 8) // A 64-bit/128-bit representation of b. blockSize fit by accident. + + // Step 1. Append padding + bitPadding(to: &self.accumulated, blockSize: self.blockSize, allowance: self.blockSize / 8) + + // Step 2. Append Length a 64-bit representation of lengthInBits + self.accumulated += lengthBytes + } + + var processedBytes = 0 + for chunk in self.accumulated.batched(by: self.blockSize) { + if isLast || (self.accumulated.count - processedBytes) >= self.blockSize { + switch self.variant { + case .sha224, .sha256: + self.process32(block: chunk, currentHash: &self.accumulatedHash32) + case .sha384, .sha512: + self.process64(block: chunk, currentHash: &self.accumulatedHash64) + } + processedBytes += chunk.count + } + } + self.accumulated.removeFirst(processedBytes) + self.processedBytesTotalCount += processedBytes + + // output current hash + var result = Array(repeating: 0, count: variant.digestLength) + switch self.variant { + case .sha224, .sha256: + var pos = 0 + for idx in 0..> 24) & 0xff) + result[pos + 1] = UInt8((h >> 16) & 0xff) + result[pos + 2] = UInt8((h >> 8) & 0xff) + result[pos + 3] = UInt8(h & 0xff) + pos += 4 + } + case .sha384, .sha512: + var pos = 0 + for idx in 0..> 56) & 0xff) + result[pos + 1] = UInt8((h >> 48) & 0xff) + result[pos + 2] = UInt8((h >> 40) & 0xff) + result[pos + 3] = UInt8((h >> 32) & 0xff) + result[pos + 4] = UInt8((h >> 24) & 0xff) + result[pos + 5] = UInt8((h >> 16) & 0xff) + result[pos + 6] = UInt8((h >> 8) & 0xff) + result[pos + 7] = UInt8(h & 0xff) + pos += 8 + } + } + + // reset hash value for instance + if isLast { + switch self.variant { + case .sha224, .sha256: + self.accumulatedHash32 = self.variant.h.lazy.map { UInt32($0) } // FIXME: UInt64 for process64 + case .sha384, .sha512: + self.accumulatedHash64 = self.variant.h + } + } + + return result + } +} \ No newline at end of file diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA3.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA3.swift new file mode 100644 index 000000000..24027062c --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA3.swift @@ -0,0 +1,317 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +// http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf +// http://keccak.noekeon.org/specs_summary.html +// + +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(ucrt) +import ucrt +#endif + +public final class SHA3: DigestType { + let round_constants: Array = [ + 0x0000000000000001, 0x0000000000008082, 0x800000000000808a, 0x8000000080008000, + 0x000000000000808b, 0x0000000080000001, 0x8000000080008081, 0x8000000000008009, + 0x000000000000008a, 0x0000000000000088, 0x0000000080008009, 0x000000008000000a, + 0x000000008000808b, 0x800000000000008b, 0x8000000000008089, 0x8000000000008003, + 0x8000000000008002, 0x8000000000000080, 0x000000000000800a, 0x800000008000000a, + 0x8000000080008081, 0x8000000000008080, 0x0000000080000001, 0x8000000080008008 + ] + + public let blockSize: Int + public let digestLength: Int + public let markByte: UInt8 + + @usableFromInline + var accumulated = Array() + + + @usableFromInline + var accumulatedHash: Array + + public enum Variant { + case sha224, sha256, sha384, sha512, keccak224, keccak256, keccak384, keccak512 + + var digestLength: Int { + 100 - (self.blockSize / 2) + } + + var blockSize: Int { + (1600 - self.outputLength * 2) / 8 + } + + var markByte: UInt8 { + switch self { + case .sha224, .sha256, .sha384, .sha512: + return 0x06 // 0x1F for SHAKE + case .keccak224, .keccak256, .keccak384, .keccak512: + return 0x01 + } + } + + public var outputLength: Int { + switch self { + case .sha224, .keccak224: + return 224 + case .sha256, .keccak256: + return 256 + case .sha384, .keccak384: + return 384 + case .sha512, .keccak512: + return 512 + } + } + } + + public init(variant: SHA3.Variant) { + self.blockSize = variant.blockSize + self.digestLength = variant.digestLength + self.markByte = variant.markByte + self.accumulatedHash = Array(repeating: 0, count: self.digestLength) + } + + @inlinable + public func calculate(for bytes: Array) -> Array { + do { + return try update(withBytes: bytes.slice, isLast: true) + } catch { + return [] + } + } + + public func callAsFunction(_ bytes: Array) -> Array { + calculate(for: bytes) + } + + /// 1. For all pairs (x,z) such that 0≤x<5 and 0≤z) { + let c = UnsafeMutablePointer.allocate(capacity: 5) + c.initialize(repeating: 0, count: 5) + defer { + c.deinitialize(count: 5) + c.deallocate() + } + let d = UnsafeMutablePointer.allocate(capacity: 5) + d.initialize(repeating: 0, count: 5) + defer { + d.deinitialize(count: 5) + d.deallocate() + } + + for i in 0..<5 { + c[i] = a[i] ^ a[i &+ 5] ^ a[i &+ 10] ^ a[i &+ 15] ^ a[i &+ 20] + } + + d[0] = rotateLeft(c[1], by: 1) ^ c[4] + d[1] = rotateLeft(c[2], by: 1) ^ c[0] + d[2] = rotateLeft(c[3], by: 1) ^ c[1] + d[3] = rotateLeft(c[4], by: 1) ^ c[2] + d[4] = rotateLeft(c[0], by: 1) ^ c[3] + + for i in 0..<5 { + a[i] ^= d[i] + a[i &+ 5] ^= d[i] + a[i &+ 10] ^= d[i] + a[i &+ 15] ^= d[i] + a[i &+ 20] ^= d[i] + } + } + + /// A′[x, y, z]=A[(x &+ 3y) mod 5, x, z] + private func π(_ a: inout Array) { + let a1 = a[1] + a[1] = a[6] + a[6] = a[9] + a[9] = a[22] + a[22] = a[14] + a[14] = a[20] + a[20] = a[2] + a[2] = a[12] + a[12] = a[13] + a[13] = a[19] + a[19] = a[23] + a[23] = a[15] + a[15] = a[4] + a[4] = a[24] + a[24] = a[21] + a[21] = a[8] + a[8] = a[16] + a[16] = a[5] + a[5] = a[3] + a[3] = a[18] + a[18] = a[17] + a[17] = a[11] + a[11] = a[7] + a[7] = a[10] + a[10] = a1 + } + + /// For all triples (x, y, z) such that 0≤x<5, 0≤y<5, and 0≤z) { + for i in stride(from: 0, to: 25, by: 5) { + let a0 = a[0 &+ i] + let a1 = a[1 &+ i] + a[0 &+ i] ^= ~a1 & a[2 &+ i] + a[1 &+ i] ^= ~a[2 &+ i] & a[3 &+ i] + a[2 &+ i] ^= ~a[3 &+ i] & a[4 &+ i] + a[3 &+ i] ^= ~a[4 &+ i] & a0 + a[4 &+ i] ^= ~a0 & a1 + } + } + + private func ι(_ a: inout Array, round: Int) { + a[0] ^= self.round_constants[round] + } + + @usableFromInline + func process(block chunk: ArraySlice, currentHash hh: inout Array) { + // expand + hh[0] ^= chunk[0].littleEndian + hh[1] ^= chunk[1].littleEndian + hh[2] ^= chunk[2].littleEndian + hh[3] ^= chunk[3].littleEndian + hh[4] ^= chunk[4].littleEndian + hh[5] ^= chunk[5].littleEndian + hh[6] ^= chunk[6].littleEndian + hh[7] ^= chunk[7].littleEndian + hh[8] ^= chunk[8].littleEndian + if self.blockSize > 72 { // 72 / 8, sha-512 + hh[9] ^= chunk[9].littleEndian + hh[10] ^= chunk[10].littleEndian + hh[11] ^= chunk[11].littleEndian + hh[12] ^= chunk[12].littleEndian + if self.blockSize > 104 { // 104 / 8, sha-384 + hh[13] ^= chunk[13].littleEndian + hh[14] ^= chunk[14].littleEndian + hh[15] ^= chunk[15].littleEndian + hh[16] ^= chunk[16].littleEndian + if self.blockSize > 136 { // 136 / 8, sha-256 + hh[17] ^= chunk[17].littleEndian + // FULL_SHA3_FAMILY_SUPPORT + if self.blockSize > 144 { // 144 / 8, sha-224 + hh[18] ^= chunk[18].littleEndian + hh[19] ^= chunk[19].littleEndian + hh[20] ^= chunk[20].littleEndian + hh[21] ^= chunk[21].littleEndian + hh[22] ^= chunk[22].littleEndian + hh[23] ^= chunk[23].littleEndian + hh[24] ^= chunk[24].littleEndian + } + } + } + } + + // Keccak-f + for round in 0..<24 { + self.θ(&hh) + + hh[1] = rotateLeft(hh[1], by: 1) + hh[2] = rotateLeft(hh[2], by: 62) + hh[3] = rotateLeft(hh[3], by: 28) + hh[4] = rotateLeft(hh[4], by: 27) + hh[5] = rotateLeft(hh[5], by: 36) + hh[6] = rotateLeft(hh[6], by: 44) + hh[7] = rotateLeft(hh[7], by: 6) + hh[8] = rotateLeft(hh[8], by: 55) + hh[9] = rotateLeft(hh[9], by: 20) + hh[10] = rotateLeft(hh[10], by: 3) + hh[11] = rotateLeft(hh[11], by: 10) + hh[12] = rotateLeft(hh[12], by: 43) + hh[13] = rotateLeft(hh[13], by: 25) + hh[14] = rotateLeft(hh[14], by: 39) + hh[15] = rotateLeft(hh[15], by: 41) + hh[16] = rotateLeft(hh[16], by: 45) + hh[17] = rotateLeft(hh[17], by: 15) + hh[18] = rotateLeft(hh[18], by: 21) + hh[19] = rotateLeft(hh[19], by: 8) + hh[20] = rotateLeft(hh[20], by: 18) + hh[21] = rotateLeft(hh[21], by: 2) + hh[22] = rotateLeft(hh[22], by: 61) + hh[23] = rotateLeft(hh[23], by: 56) + hh[24] = rotateLeft(hh[24], by: 14) + + self.π(&hh) + self.χ(&hh) + self.ι(&hh, round: round) + } + } +} + +extension SHA3: Updatable { + + @inlinable + public func update(withBytes bytes: ArraySlice, isLast: Bool = false) throws -> Array { + self.accumulated += bytes + + if isLast { + // Add padding + let markByteIndex = self.accumulated.count + + // We need to always pad the input. Even if the input is a multiple of blockSize. + let r = self.blockSize * 8 + let q = (r / 8) - (accumulated.count % (r / 8)) + self.accumulated += Array(repeating: 0, count: q) + + self.accumulated[markByteIndex] |= self.markByte + self.accumulated[self.accumulated.count - 1] |= 0x80 + } + + var processedBytes = 0 + for chunk in self.accumulated.batched(by: self.blockSize) { + if isLast || (self.accumulated.count - processedBytes) >= self.blockSize { + self.process(block: chunk.toUInt64Array().slice, currentHash: &self.accumulatedHash) + processedBytes += chunk.count + } + } + self.accumulated.removeFirst(processedBytes) + + // TODO: verify performance, reduce vs for..in + let result = self.accumulatedHash.reduce(into: Array()) { (result, value) in + result += value.bigEndian.bytes() + } + + // reset hash value for instance + if isLast { + self.accumulatedHash = Array(repeating: 0, count: self.digestLength) + } + + return Array(result[0.. +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +/** array of bytes */ +extension UInt16 { + @_specialize(where T == ArraySlice) + init(bytes: T) where T.Element == UInt8, T.Index == Int { + self = UInt16(bytes: bytes, fromIndex: bytes.startIndex) + } + + @_specialize(where T == ArraySlice) + init(bytes: T, fromIndex index: T.Index) where T.Element == UInt8, T.Index == Int { + if bytes.isEmpty { + self = 0 + return + } + + let count = bytes.count + + let val0 = count > 0 ? UInt16(bytes[index.advanced(by: 0)]) << 8 : 0 + let val1 = count > 1 ? UInt16(bytes[index.advanced(by: 1)]) : 0 + + self = val0 | val1 + } +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt32+Extension.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt32+Extension.swift new file mode 100644 index 000000000..18e1599a9 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt32+Extension.swift @@ -0,0 +1,65 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(ucrt) +import ucrt +#endif + +protocol _UInt32Type {} +extension UInt32: _UInt32Type {} + +/** array of bytes */ +extension UInt32 { + @_specialize(where T == ArraySlice) + init(bytes: T) where T.Element == UInt8, T.Index == Int { + self = UInt32(bytes: bytes, fromIndex: bytes.startIndex) + } + + @_specialize(where T == ArraySlice) + @inlinable + init(bytes: T, fromIndex index: T.Index) where T.Element == UInt8, T.Index == Int { + if bytes.isEmpty { + self = 0 + return + } + + let count = bytes.count + + let val0 = count > 0 ? UInt32(bytes[index.advanced(by: 0)]) << 24 : 0 + let val1 = count > 1 ? UInt32(bytes[index.advanced(by: 1)]) << 16 : 0 + let val2 = count > 2 ? UInt32(bytes[index.advanced(by: 2)]) << 8 : 0 + let val3 = count > 3 ? UInt32(bytes[index.advanced(by: 3)]) : 0 + + self = val0 | val1 | val2 | val3 + } +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt64+Extension.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt64+Extension.swift new file mode 100644 index 000000000..1bbdc79bf --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt64+Extension.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +/** array of bytes */ +extension UInt64 { + @_specialize(where T == ArraySlice) + init(bytes: T) where T.Element == UInt8, T.Index == Int { + self = UInt64(bytes: bytes, fromIndex: bytes.startIndex) + } + + @_specialize(where T == ArraySlice) + @inlinable + init(bytes: T, fromIndex index: T.Index) where T.Element == UInt8, T.Index == Int { + if bytes.isEmpty { + self = 0 + return + } + + let count = bytes.count + + let val0 = count > 0 ? UInt64(bytes[index.advanced(by: 0)]) << 56 : 0 + let val1 = count > 1 ? UInt64(bytes[index.advanced(by: 1)]) << 48 : 0 + let val2 = count > 2 ? UInt64(bytes[index.advanced(by: 2)]) << 40 : 0 + let val3 = count > 3 ? UInt64(bytes[index.advanced(by: 3)]) << 32 : 0 + let val4 = count > 4 ? UInt64(bytes[index.advanced(by: 4)]) << 24 : 0 + let val5 = count > 5 ? UInt64(bytes[index.advanced(by: 5)]) << 16 : 0 + let val6 = count > 6 ? UInt64(bytes[index.advanced(by: 6)]) << 8 : 0 + let val7 = count > 7 ? UInt64(bytes[index.advanced(by: 7)]) : 0 + + self = val0 | val1 | val2 | val3 | val4 | val5 | val6 | val7 + } +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt8+Extension.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt8+Extension.swift new file mode 100644 index 000000000..b65ed5f5e --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt8+Extension.swift @@ -0,0 +1,88 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(ucrt) +import ucrt +#endif + +public protocol _UInt8Type {} +extension UInt8: _UInt8Type {} + +/** casting */ +extension UInt8 { + /** cast because UInt8() because std initializer crash if value is > byte */ + static func with(value: UInt64) -> UInt8 { + let tmp = value & 0xff + return UInt8(tmp) + } + + static func with(value: UInt32) -> UInt8 { + let tmp = value & 0xff + return UInt8(tmp) + } + + static func with(value: UInt16) -> UInt8 { + let tmp = value & 0xff + return UInt8(tmp) + } +} + +/** Bits */ +extension UInt8 { + /** array of bits */ + public func bits() -> [Bit] { + let totalBitsCount = MemoryLayout.size * 8 + + var bitsArray = [Bit](repeating: Bit.zero, count: totalBitsCount) + + for j in 0.. String { + var s = String() + let arr: [Bit] = self.bits() + for idx in arr.indices { + s += (arr[idx] == Bit.one ? "1" : "0") + if idx.advanced(by: 1) % 8 == 0 { s += " " } + } + return s + } +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Updatable.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Updatable.swift new file mode 100644 index 000000000..e54fc60df --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Updatable.swift @@ -0,0 +1,121 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +/// A type that supports incremental updates. For example Digest or Cipher may be updatable +/// and calculate result incerementally. +public protocol Updatable { + /// Update given bytes in chunks. + /// + /// - parameter bytes: Bytes to process. + /// - parameter isLast: Indicate if given chunk is the last one. No more updates after this call. + /// - returns: Processed partial result data or empty array. + mutating func update(withBytes bytes: ArraySlice, isLast: Bool) throws -> Array + + /// Update given bytes in chunks. + /// + /// - Parameters: + /// - bytes: Bytes to process. + /// - isLast: Indicate if given chunk is the last one. No more updates after this call. + /// - output: Resulting bytes callback. + /// - Returns: Processed partial result data or empty array. + mutating func update(withBytes bytes: ArraySlice, isLast: Bool, output: (_ bytes: Array) -> Void) throws +} + +extension Updatable { + @inlinable + public mutating func update(withBytes bytes: ArraySlice, isLast: Bool = false, output: (_ bytes: Array) -> Void) throws { + let processed = try update(withBytes: bytes, isLast: isLast) + if !processed.isEmpty { + output(processed) + } + } + + @inlinable + public mutating func update(withBytes bytes: ArraySlice, isLast: Bool = false) throws -> Array { + try self.update(withBytes: bytes, isLast: isLast) + } + + @inlinable + public mutating func update(withBytes bytes: Array, isLast: Bool = false) throws -> Array { + try self.update(withBytes: bytes.slice, isLast: isLast) + } + + @inlinable + public mutating func update(withBytes bytes: Array, isLast: Bool = false, output: (_ bytes: Array) -> Void) throws { + try self.update(withBytes: bytes.slice, isLast: isLast, output: output) + } + + /// Finish updates. This may apply padding. + /// - parameter bytes: Bytes to process + /// - returns: Processed data. + @inlinable + public mutating func finish(withBytes bytes: ArraySlice) throws -> Array { + try self.update(withBytes: bytes, isLast: true) + } + + @inlinable + public mutating func finish(withBytes bytes: Array) throws -> Array { + try self.finish(withBytes: bytes.slice) + } + + /// Finish updates. May add padding. + /// + /// - Returns: Processed data + /// - Throws: Error + @inlinable + public mutating func finish() throws -> Array { + try self.update(withBytes: [], isLast: true) + } + + /// Finish updates. This may apply padding. + /// - parameter bytes: Bytes to process + /// - parameter output: Resulting data + /// - returns: Processed data. + @inlinable + public mutating func finish(withBytes bytes: ArraySlice, output: (_ bytes: Array) -> Void) throws { + let processed = try update(withBytes: bytes, isLast: true) + if !processed.isEmpty { + output(processed) + } + } + + @inlinable + public mutating func finish(withBytes bytes: Array, output: (_ bytes: Array) -> Void) throws { + try self.finish(withBytes: bytes.slice, output: output) + } + + /// Finish updates. May add padding. + /// + /// - Parameter output: Processed data + /// - Throws: Error + @inlinable + public mutating func finish(output: (Array) -> Void) throws { + try self.finish(withBytes: [], output: output) + } +} \ No newline at end of file diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Utils.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Utils.swift new file mode 100644 index 000000000..6a5ad9fd5 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Utils.swift @@ -0,0 +1,131 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +@inlinable +func rotateLeft(_ value: UInt8, by: UInt8) -> UInt8 { + ((value << by) & 0xff) | (value >> (8 - by)) +} + +@inlinable +func rotateLeft(_ value: UInt16, by: UInt16) -> UInt16 { + ((value << by) & 0xffff) | (value >> (16 - by)) +} + +@inlinable +func rotateLeft(_ value: UInt32, by: UInt32) -> UInt32 { + ((value << by) & 0xffffffff) | (value >> (32 - by)) +} + +@inlinable +func rotateLeft(_ value: UInt64, by: UInt64) -> UInt64 { + (value << by) | (value >> (64 - by)) +} + +@inlinable +func rotateRight(_ value: UInt16, by: UInt16) -> UInt16 { + (value >> by) | (value << (16 - by)) +} + +@inlinable +func rotateRight(_ value: UInt32, by: UInt32) -> UInt32 { + (value >> by) | (value << (32 - by)) +} + +@inlinable +func rotateRight(_ value: UInt64, by: UInt64) -> UInt64 { + ((value >> by) | (value << (64 - by))) +} + +@inlinable +func reversed(_ uint8: UInt8) -> UInt8 { + var v = uint8 + v = (v & 0xf0) >> 4 | (v & 0x0f) << 4 + v = (v & 0xcc) >> 2 | (v & 0x33) << 2 + v = (v & 0xaa) >> 1 | (v & 0x55) << 1 + return v +} + +@inlinable +func reversed(_ uint32: UInt32) -> UInt32 { + var v = uint32 + v = ((v >> 1) & 0x55555555) | ((v & 0x55555555) << 1) + v = ((v >> 2) & 0x33333333) | ((v & 0x33333333) << 2) + v = ((v >> 4) & 0x0f0f0f0f) | ((v & 0x0f0f0f0f) << 4) + v = ((v >> 8) & 0x00ff00ff) | ((v & 0x00ff00ff) << 8) + v = ((v >> 16) & 0xffff) | ((v & 0xffff) << 16) + return v +} + +@inlinable +func xor(_ left: T, _ right: V) -> ArraySlice where T: RandomAccessCollection, V: RandomAccessCollection, T.Element == UInt8, T.Index == Int, V.Element == UInt8, V.Index == Int { + return xor(left, right).slice +} + +@inlinable +func xor(_ left: T, _ right: V) -> Array where T: RandomAccessCollection, V: RandomAccessCollection, T.Element == UInt8, T.Index == Int, V.Element == UInt8, V.Index == Int { + let length = Swift.min(left.count, right.count) + + let buf = UnsafeMutablePointer.allocate(capacity: length) + buf.initialize(repeating: 0, count: length) + defer { + buf.deinitialize(count: length) + buf.deallocate() + } + + // xor + for i in 0.., blockSize: Int, allowance: Int = 0) { + let msgLength = data.count + // Step 1. Append Padding Bits + // append one bit (UInt8 with one bit) to message + data.append(0x80) + + // Step 2. append "0" bit until message length in bits ≡ 448 (mod 512) + let max = blockSize - allowance // 448, 986 + if msgLength % blockSize < max { // 448 + data += Array(repeating: 0, count: max - 1 - (msgLength % blockSize)) + } else { + data += Array(repeating: 0, count: blockSize + max - 1 - (msgLength % blockSize)) + } +} \ No newline at end of file diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/ZeroPadding.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/ZeroPadding.swift new file mode 100644 index 000000000..f6846a45f --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/ZeroPadding.swift @@ -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 +// +//===----------------------------------------------------------------------===// + +// +// CryptoSwift +// +// Copyright (C) 2014-2022 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +/// All the bytes that are required to be padded are padded with zero. +/// Zero padding may not be reversible if the original file ends with one or more zero bytes. +struct ZeroPadding: PaddingProtocol { + init() { + } + + @inlinable + func add(to bytes: Array, blockSize: Int) -> Array { + let paddingCount = blockSize - (bytes.count % blockSize) + if paddingCount > 0 { + return bytes + Array(repeating: 0, count: paddingCount) + } + return bytes + } + + @inlinable + func remove(from bytes: Array, blockSize _: Int?) -> Array { + for (idx, value) in bytes.reversed().enumerated() { + if value != 0 { + return Array(bytes[0.. String { + + var signature = "" + do { + let kDate = try HMAC.authenticate(for: Data(signingData.date.utf8), using: "AWS4\(credentials.secretAccessKey)".array) + let kRegion = try HMAC.authenticate(for: Data(region.utf8), using: kDate) + let kService = try HMAC.authenticate(for: Data(name.utf8), using: kRegion) + let kSigning = try HMAC.authenticate(for: Data("aws4_request".utf8), using: kService) + let kSignature = try HMAC.authenticate(for: stringToSign(signingData: signingData), using: kSigning) + + signature = kSignature.toHexString() + } catch { + fatalError("HMAC computation is not supposed to throw") + } + + return signature + + // original code from Adam Fowler, using swift-crypto package: + /* let kDate = HMAC.authenticationCode(for: Data(signingData.date.utf8), using: SymmetricKey(data: Array("AWS4\(credentials.secretAccessKey)".utf8))) let kRegion = HMAC.authenticationCode(for: Data(region.utf8), using: SymmetricKey(data: kDate)) let kService = HMAC.authenticationCode(for: Data(name.utf8), using: SymmetricKey(data: kRegion)) let kSigning = HMAC.authenticationCode(for: Data("aws4_request".utf8), using: SymmetricKey(data: kService)) let kSignature = HMAC.authenticationCode(for: stringToSign(signingData: signingData), using: SymmetricKey(data: kSigning)) return kSignature.hexDigest() + */ } /// Stage 2 Create the string to sign as in https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html diff --git a/Sources/AWSLambdaDeployerHelper/main.swift b/Sources/AWSLambdaPluginHelper/main.swift similarity index 100% rename from Sources/AWSLambdaDeployerHelper/main.swift rename to Sources/AWSLambdaPluginHelper/main.swift diff --git a/Tests/AWSLambdaPluginHelperTests/AWSSignerTests.swift b/Tests/AWSLambdaPluginHelperTests/AWSSignerTests.swift new file mode 100644 index 000000000..4220e2355 --- /dev/null +++ b/Tests/AWSLambdaPluginHelperTests/AWSSignerTests.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 Logging +import Testing + +@testable import AWSLambdaPluginHelper + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@Suite +struct SignerTests { + let credentials : Credential = StaticCredential(accessKeyId: "MYACCESSKEY", secretAccessKey: "MYSECRETACCESSKEY") + + @Test + func testSignGetHeaders() { + let signer = AWSSigner(credentials: credentials, name: "glacier", region:"us-east-1") + let headers = signer.signHeaders(url: URL(string:"https://glacier.us-east-1.amazonaws.com/-/vaults")!, method: .GET, headers: ["x-amz-glacier-version":"2012-06-01"], date: Date(timeIntervalSinceReferenceDate: 2000000)) + #expect(headers["Authorization"].first == "AWS4-HMAC-SHA256 Credential=MYACCESSKEY/20010124/us-east-1/glacier/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-glacier-version, Signature=acfa9b03fca6b098d7b88bfd9bbdb4687f5b34e944a9c6ed9f4814c1b0b06d62") + } + + @Test + func testSignPutHeaders() { + let signer = AWSSigner(credentials: credentials, name: "sns", region:"eu-west-1") + let headers = signer.signHeaders(url: URL(string: "https://sns.eu-west-1.amazonaws.com/")!, method: .POST, headers: ["Content-Type": "application/x-www-form-urlencoded; charset=utf-8"], body: .string("Action=ListTopics&Version=2010-03-31"), date: Date(timeIntervalSinceReferenceDate: 200)) + #expect(headers["Authorization"].first == "AWS4-HMAC-SHA256 Credential=MYACCESSKEY/20010101/eu-west-1/sns/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=1d29943055a8ad094239e8de06082100f2426ebbb2c6a5bbcbb04c63e6a3f274") + } + + @Test + func testSignS3GetURL() { + let signer = AWSSigner(credentials: credentials, name: "s3", region:"us-east-1") + let url = signer.signURL(url: URL(string: "https://s3.us-east-1.amazonaws.com/")!, method: .GET, date:Date(timeIntervalSinceReferenceDate: 100000)) + #expect(url.absoluteString == "https://s3.us-east-1.amazonaws.com/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=MYACCESSKEY%2F20010102%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20010102T034640Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=27957103c8bfdff3560372b1d85976ed29c944f34295eca2d4fdac7fc02c375a") + } + + @Test + func testSignS3PutURL() { + let signer = AWSSigner(credentials: credentials, name: "s3", region:"eu-west-1") + let url = signer.signURL(url: URL(string: "https://test-bucket.s3.amazonaws.com/test-put.txt")!, method: .PUT, body: .string("Testing signed URLs"), date:Date(timeIntervalSinceReferenceDate: 100000)) + #expect(url.absoluteString == "https://test-bucket.s3.amazonaws.com/test-put.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=MYACCESSKEY%2F20010102%2Feu-west-1%2Fs3%2Faws4_request&X-Amz-Date=20010102T034640Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=13d665549a6ea5eb6a1615ede83440eaed3e0ee25c964e62d188c896d916d96f") + } +} \ No newline at end of file diff --git a/Tests/AWSLambdaPluginHelperTests/CryptoTests.swift b/Tests/AWSLambdaPluginHelperTests/CryptoTests.swift new file mode 100644 index 000000000..3e88285a9 --- /dev/null +++ b/Tests/AWSLambdaPluginHelperTests/CryptoTests.swift @@ -0,0 +1,78 @@ +//===----------------------------------------------------------------------===// +// +// 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 Testing + +@testable import AWSLambdaPluginHelper + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@Suite +struct CryptoTests { + + @Test + func testSHA256() { + + // given + let input = "hello world" + let expected = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" + + // when + let result = Digest.sha256(input.array).toHexString() + + // then + #expect(result == expected) + } + + @Test + func testHMAC() throws { + + // given + let input = "hello world" + let secret = "secretkey" + let expected = "ae6cd2605d622316564d1f76bfc0c04f89d9fafb14f45b3e18c2a3e28bdef29d" + + // when + let authenticator = HMAC(key: secret.array, variant: .sha2(.sha256)) + + #expect(throws: Never.self) { + let result = try authenticator.authenticate(input.array).toHexString() + // then + #expect(result == expected) + } + + } + + @Test + func testHMACExtension() throws { + + // given + let input = "hello world" + let secret = "secretkey" + let expected = "ae6cd2605d622316564d1f76bfc0c04f89d9fafb14f45b3e18c2a3e28bdef29d" + + // when + let result = try HMAC.authenticate(for: input.array, using: secret.array).toHexString() + + // then + #expect(result == expected) + + } + +} \ No newline at end of file From 90517adfe0076818c5d59b9b9b5cc64ed7289888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 16 Nov 2024 14:08:04 +0100 Subject: [PATCH 05/27] swift format --- Package.swift | 10 +- Plugins/AWSLambdaDeployer/Plugin.swift | 3 +- .../AWSLambdaPluginHelper/Extensions.swift | 16 +- .../Vendored/crypto/Array+Extensions.swift | 232 +++---- .../Vendored/crypto/Authenticator.swift | 6 +- .../Vendored/crypto/BatchedCollections.swift | 92 +-- .../Vendored/crypto/Bit.swift | 12 +- .../crypto/Collections+Extensions.swift | 70 +- .../Vendored/crypto/Digest.swift | 102 +-- .../Vendored/crypto/DigestType.swift | 4 +- .../Vendored/crypto/Generics.swift | 22 +- .../Vendored/crypto/HMAC.swift | 188 ++--- .../Vendored/crypto/Int+Extension.swift | 14 +- .../Vendored/crypto/NoPadding.swift | 16 +- .../Vendored/crypto/Padding.swift | 82 +-- .../Vendored/crypto/SHA1.swift | 260 +++---- .../Vendored/crypto/SHA2.swift | 654 +++++++++--------- .../Vendored/crypto/SHA3.swift | 489 +++++++------ .../Vendored/crypto/UInt16+Extension.swift | 32 +- .../Vendored/crypto/UInt32+Extension.swift | 38 +- .../Vendored/crypto/UInt64+Extension.swift | 46 +- .../Vendored/crypto/UInt8+Extension.swift | 70 +- .../Vendored/crypto/Updatable.swift | 156 +++-- .../Vendored/crypto/Utils.swift | 132 ++-- .../Vendored/crypto/ZeroPadding.swift | 34 +- .../Vendored/signer/AWSCredentials.swift | 10 +- .../Vendored/signer/AWSSigner.swift | 183 +++-- Sources/AWSLambdaPluginHelper/main.swift | 2 +- .../AWSSignerTests.swift | 74 +- .../CryptoTests.swift | 100 +-- 30 files changed, 1649 insertions(+), 1500 deletions(-) diff --git a/Package.swift b/Package.swift index f34b38eb7..5f6f2d824 100644 --- a/Package.swift +++ b/Package.swift @@ -17,20 +17,20 @@ let package = Package( // 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"]), - + // an executable that implements the business logic for the plugins .executable(name: "AWSLambdaPluginHelper", targets: ["AWSLambdaPluginHelper"]), - + // for testing only .library(name: "AWSLambdaTesting", targets: ["AWSLambdaTesting"]), ], diff --git a/Plugins/AWSLambdaDeployer/Plugin.swift b/Plugins/AWSLambdaDeployer/Plugin.swift index 977b1a2d0..08cdc2696 100644 --- a/Plugins/AWSLambdaDeployer/Plugin.swift +++ b/Plugins/AWSLambdaDeployer/Plugin.swift @@ -19,7 +19,6 @@ import PackagePlugin @available(macOS 15.0, *) struct AWSLambdaDeployer: CommandPlugin { - func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws { let configuration = try Configuration(context: context, arguments: arguments) @@ -27,7 +26,7 @@ struct AWSLambdaDeployer: CommandPlugin { self.displayHelpMessage() return } - + let tool = try context.tool(named: "AWSLambdaDeployerHelper") try Utils.execute(executable: tool.url, arguments: [], logLevel: .debug) } diff --git a/Sources/AWSLambdaPluginHelper/Extensions.swift b/Sources/AWSLambdaPluginHelper/Extensions.swift index 671c4cec6..67a8202b1 100644 --- a/Sources/AWSLambdaPluginHelper/Extensions.swift +++ b/Sources/AWSLambdaPluginHelper/Extensions.swift @@ -26,7 +26,7 @@ import Foundation extension Data { var bytes: [UInt8] { - return [UInt8](self) + [UInt8](self) } } @@ -37,12 +37,20 @@ extension String { } extension HMAC { - public static func authenticate(for data: [UInt8], using key: [UInt8], variant: HMAC.Variant = .sha2(.sha256)) throws -> [UInt8] { + public static func authenticate( + for data: [UInt8], + using key: [UInt8], + variant: HMAC.Variant = .sha2(.sha256) + ) throws -> [UInt8] { let authenticator = HMAC(key: key, variant: variant) return try authenticator.authenticate(data) } - public static func authenticate(for data: Data, using key: [UInt8], variant: HMAC.Variant = .sha2(.sha256)) throws -> [UInt8] { + public static func authenticate( + for data: Data, + using key: [UInt8], + variant: HMAC.Variant = .sha2(.sha256) + ) throws -> [UInt8] { let authenticator = HMAC(key: key, variant: variant) return try authenticator.authenticate(data.bytes) } -} \ No newline at end of file +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Array+Extensions.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Array+Extensions.swift index fd0631673..000100185 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Array+Extensions.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Array+Extensions.swift @@ -14,142 +14,142 @@ // extension Array { - @inlinable - init(reserveCapacity: Int) { - self = Array() - self.reserveCapacity(reserveCapacity) - } - - @inlinable - var slice: ArraySlice { - self[self.startIndex ..< self.endIndex] - } - - @inlinable - subscript (safe index: Index) -> Element? { - return indices.contains(index) ? self[index] : nil - } + @inlinable + init(reserveCapacity: Int) { + self = [Element]() + self.reserveCapacity(reserveCapacity) + } + + @inlinable + var slice: ArraySlice { + self[self.startIndex.. Element? { + indices.contains(index) ? self[index] : nil + } } extension Array where Element == UInt8 { - public init(hex: String) { - self.init(reserveCapacity: hex.unicodeScalars.lazy.underestimatedCount) - var buffer: UInt8? - var skip = hex.hasPrefix("0x") ? 2 : 0 - for char in hex.unicodeScalars.lazy { - guard skip == 0 else { - skip -= 1 - continue - } - guard char.value >= 48 && char.value <= 102 else { - removeAll() - return - } - let v: UInt8 - let c: UInt8 = UInt8(char.value) - switch c { - case let c where c <= 57: - v = c - 48 - case let c where c >= 65 && c <= 70: - v = c - 55 - case let c where c >= 97: - v = c - 87 - default: - removeAll() - return - } - if let b = buffer { - append(b << 4 | v) - buffer = nil - } else { - buffer = v - } - } - if let b = buffer { - append(b) + public init(hex: String) { + self.init(reserveCapacity: hex.unicodeScalars.lazy.underestimatedCount) + var buffer: UInt8? + var skip = hex.hasPrefix("0x") ? 2 : 0 + for char in hex.unicodeScalars.lazy { + guard skip == 0 else { + skip -= 1 + continue + } + guard char.value >= 48 && char.value <= 102 else { + removeAll() + return + } + let v: UInt8 + let c: UInt8 = UInt8(char.value) + switch c { + case let c where c <= 57: + v = c - 48 + case let c where c >= 65 && c <= 70: + v = c - 55 + case let c where c >= 97: + v = c - 87 + default: + removeAll() + return + } + if let b = buffer { + append(b << 4 | v) + buffer = nil + } else { + buffer = v + } + } + if let b = buffer { + append(b) + } } - } - - public func toHexString() -> String { - `lazy`.reduce(into: "") { - var s = String($1, radix: 16) - if s.count == 1 { - s = "0" + s - } - $0 += s + + public func toHexString() -> String { + `lazy`.reduce(into: "") { + var s = String($1, radix: 16) + if s.count == 1 { + s = "0" + s + } + $0 += s + } } - } } extension Array where Element == UInt8 { - /// split in chunks with given chunk size - @available(*, deprecated) - public func chunks(size chunksize: Int) -> Array> { - var words = Array>() - words.reserveCapacity(count / chunksize) - for idx in stride(from: chunksize, through: count, by: chunksize) { - words.append(Array(self[idx - chunksize ..< idx])) // slow for large table - } - let remainder = suffix(count % chunksize) - if !remainder.isEmpty { - words.append(Array(remainder)) + /// split in chunks with given chunk size + @available(*, deprecated) + public func chunks(size chunksize: Int) -> [[Element]] { + var words = [[Element]]() + words.reserveCapacity(count / chunksize) + for idx in stride(from: chunksize, through: count, by: chunksize) { + words.append(Array(self[idx - chunksize.. [Element] { - // Digest.md5(self) - // } + // public func md5() -> [Element] { + // Digest.md5(self) + // } - // public func sha1() -> [Element] { - // Digest.sha1(self) - // } + // public func sha1() -> [Element] { + // Digest.sha1(self) + // } - // public func sha224() -> [Element] { - // Digest.sha224(self) - // } + // public func sha224() -> [Element] { + // Digest.sha224(self) + // } - public func sha256() -> [Element] { - Digest.sha256(self) - } + public func sha256() -> [Element] { + Digest.sha256(self) + } - public func sha384() -> [Element] { - Digest.sha384(self) - } + public func sha384() -> [Element] { + Digest.sha384(self) + } - public func sha512() -> [Element] { - Digest.sha512(self) - } + public func sha512() -> [Element] { + Digest.sha512(self) + } - public func sha2(_ variant: SHA2.Variant) -> [Element] { - Digest.sha2(self, variant: variant) - } + public func sha2(_ variant: SHA2.Variant) -> [Element] { + Digest.sha2(self, variant: variant) + } - public func sha3(_ variant: SHA3.Variant) -> [Element] { - Digest.sha3(self, variant: variant) - } + public func sha3(_ variant: SHA3.Variant) -> [Element] { + Digest.sha3(self, variant: variant) + } - // public func crc32(seed: UInt32? = nil, reflect: Bool = true) -> UInt32 { - // Checksum.crc32(self, seed: seed, reflect: reflect) - // } + // public func crc32(seed: UInt32? = nil, reflect: Bool = true) -> UInt32 { + // Checksum.crc32(self, seed: seed, reflect: reflect) + // } - // public func crc32c(seed: UInt32? = nil, reflect: Bool = true) -> UInt32 { - // Checksum.crc32c(self, seed: seed, reflect: reflect) - // } + // public func crc32c(seed: UInt32? = nil, reflect: Bool = true) -> UInt32 { + // Checksum.crc32c(self, seed: seed, reflect: reflect) + // } - // public func crc16(seed: UInt16? = nil) -> UInt16 { - // Checksum.crc16(self, seed: seed) - // } + // public func crc16(seed: UInt16? = nil) -> UInt16 { + // Checksum.crc16(self, seed: seed) + // } - // public func encrypt(cipher: Cipher) throws -> [Element] { - // try cipher.encrypt(self.slice) - // } + // public func encrypt(cipher: Cipher) throws -> [Element] { + // try cipher.encrypt(self.slice) + // } - // public func decrypt(cipher: Cipher) throws -> [Element] { - // try cipher.decrypt(self.slice) - // } + // public func decrypt(cipher: Cipher) throws -> [Element] { + // try cipher.decrypt(self.slice) + // } - public func authenticate(with authenticator: A) throws -> [Element] { - try authenticator.authenticate(self) - } -} \ No newline at end of file + public func authenticate(with authenticator: A) throws -> [Element] { + try authenticator.authenticate(self) + } +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Authenticator.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Authenticator.swift index afabf51bc..d45363e63 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Authenticator.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Authenticator.swift @@ -15,6 +15,6 @@ /// Message authentication code. public protocol Authenticator { - /// Calculate Message Authentication Code (MAC) for message. - func authenticate(_ bytes: Array) throws -> Array -} \ No newline at end of file + /// Calculate Message Authentication Code (MAC) for message. + func authenticate(_ bytes: [UInt8]) throws -> [UInt8] +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/BatchedCollections.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/BatchedCollections.swift index 43efb56a3..1dd0fd0e4 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/BatchedCollections.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/BatchedCollections.swift @@ -15,67 +15,73 @@ @usableFromInline struct BatchedCollectionIndex { - let range: Range + let range: Range } extension BatchedCollectionIndex: Comparable { - @usableFromInline - static func == (lhs: BatchedCollectionIndex, rhs: BatchedCollectionIndex) -> Bool { - lhs.range.lowerBound == rhs.range.lowerBound - } + @usableFromInline + static func == ( + lhs: BatchedCollectionIndex, + rhs: BatchedCollectionIndex + ) -> Bool { + lhs.range.lowerBound == rhs.range.lowerBound + } - @usableFromInline - static func < (lhs: BatchedCollectionIndex, rhs: BatchedCollectionIndex) -> Bool { - lhs.range.lowerBound < rhs.range.lowerBound - } + @usableFromInline + static func < ( + lhs: BatchedCollectionIndex, + rhs: BatchedCollectionIndex + ) -> Bool { + lhs.range.lowerBound < rhs.range.lowerBound + } } protocol BatchedCollectionType: Collection { - associatedtype Base: Collection + associatedtype Base: Collection } @usableFromInline struct BatchedCollection: Collection { - let base: Base - let size: Int + let base: Base + let size: Int - @usableFromInline - init(base: Base, size: Int) { - self.base = base - self.size = size - } + @usableFromInline + init(base: Base, size: Int) { + self.base = base + self.size = size + } - @usableFromInline - typealias Index = BatchedCollectionIndex + @usableFromInline + typealias Index = BatchedCollectionIndex - private func nextBreak(after idx: Base.Index) -> Base.Index { - self.base.index(idx, offsetBy: self.size, limitedBy: self.base.endIndex) ?? self.base.endIndex - } + private func nextBreak(after idx: Base.Index) -> Base.Index { + self.base.index(idx, offsetBy: self.size, limitedBy: self.base.endIndex) ?? self.base.endIndex + } - @usableFromInline - var startIndex: Index { - Index(range: self.base.startIndex.. Index { - Index(range: idx.range.upperBound.. Index { + Index(range: idx.range.upperBound.. Base.SubSequence { - self.base[idx.range] - } + @usableFromInline + subscript(idx: Index) -> Base.SubSequence { + self.base[idx.range] + } } extension Collection { - @inlinable - func batched(by size: Int) -> BatchedCollection { - BatchedCollection(base: self, size: size) - } -} \ No newline at end of file + @inlinable + func batched(by size: Int) -> BatchedCollection { + BatchedCollection(base: self, size: size) + } +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Bit.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Bit.swift index 3520b0dd4..c2e29de60 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Bit.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Bit.swift @@ -14,13 +14,13 @@ // public enum Bit: Int { - case zero - case one + case zero + case one } extension Bit { - @inlinable - func inverted() -> Bit { - self == .zero ? .one : .zero - } + @inlinable + func inverted() -> Bit { + self == .zero ? .one : .zero + } } diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Collections+Extensions.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Collections+Extensions.swift index 84d68d176..a205b2dfe 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Collections+Extensions.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Collections+Extensions.swift @@ -27,49 +27,49 @@ // - This notice may not be removed or altered from any source or binary distribution. // extension Collection where Self.Element == UInt8, Self.Index == Int { - // Big endian order - @inlinable - func toUInt32Array() -> Array { - guard !isEmpty else { - return [] - } + // Big endian order + @inlinable + func toUInt32Array() -> [UInt32] { + guard !isEmpty else { + return [] + } - let c = strideCount(from: startIndex, to: endIndex, by: 4) - return Array(unsafeUninitializedCapacity: c) { buf, count in - var counter = 0 - for idx in stride(from: startIndex, to: endIndex, by: 4) { - let val = UInt32(bytes: self, fromIndex: idx).bigEndian - buf[counter] = val - counter += 1 - } - count = counter - assert(counter == c) + let c = strideCount(from: startIndex, to: endIndex, by: 4) + return [UInt32](unsafeUninitializedCapacity: c) { buf, count in + var counter = 0 + for idx in stride(from: startIndex, to: endIndex, by: 4) { + let val = UInt32(bytes: self, fromIndex: idx).bigEndian + buf[counter] = val + counter += 1 + } + count = counter + assert(counter == c) + } } - } - // Big endian order - @inlinable - func toUInt64Array() -> Array { - guard !isEmpty else { - return [] - } + // Big endian order + @inlinable + func toUInt64Array() -> [UInt64] { + guard !isEmpty else { + return [] + } - let c = strideCount(from: startIndex, to: endIndex, by: 8) - return Array(unsafeUninitializedCapacity: c) { buf, count in - var counter = 0 - for idx in stride(from: startIndex, to: endIndex, by: 8) { - let val = UInt64(bytes: self, fromIndex: idx).bigEndian - buf[counter] = val - counter += 1 - } - count = counter - assert(counter == c) + let c = strideCount(from: startIndex, to: endIndex, by: 8) + return [UInt64](unsafeUninitializedCapacity: c) { buf, count in + var counter = 0 + for idx in stride(from: startIndex, to: endIndex, by: 8) { + let val = UInt64(bytes: self, fromIndex: idx).bigEndian + buf[counter] = val + counter += 1 + } + count = counter + assert(counter == c) + } } - } } @usableFromInline func strideCount(from: Int, to: Int, by: Int) -> Int { let count = to - from return count / by + (count % by > 0 ? 1 : 0) -} \ No newline at end of file +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Digest.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Digest.swift index 491f6fdbd..020df5919 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Digest.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Digest.swift @@ -32,61 +32,61 @@ public typealias Hash = Digest /// Hash functions to calculate Digest. public struct Digest { - /// Calculate MD5 Digest - /// - parameter bytes: input message - /// - returns: Digest bytes - // public static func md5(_ bytes: Array) -> Array { - // MD5().calculate(for: bytes) - // } + /// Calculate MD5 Digest + /// - parameter bytes: input message + /// - returns: Digest bytes + // public static func md5(_ bytes: Array) -> Array { + // MD5().calculate(for: bytes) + // } - /// Calculate SHA1 Digest - /// - parameter bytes: input message - /// - returns: Digest bytes - public static func sha1(_ bytes: Array) -> Array { - SHA1().calculate(for: bytes) - } + /// Calculate SHA1 Digest + /// - parameter bytes: input message + /// - returns: Digest bytes + public static func sha1(_ bytes: [UInt8]) -> [UInt8] { + SHA1().calculate(for: bytes) + } - /// Calculate SHA2-224 Digest - /// - parameter bytes: input message - /// - returns: Digest bytes - public static func sha224(_ bytes: Array) -> Array { - self.sha2(bytes, variant: .sha224) - } + /// Calculate SHA2-224 Digest + /// - parameter bytes: input message + /// - returns: Digest bytes + public static func sha224(_ bytes: [UInt8]) -> [UInt8] { + self.sha2(bytes, variant: .sha224) + } - /// Calculate SHA2-256 Digest - /// - parameter bytes: input message - /// - returns: Digest bytes - public static func sha256(_ bytes: Array) -> Array { - self.sha2(bytes, variant: .sha256) - } + /// Calculate SHA2-256 Digest + /// - parameter bytes: input message + /// - returns: Digest bytes + public static func sha256(_ bytes: [UInt8]) -> [UInt8] { + self.sha2(bytes, variant: .sha256) + } - /// Calculate SHA2-384 Digest - /// - parameter bytes: input message - /// - returns: Digest bytes - public static func sha384(_ bytes: Array) -> Array { - self.sha2(bytes, variant: .sha384) - } + /// Calculate SHA2-384 Digest + /// - parameter bytes: input message + /// - returns: Digest bytes + public static func sha384(_ bytes: [UInt8]) -> [UInt8] { + self.sha2(bytes, variant: .sha384) + } - /// Calculate SHA2-512 Digest - /// - parameter bytes: input message - /// - returns: Digest bytes - public static func sha512(_ bytes: Array) -> Array { - self.sha2(bytes, variant: .sha512) - } + /// Calculate SHA2-512 Digest + /// - parameter bytes: input message + /// - returns: Digest bytes + public static func sha512(_ bytes: [UInt8]) -> [UInt8] { + self.sha2(bytes, variant: .sha512) + } - /// Calculate SHA2 Digest - /// - parameter bytes: input message - /// - parameter variant: SHA-2 variant - /// - returns: Digest bytes - public static func sha2(_ bytes: Array, variant: SHA2.Variant) -> Array { - SHA2(variant: variant).calculate(for: bytes) - } + /// Calculate SHA2 Digest + /// - parameter bytes: input message + /// - parameter variant: SHA-2 variant + /// - returns: Digest bytes + public static func sha2(_ bytes: [UInt8], variant: SHA2.Variant) -> [UInt8] { + SHA2(variant: variant).calculate(for: bytes) + } - /// Calculate SHA3 Digest - /// - parameter bytes: input message - /// - parameter variant: SHA-3 variant - /// - returns: Digest bytes - public static func sha3(_ bytes: Array, variant: SHA3.Variant) -> Array { - SHA3(variant: variant).calculate(for: bytes) - } -} \ No newline at end of file + /// Calculate SHA3 Digest + /// - parameter bytes: input message + /// - parameter variant: SHA-3 variant + /// - returns: Digest bytes + public static func sha3(_ bytes: [UInt8], variant: SHA3.Variant) -> [UInt8] { + SHA3(variant: variant).calculate(for: bytes) + } +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/DigestType.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/DigestType.swift index 13f27c1f0..351387868 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/DigestType.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/DigestType.swift @@ -28,5 +28,5 @@ // internal protocol DigestType { - func calculate(for bytes: Array) -> Array -} \ No newline at end of file + func calculate(for bytes: [UInt8]) -> [UInt8] +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Generics.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Generics.swift index 2ba535d54..38c6e3b97 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Generics.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Generics.swift @@ -40,18 +40,18 @@ @_specialize(where T == UInt32) @_specialize(where T == UInt64) @inlinable -func arrayOfBytes(value: T, length totalBytes: Int = MemoryLayout.size) -> Array { - let valuePointer = UnsafeMutablePointer.allocate(capacity: 1) - valuePointer.pointee = value +func arrayOfBytes(value: T, length totalBytes: Int = MemoryLayout.size) -> [UInt8] { + let valuePointer = UnsafeMutablePointer.allocate(capacity: 1) + valuePointer.pointee = value - let bytesPointer = UnsafeMutablePointer(OpaquePointer(valuePointer)) - var bytes = Array(repeating: 0, count: totalBytes) - for j in 0...size, totalBytes) { - bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee - } + let bytesPointer = UnsafeMutablePointer(OpaquePointer(valuePointer)) + var bytes = [UInt8](repeating: 0, count: totalBytes) + for j in 0...size, totalBytes) { + bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee + } - valuePointer.deinitialize(count: 1) - valuePointer.deallocate() + valuePointer.deinitialize(count: 1) + valuePointer.deallocate() - return bytes + return bytes } diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift index b257eea45..b6fffc887 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift @@ -28,112 +28,112 @@ // public final class HMAC: Authenticator { - public enum Error: Swift.Error { - case authenticateError - case invalidInput - } + public enum Error: Swift.Error { + case authenticateError + case invalidInput + } - public enum Variant { - // case md5 - case sha1 - case sha2(SHA2.Variant) - case sha3(SHA3.Variant) + public enum Variant { + // case md5 + case sha1 + case sha2(SHA2.Variant) + case sha3(SHA3.Variant) - @available(*, deprecated, message: "Use sha2(variant) instead.") - case sha256, sha384, sha512 + @available(*, deprecated, message: "Use sha2(variant) instead.") + case sha256, sha384, sha512 - var digestLength: Int { - switch self { - case .sha1: - return SHA1.digestLength - case .sha256: - return SHA2.Variant.sha256.digestLength - case .sha384: - return SHA2.Variant.sha384.digestLength - case .sha512: - return SHA2.Variant.sha512.digestLength - case .sha2(let variant): - return variant.digestLength - case .sha3(let variant): - return variant.digestLength - // case .md5: - // return MD5.digestLength - } - } + var digestLength: Int { + switch self { + case .sha1: + return SHA1.digestLength + case .sha256: + return SHA2.Variant.sha256.digestLength + case .sha384: + return SHA2.Variant.sha384.digestLength + case .sha512: + return SHA2.Variant.sha512.digestLength + case .sha2(let variant): + return variant.digestLength + case .sha3(let variant): + return variant.digestLength + // case .md5: + // return MD5.digestLength + } + } - func calculateHash(_ bytes: Array) -> Array { - switch self { - case .sha1: - return Digest.sha1(bytes) - case .sha256: - return Digest.sha256(bytes) - case .sha384: - return Digest.sha384(bytes) - case .sha512: - return Digest.sha512(bytes) - case .sha2(let variant): - return Digest.sha2(bytes, variant: variant) - case .sha3(let variant): - return Digest.sha3(bytes, variant: variant) - // case .md5: - // return Digest.md5(bytes) - } - } + func calculateHash(_ bytes: [UInt8]) -> [UInt8] { + switch self { + case .sha1: + return Digest.sha1(bytes) + case .sha256: + return Digest.sha256(bytes) + case .sha384: + return Digest.sha384(bytes) + case .sha512: + return Digest.sha512(bytes) + case .sha2(let variant): + return Digest.sha2(bytes, variant: variant) + case .sha3(let variant): + return Digest.sha3(bytes, variant: variant) + // case .md5: + // return Digest.md5(bytes) + } + } - func blockSize() -> Int { - switch self { - // case .md5: - // return MD5.blockSize - case .sha1: - return SHA1.blockSize - case .sha256: - return SHA2.Variant.sha256.blockSize - case .sha384: - return SHA2.Variant.sha384.blockSize - case .sha512: - return SHA2.Variant.sha512.blockSize - case .sha2(let variant): - return variant.blockSize - case .sha3(let variant): - return variant.blockSize - } + func blockSize() -> Int { + switch self { + // case .md5: + // return MD5.blockSize + case .sha1: + return SHA1.blockSize + case .sha256: + return SHA2.Variant.sha256.blockSize + case .sha384: + return SHA2.Variant.sha384.blockSize + case .sha512: + return SHA2.Variant.sha512.blockSize + case .sha2(let variant): + return variant.blockSize + case .sha3(let variant): + return variant.blockSize + } + } } - } - var key: Array - let variant: Variant + var key: [UInt8] + let variant: Variant - // public init(key: Array, variant: HMAC.Variant = .md5) { - public init(key: Array, variant: HMAC.Variant = .sha2(.sha256)) { - self.variant = variant - self.key = key + // public init(key: Array, variant: HMAC.Variant = .md5) { + public init(key: [UInt8], variant: HMAC.Variant = .sha2(.sha256)) { + self.variant = variant + self.key = key - if key.count > variant.blockSize() { - let hash = variant.calculateHash(key) - self.key = hash - } + if key.count > variant.blockSize() { + let hash = variant.calculateHash(key) + self.key = hash + } - if key.count < variant.blockSize() { - self.key = ZeroPadding().add(to: key, blockSize: variant.blockSize()) + if key.count < variant.blockSize() { + self.key = ZeroPadding().add(to: key, blockSize: variant.blockSize()) + } } - } - // MARK: Authenticator + // MARK: Authenticator - public func authenticate(_ bytes: Array) throws -> Array { - var opad = Array(repeating: 0x5c, count: variant.blockSize()) - for idx in self.key.indices { - opad[idx] = self.key[idx] ^ opad[idx] - } - var ipad = Array(repeating: 0x36, count: variant.blockSize()) - for idx in self.key.indices { - ipad[idx] = self.key[idx] ^ ipad[idx] - } + public func authenticate(_ bytes: [UInt8]) throws -> [UInt8] { + var opad = [UInt8](repeating: 0x5c, count: variant.blockSize()) + for idx in self.key.indices { + opad[idx] = self.key[idx] ^ opad[idx] + } + var ipad = [UInt8](repeating: 0x36, count: variant.blockSize()) + for idx in self.key.indices { + ipad[idx] = self.key[idx] ^ ipad[idx] + } - let ipadAndMessageHash = self.variant.calculateHash(ipad + bytes) - let result = self.variant.calculateHash(opad + ipadAndMessageHash) + let ipadAndMessageHash = self.variant.calculateHash(ipad + bytes) + let result = self.variant.calculateHash(opad + ipadAndMessageHash) - // return Array(result[0..<10]) // 80 bits - return result - } -} \ No newline at end of file + // return Array(result[0..<10]) // 80 bits + return result + } +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Int+Extension.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Int+Extension.swift index b73a19cfa..33dddb516 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Int+Extension.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Int+Extension.swift @@ -37,11 +37,11 @@ import ucrt #endif extension FixedWidthInteger { - @inlinable - func bytes(totalBytes: Int = MemoryLayout.size) -> Array { - arrayOfBytes(value: self.littleEndian, length: totalBytes) - // TODO: adjust bytes order - // var value = self.littleEndian - // return withUnsafeBytes(of: &value, Array.init).reversed() - } + @inlinable + func bytes(totalBytes: Int = MemoryLayout.size) -> [UInt8] { + arrayOfBytes(value: self.littleEndian, length: totalBytes) + // TODO: adjust bytes order + // var value = self.littleEndian + // return withUnsafeBytes(of: &value, Array.init).reversed() + } } diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/NoPadding.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/NoPadding.swift index fa7f007ab..a6c519130 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/NoPadding.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/NoPadding.swift @@ -28,14 +28,14 @@ // struct NoPadding: PaddingProtocol { - init() { - } + init() { + } - func add(to data: Array, blockSize _: Int) -> Array { - data - } + func add(to data: [UInt8], blockSize _: Int) -> [UInt8] { + data + } - func remove(from data: Array, blockSize _: Int?) -> Array { - data - } + func remove(from data: [UInt8], blockSize _: Int?) -> [UInt8] { + data + } } diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift index 74e70e8d7..be90ba373 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift @@ -28,52 +28,52 @@ // public protocol PaddingProtocol { - func add(to: Array, blockSize: Int) -> Array - func remove(from: Array, blockSize: Int?) -> Array + func add(to: [UInt8], blockSize: Int) -> [UInt8] + func remove(from: [UInt8], blockSize: Int?) -> [UInt8] } public enum Padding: PaddingProtocol { - case noPadding, zeroPadding //, pkcs7, pkcs5, eme_pkcs1v15, emsa_pkcs1v15, iso78164, iso10126 + case noPadding, zeroPadding //, pkcs7, pkcs5, eme_pkcs1v15, emsa_pkcs1v15, iso78164, iso10126 - public func add(to: Array, blockSize: Int) -> Array { - switch self { - case .noPadding: - return to // NoPadding().add(to: to, blockSize: blockSize) - case .zeroPadding: - return ZeroPadding().add(to: to, blockSize: blockSize) - // case .pkcs7: - // return PKCS7.Padding().add(to: to, blockSize: blockSize) - // case .pkcs5: - // return PKCS5.Padding().add(to: to, blockSize: blockSize) - // case .eme_pkcs1v15: - // return EMEPKCS1v15Padding().add(to: to, blockSize: blockSize) - // case .emsa_pkcs1v15: - // return EMSAPKCS1v15Padding().add(to: to, blockSize: blockSize) - // case .iso78164: - // return ISO78164Padding().add(to: to, blockSize: blockSize) - // case .iso10126: - // return ISO10126Padding().add(to: to, blockSize: blockSize) + public func add(to: [UInt8], blockSize: Int) -> [UInt8] { + switch self { + case .noPadding: + return to // NoPadding().add(to: to, blockSize: blockSize) + case .zeroPadding: + return ZeroPadding().add(to: to, blockSize: blockSize) + // case .pkcs7: + // return PKCS7.Padding().add(to: to, blockSize: blockSize) + // case .pkcs5: + // return PKCS5.Padding().add(to: to, blockSize: blockSize) + // case .eme_pkcs1v15: + // return EMEPKCS1v15Padding().add(to: to, blockSize: blockSize) + // case .emsa_pkcs1v15: + // return EMSAPKCS1v15Padding().add(to: to, blockSize: blockSize) + // case .iso78164: + // return ISO78164Padding().add(to: to, blockSize: blockSize) + // case .iso10126: + // return ISO10126Padding().add(to: to, blockSize: blockSize) + } } - } - public func remove(from: Array, blockSize: Int?) -> Array { - switch self { - case .noPadding: - return from //NoPadding().remove(from: from, blockSize: blockSize) - case .zeroPadding: - return ZeroPadding().remove(from: from, blockSize: blockSize) - // case .pkcs7: - // return PKCS7.Padding().remove(from: from, blockSize: blockSize) - // case .pkcs5: - // return PKCS5.Padding().remove(from: from, blockSize: blockSize) - // case .eme_pkcs1v15: - // return EMEPKCS1v15Padding().remove(from: from, blockSize: blockSize) - // case .emsa_pkcs1v15: - // return EMSAPKCS1v15Padding().remove(from: from, blockSize: blockSize) - // case .iso78164: - // return ISO78164Padding().remove(from: from, blockSize: blockSize) - // case .iso10126: - // return ISO10126Padding().remove(from: from, blockSize: blockSize) + public func remove(from: [UInt8], blockSize: Int?) -> [UInt8] { + switch self { + case .noPadding: + return from //NoPadding().remove(from: from, blockSize: blockSize) + case .zeroPadding: + return ZeroPadding().remove(from: from, blockSize: blockSize) + // case .pkcs7: + // return PKCS7.Padding().remove(from: from, blockSize: blockSize) + // case .pkcs5: + // return PKCS5.Padding().remove(from: from, blockSize: blockSize) + // case .eme_pkcs1v15: + // return EMEPKCS1v15Padding().remove(from: from, blockSize: blockSize) + // case .emsa_pkcs1v15: + // return EMSAPKCS1v15Padding().remove(from: from, blockSize: blockSize) + // case .iso78164: + // return ISO78164Padding().remove(from: from, blockSize: blockSize) + // case .iso10126: + // return ISO10126Padding().remove(from: from, blockSize: blockSize) + } } - } } diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA1.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA1.swift index 46f57746e..b5fcd3fba 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA1.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA1.swift @@ -29,148 +29,150 @@ public final class SHA1: DigestType { - @usableFromInline - static let digestLength: Int = 20 // 160 / 8 + @usableFromInline + static let digestLength: Int = 20 // 160 / 8 - @usableFromInline - static let blockSize: Int = 64 + @usableFromInline + static let blockSize: Int = 64 - @usableFromInline - static let hashInitialValue: ContiguousArray = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0] + @usableFromInline + static let hashInitialValue: ContiguousArray = [ + 0x6745_2301, 0xefcd_ab89, 0x98ba_dcfe, 0x1032_5476, 0xc3d2_e1f0, + ] - @usableFromInline - var accumulated = Array() + @usableFromInline + var accumulated = [UInt8]() - @usableFromInline - var processedBytesTotalCount: Int = 0 + @usableFromInline + var processedBytesTotalCount: Int = 0 - @usableFromInline - var accumulatedHash: ContiguousArray = SHA1.hashInitialValue + @usableFromInline + var accumulatedHash: ContiguousArray = SHA1.hashInitialValue - public init() { - } - - @inlinable - public func calculate(for bytes: Array) -> Array { - do { - return try update(withBytes: bytes.slice, isLast: true) - } catch { - return [] - } - } - - public func callAsFunction(_ bytes: Array) -> Array { - calculate(for: bytes) - } - - @usableFromInline - func process(block chunk: ArraySlice, currentHash hh: inout ContiguousArray) { - // break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15, big-endian - // Extend the sixteen 32-bit words into eighty 32-bit words: - let M = UnsafeMutablePointer.allocate(capacity: 80) - M.initialize(repeating: 0, count: 80) - defer { - M.deinitialize(count: 80) - M.deallocate() + public init() { } - for x in 0..<80 { - switch x { - case 0...15: - let start = chunk.startIndex.advanced(by: x * 4) // * MemoryLayout.size - M[x] = UInt32(bytes: chunk, fromIndex: start) - default: - M[x] = rotateLeft(M[x - 3] ^ M[x - 8] ^ M[x - 14] ^ M[x - 16], by: 1) - } + @inlinable + public func calculate(for bytes: [UInt8]) -> [UInt8] { + do { + return try update(withBytes: bytes.slice, isLast: true) + } catch { + return [] + } } - var A = hh[0] - var B = hh[1] - var C = hh[2] - var D = hh[3] - var E = hh[4] - - // Main loop - for j in 0...79 { - var f: UInt32 = 0 - var k: UInt32 = 0 - - switch j { - case 0...19: - f = (B & C) | ((~B) & D) - k = 0x5a827999 - case 20...39: - f = B ^ C ^ D - k = 0x6ed9eba1 - case 40...59: - f = (B & C) | (B & D) | (C & D) - k = 0x8f1bbcdc - case 60...79: - f = B ^ C ^ D - k = 0xca62c1d6 - default: - break - } - - let temp = rotateLeft(A, by: 5) &+ f &+ E &+ M[j] &+ k - E = D - D = C - C = rotateLeft(B, by: 30) - B = A - A = temp + public func callAsFunction(_ bytes: [UInt8]) -> [UInt8] { + calculate(for: bytes) } - hh[0] = hh[0] &+ A - hh[1] = hh[1] &+ B - hh[2] = hh[2] &+ C - hh[3] = hh[3] &+ D - hh[4] = hh[4] &+ E - } + @usableFromInline + func process(block chunk: ArraySlice, currentHash hh: inout ContiguousArray) { + // break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15, big-endian + // Extend the sixteen 32-bit words into eighty 32-bit words: + let M = UnsafeMutablePointer.allocate(capacity: 80) + M.initialize(repeating: 0, count: 80) + defer { + M.deinitialize(count: 80) + M.deallocate() + } + + for x in 0..<80 { + switch x { + case 0...15: + let start = chunk.startIndex.advanced(by: x * 4) // * MemoryLayout.size + M[x] = UInt32(bytes: chunk, fromIndex: start) + default: + M[x] = rotateLeft(M[x - 3] ^ M[x - 8] ^ M[x - 14] ^ M[x - 16], by: 1) + } + } + + var A = hh[0] + var B = hh[1] + var C = hh[2] + var D = hh[3] + var E = hh[4] + + // Main loop + for j in 0...79 { + var f: UInt32 = 0 + var k: UInt32 = 0 + + switch j { + case 0...19: + f = (B & C) | ((~B) & D) + k = 0x5a82_7999 + case 20...39: + f = B ^ C ^ D + k = 0x6ed9_eba1 + case 40...59: + f = (B & C) | (B & D) | (C & D) + k = 0x8f1b_bcdc + case 60...79: + f = B ^ C ^ D + k = 0xca62_c1d6 + default: + break + } + + let temp = rotateLeft(A, by: 5) &+ f &+ E &+ M[j] &+ k + E = D + D = C + C = rotateLeft(B, by: 30) + B = A + A = temp + } + + hh[0] = hh[0] &+ A + hh[1] = hh[1] &+ B + hh[2] = hh[2] &+ C + hh[3] = hh[3] &+ D + hh[4] = hh[4] &+ E + } } extension SHA1: Updatable { - @discardableResult @inlinable - public func update(withBytes bytes: ArraySlice, isLast: Bool = false) throws -> Array { - self.accumulated += bytes - - if isLast { - let lengthInBits = (processedBytesTotalCount + self.accumulated.count) * 8 - let lengthBytes = lengthInBits.bytes(totalBytes: 64 / 8) // A 64-bit representation of b - - // Step 1. Append padding - bitPadding(to: &self.accumulated, blockSize: SHA1.blockSize, allowance: 64 / 8) - - // Step 2. Append Length a 64-bit representation of lengthInBits - self.accumulated += lengthBytes + @discardableResult @inlinable + public func update(withBytes bytes: ArraySlice, isLast: Bool = false) throws -> [UInt8] { + self.accumulated += bytes + + if isLast { + let lengthInBits = (processedBytesTotalCount + self.accumulated.count) * 8 + let lengthBytes = lengthInBits.bytes(totalBytes: 64 / 8) // A 64-bit representation of b + + // Step 1. Append padding + bitPadding(to: &self.accumulated, blockSize: SHA1.blockSize, allowance: 64 / 8) + + // Step 2. Append Length a 64-bit representation of lengthInBits + self.accumulated += lengthBytes + } + + var processedBytes = 0 + for chunk in self.accumulated.batched(by: SHA1.blockSize) { + if isLast || (self.accumulated.count - processedBytes) >= SHA1.blockSize { + self.process(block: chunk, currentHash: &self.accumulatedHash) + processedBytes += chunk.count + } + } + self.accumulated.removeFirst(processedBytes) + self.processedBytesTotalCount += processedBytes + + // output current hash + var result = [UInt8](repeating: 0, count: SHA1.digestLength) + var pos = 0 + for idx in 0..> 24) & 0xff) + result[pos + 1] = UInt8((h >> 16) & 0xff) + result[pos + 2] = UInt8((h >> 8) & 0xff) + result[pos + 3] = UInt8(h & 0xff) + pos += 4 + } + + // reset hash value for instance + if isLast { + self.accumulatedHash = SHA1.hashInitialValue + } + + return result } - - var processedBytes = 0 - for chunk in self.accumulated.batched(by: SHA1.blockSize) { - if isLast || (self.accumulated.count - processedBytes) >= SHA1.blockSize { - self.process(block: chunk, currentHash: &self.accumulatedHash) - processedBytes += chunk.count - } - } - self.accumulated.removeFirst(processedBytes) - self.processedBytesTotalCount += processedBytes - - // output current hash - var result = Array(repeating: 0, count: SHA1.digestLength) - var pos = 0 - for idx in 0..> 24) & 0xff) - result[pos + 1] = UInt8((h >> 16) & 0xff) - result[pos + 2] = UInt8((h >> 8) & 0xff) - result[pos + 3] = UInt8(h & 0xff) - pos += 4 - } - - // reset hash value for instance - if isLast { - self.accumulatedHash = SHA1.hashInitialValue - } - - return result - } -} \ No newline at end of file +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift index 9d4e4aca9..4f54e39e7 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift @@ -31,356 +31,386 @@ // public final class SHA2: DigestType { - @usableFromInline - let variant: Variant + @usableFromInline + let variant: Variant + + @usableFromInline + let size: Int + + @usableFromInline + let blockSize: Int + + @usableFromInline + let digestLength: Int - @usableFromInline - let size: Int + private let k: [UInt64] - @usableFromInline - let blockSize: Int + @usableFromInline + var accumulated = [UInt8]() - @usableFromInline - let digestLength: Int + @usableFromInline + var processedBytesTotalCount: Int = 0 - private let k: Array + @usableFromInline + var accumulatedHash32 = [UInt32]() - @usableFromInline - var accumulated = Array() + @usableFromInline + var accumulatedHash64 = [UInt64]() - @usableFromInline - var processedBytesTotalCount: Int = 0 + @frozen + public enum Variant: RawRepresentable { + case sha224, sha256, sha384, sha512 - @usableFromInline - var accumulatedHash32 = Array() + public var digestLength: Int { + self.rawValue / 8 + } - @usableFromInline - var accumulatedHash64 = Array() + public var blockSize: Int { + switch self { + case .sha224, .sha256: + return 64 + case .sha384, .sha512: + return 128 + } + } - @frozen - public enum Variant: RawRepresentable { - case sha224, sha256, sha384, sha512 + public typealias RawValue = Int + public var rawValue: RawValue { + switch self { + case .sha224: + return 224 + case .sha256: + return 256 + case .sha384: + return 384 + case .sha512: + return 512 + } + } - public var digestLength: Int { - self.rawValue / 8 + public init?(rawValue: RawValue) { + switch rawValue { + case 224: + self = .sha224 + case 256: + self = .sha256 + case 384: + self = .sha384 + case 512: + self = .sha512 + default: + return nil + } + } + + @usableFromInline + var h: [UInt64] { + switch self { + case .sha224: + return [ + 0xc105_9ed8, 0x367c_d507, 0x3070_dd17, 0xf70e_5939, 0xffc0_0b31, 0x6858_1511, 0x64f9_8fa7, + 0xbefa_4fa4, + ] + case .sha256: + return [ + 0x6a09_e667, 0xbb67_ae85, 0x3c6e_f372, 0xa54f_f53a, 0x510e_527f, 0x9b05_688c, 0x1f83_d9ab, + 0x5be0_cd19, + ] + case .sha384: + return [ + 0xcbbb_9d5d_c105_9ed8, 0x629a_292a_367c_d507, 0x9159_015a_3070_dd17, 0x152f_ecd8_f70e_5939, + 0x6733_2667_ffc0_0b31, 0x8eb4_4a87_6858_1511, 0xdb0c_2e0d_64f9_8fa7, 0x47b5_481d_befa_4fa4, + ] + case .sha512: + return [ + 0x6a09_e667_f3bc_c908, 0xbb67_ae85_84ca_a73b, 0x3c6e_f372_fe94_f82b, 0xa54f_f53a_5f1d_36f1, + 0x510e_527f_ade6_82d1, 0x9b05_688c_2b3e_6c1f, 0x1f83_d9ab_fb41_bd6b, 0x5be0_cd19_137e_2179, + ] + } + } + + @usableFromInline + var finalLength: Int { + switch self { + case .sha224: + return 7 + case .sha384: + return 6 + default: + return Int.max + } + } } - public var blockSize: Int { - switch self { + public init(variant: SHA2.Variant) { + self.variant = variant + switch self.variant { case .sha224, .sha256: - return 64 + self.accumulatedHash32 = variant.h.map { UInt32($0) } // FIXME: UInt64 for process64 + self.blockSize = variant.blockSize + self.size = variant.rawValue + self.digestLength = variant.digestLength + self.k = [ + 0x428a_2f98, 0x7137_4491, 0xb5c0_fbcf, 0xe9b5_dba5, 0x3956_c25b, 0x59f1_11f1, 0x923f_82a4, 0xab1c_5ed5, + 0xd807_aa98, 0x1283_5b01, 0x2431_85be, 0x550c_7dc3, 0x72be_5d74, 0x80de_b1fe, 0x9bdc_06a7, 0xc19b_f174, + 0xe49b_69c1, 0xefbe_4786, 0x0fc1_9dc6, 0x240c_a1cc, 0x2de9_2c6f, 0x4a74_84aa, 0x5cb0_a9dc, 0x76f9_88da, + 0x983e_5152, 0xa831_c66d, 0xb003_27c8, 0xbf59_7fc7, 0xc6e0_0bf3, 0xd5a7_9147, 0x06ca_6351, 0x1429_2967, + 0x27b7_0a85, 0x2e1b_2138, 0x4d2c_6dfc, 0x5338_0d13, 0x650a_7354, 0x766a_0abb, 0x81c2_c92e, 0x9272_2c85, + 0xa2bf_e8a1, 0xa81a_664b, 0xc24b_8b70, 0xc76c_51a3, 0xd192_e819, 0xd699_0624, 0xf40e_3585, 0x106a_a070, + 0x19a4_c116, 0x1e37_6c08, 0x2748_774c, 0x34b0_bcb5, 0x391c_0cb3, 0x4ed8_aa4a, 0x5b9c_ca4f, 0x682e_6ff3, + 0x748f_82ee, 0x78a5_636f, 0x84c8_7814, 0x8cc7_0208, 0x90be_fffa, 0xa450_6ceb, 0xbef9_a3f7, 0xc671_78f2, + ] case .sha384, .sha512: - return 128 - } + self.accumulatedHash64 = variant.h + self.blockSize = variant.blockSize + self.size = variant.rawValue + self.digestLength = variant.digestLength + self.k = [ + 0x428a_2f98_d728_ae22, 0x7137_4491_23ef_65cd, 0xb5c0_fbcf_ec4d_3b2f, 0xe9b5_dba5_8189_dbbc, + 0x3956_c25b_f348_b538, + 0x59f1_11f1_b605_d019, 0x923f_82a4_af19_4f9b, 0xab1c_5ed5_da6d_8118, 0xd807_aa98_a303_0242, + 0x1283_5b01_4570_6fbe, + 0x2431_85be_4ee4_b28c, 0x550c_7dc3_d5ff_b4e2, 0x72be_5d74_f27b_896f, 0x80de_b1fe_3b16_96b1, + 0x9bdc_06a7_25c7_1235, + 0xc19b_f174_cf69_2694, 0xe49b_69c1_9ef1_4ad2, 0xefbe_4786_384f_25e3, 0x0fc1_9dc6_8b8c_d5b5, + 0x240c_a1cc_77ac_9c65, + 0x2de9_2c6f_592b_0275, 0x4a74_84aa_6ea6_e483, 0x5cb0_a9dc_bd41_fbd4, 0x76f9_88da_8311_53b5, + 0x983e_5152_ee66_dfab, + 0xa831_c66d_2db4_3210, 0xb003_27c8_98fb_213f, 0xbf59_7fc7_beef_0ee4, 0xc6e0_0bf3_3da8_8fc2, + 0xd5a7_9147_930a_a725, + 0x06ca_6351_e003_826f, 0x1429_2967_0a0e_6e70, 0x27b7_0a85_46d2_2ffc, 0x2e1b_2138_5c26_c926, + 0x4d2c_6dfc_5ac4_2aed, + 0x5338_0d13_9d95_b3df, 0x650a_7354_8baf_63de, 0x766a_0abb_3c77_b2a8, 0x81c2_c92e_47ed_aee6, + 0x9272_2c85_1482_353b, + 0xa2bf_e8a1_4cf1_0364, 0xa81a_664b_bc42_3001, 0xc24b_8b70_d0f8_9791, 0xc76c_51a3_0654_be30, + 0xd192_e819_d6ef_5218, + 0xd699_0624_5565_a910, 0xf40e_3585_5771_202a, 0x106a_a070_32bb_d1b8, 0x19a4_c116_b8d2_d0c8, + 0x1e37_6c08_5141_ab53, + 0x2748_774c_df8e_eb99, 0x34b0_bcb5_e19b_48a8, 0x391c_0cb3_c5c9_5a63, 0x4ed8_aa4a_e341_8acb, + 0x5b9c_ca4f_7763_e373, + 0x682e_6ff3_d6b2_b8a3, 0x748f_82ee_5def_b2fc, 0x78a5_636f_4317_2f60, 0x84c8_7814_a1f0_ab72, + 0x8cc7_0208_1a64_39ec, + 0x90be_fffa_2363_1e28, 0xa450_6ceb_de82_bde9, 0xbef9_a3f7_b2c6_7915, 0xc671_78f2_e372_532b, + 0xca27_3ece_ea26_619c, + 0xd186_b8c7_21c0_c207, 0xeada_7dd6_cde0_eb1e, 0xf57d_4f7f_ee6e_d178, 0x06f0_67aa_7217_6fba, + 0x0a63_7dc5_a2c8_98a6, + 0x113f_9804_bef9_0dae, 0x1b71_0b35_131c_471b, 0x28db_77f5_2304_7d84, 0x32ca_ab7b_40c7_2493, + 0x3c9e_be0a_15c9_bebc, + 0x431d_67c4_9c10_0d4c, 0x4cc5_d4be_cb3e_42b6, 0x597f_299c_fc65_7e2a, 0x5fcb_6fab_3ad6_faec, + 0x6c44_198c_4a47_5817, + ] + } } - public typealias RawValue = Int - public var rawValue: RawValue { - switch self { - case .sha224: - return 224 - case .sha256: - return 256 - case .sha384: - return 384 - case .sha512: - return 512 - } + @inlinable + public func calculate(for bytes: [UInt8]) -> [UInt8] { + do { + return try update(withBytes: bytes.slice, isLast: true) + } catch { + return [] + } } - public init?(rawValue: RawValue) { - switch rawValue { - case 224: - self = .sha224 - case 256: - self = .sha256 - case 384: - self = .sha384 - case 512: - self = .sha512 - default: - return nil - } + public func callAsFunction(_ bytes: [UInt8]) -> [UInt8] { + calculate(for: bytes) } @usableFromInline - var h: Array { - switch self { - case .sha224: - return [0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939, 0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4] - case .sha256: - return [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19] - case .sha384: - return [0xcbbb9d5dc1059ed8, 0x629a292a367cd507, 0x9159015a3070dd17, 0x152fecd8f70e5939, 0x67332667ffc00b31, 0x8eb44a8768581511, 0xdb0c2e0d64f98fa7, 0x47b5481dbefa4fa4] - case .sha512: - return [0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, 0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179] - } - } + func process64(block chunk: ArraySlice, currentHash hh: inout [UInt64]) { + // break chunk into sixteen 64-bit words M[j], 0 ≤ j ≤ 15, big-endian + // Extend the sixteen 64-bit words into eighty 64-bit words: + let M = UnsafeMutablePointer.allocate(capacity: self.k.count) + M.initialize(repeating: 0, count: self.k.count) + defer { + M.deinitialize(count: self.k.count) + M.deallocate() + } + for x in 0...size + M[x] = UInt64(bytes: chunk, fromIndex: start) + default: + let s0 = rotateRight(M[x - 15], by: 1) ^ rotateRight(M[x - 15], by: 8) ^ (M[x - 15] >> 7) + let s1 = rotateRight(M[x - 2], by: 19) ^ rotateRight(M[x - 2], by: 61) ^ (M[x - 2] >> 6) + M[x] = M[x - 16] &+ s0 &+ M[x - 7] &+ s1 + } + } - @usableFromInline - var finalLength: Int { - switch self { - case .sha224: - return 7 - case .sha384: - return 6 - default: - return Int.max - } - } - } - - public init(variant: SHA2.Variant) { - self.variant = variant - switch self.variant { - case .sha224, .sha256: - self.accumulatedHash32 = variant.h.map { UInt32($0) } // FIXME: UInt64 for process64 - self.blockSize = variant.blockSize - self.size = variant.rawValue - self.digestLength = variant.digestLength - self.k = [ - 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, - 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, - 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, - 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, - 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, - 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, - 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, - 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 - ] - case .sha384, .sha512: - self.accumulatedHash64 = variant.h - self.blockSize = variant.blockSize - self.size = variant.rawValue - self.digestLength = variant.digestLength - self.k = [ - 0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc, 0x3956c25bf348b538, - 0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118, 0xd807aa98a3030242, 0x12835b0145706fbe, - 0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2, 0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235, - 0xc19bf174cf692694, 0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65, - 0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5, 0x983e5152ee66dfab, - 0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4, 0xc6e00bf33da88fc2, 0xd5a79147930aa725, - 0x06ca6351e003826f, 0x142929670a0e6e70, 0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed, - 0x53380d139d95b3df, 0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b, - 0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30, 0xd192e819d6ef5218, - 0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8, 0x19a4c116b8d2d0c8, 0x1e376c085141ab53, - 0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8, 0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373, - 0x682e6ff3d6b2b8a3, 0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec, - 0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b, 0xca273eceea26619c, - 0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178, 0x06f067aa72176fba, 0x0a637dc5a2c898a6, - 0x113f9804bef90dae, 0x1b710b35131c471b, 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc, - 0x431d67c49c100d4c, 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817 - ] - } - } - - @inlinable - public func calculate(for bytes: Array) -> Array { - do { - return try update(withBytes: bytes.slice, isLast: true) - } catch { - return [] - } - } - - public func callAsFunction(_ bytes: Array) -> Array { - calculate(for: bytes) - } - - @usableFromInline - func process64(block chunk: ArraySlice, currentHash hh: inout Array) { - // break chunk into sixteen 64-bit words M[j], 0 ≤ j ≤ 15, big-endian - // Extend the sixteen 64-bit words into eighty 64-bit words: - let M = UnsafeMutablePointer.allocate(capacity: self.k.count) - M.initialize(repeating: 0, count: self.k.count) - defer { - M.deinitialize(count: self.k.count) - M.deallocate() - } - for x in 0...size - M[x] = UInt64(bytes: chunk, fromIndex: start) - default: - let s0 = rotateRight(M[x - 15], by: 1) ^ rotateRight(M[x - 15], by: 8) ^ (M[x - 15] >> 7) - let s1 = rotateRight(M[x - 2], by: 19) ^ rotateRight(M[x - 2], by: 61) ^ (M[x - 2] >> 6) - M[x] = M[x - 16] &+ s0 &+ M[x - 7] &+ s1 - } - } + var A = hh[0] + var B = hh[1] + var C = hh[2] + var D = hh[3] + var E = hh[4] + var F = hh[5] + var G = hh[6] + var H = hh[7] + + // Main loop + for j in 0.., currentHash hh: inout Array) { - // break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15, big-endian - // Extend the sixteen 32-bit words into sixty-four 32-bit words: - let M = UnsafeMutablePointer.allocate(capacity: self.k.count) - M.initialize(repeating: 0, count: self.k.count) - defer { - M.deinitialize(count: self.k.count) - M.deallocate() - } + // mutating currentHash in place is way faster than returning new result + @usableFromInline + func process32(block chunk: ArraySlice, currentHash hh: inout [UInt32]) { + // break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15, big-endian + // Extend the sixteen 32-bit words into sixty-four 32-bit words: + let M = UnsafeMutablePointer.allocate(capacity: self.k.count) + M.initialize(repeating: 0, count: self.k.count) + defer { + M.deinitialize(count: self.k.count) + M.deallocate() + } - for x in 0...size - M[x] = UInt32(bytes: chunk, fromIndex: start) - default: - let s0 = rotateRight(M[x - 15], by: 7) ^ rotateRight(M[x - 15], by: 18) ^ (M[x - 15] >> 3) - let s1 = rotateRight(M[x - 2], by: 17) ^ rotateRight(M[x - 2], by: 19) ^ (M[x - 2] >> 10) - M[x] = M[x - 16] &+ s0 &+ M[x - 7] &+ s1 - } - } + for x in 0...size + M[x] = UInt32(bytes: chunk, fromIndex: start) + default: + let s0 = rotateRight(M[x - 15], by: 7) ^ rotateRight(M[x - 15], by: 18) ^ (M[x - 15] >> 3) + let s1 = rotateRight(M[x - 2], by: 17) ^ rotateRight(M[x - 2], by: 19) ^ (M[x - 2] >> 10) + M[x] = M[x - 16] &+ s0 &+ M[x - 7] &+ s1 + } + } - var A = hh[0] - var B = hh[1] - var C = hh[2] - var D = hh[3] - var E = hh[4] - var F = hh[5] - var G = hh[6] - var H = hh[7] - - // Main loop - for j in 0.., isLast: Bool = false) throws -> Array { - self.accumulated += bytes + @inlinable + public func update(withBytes bytes: ArraySlice, isLast: Bool = false) throws -> [UInt8] { + self.accumulated += bytes - if isLast { - let lengthInBits = (processedBytesTotalCount + self.accumulated.count) * 8 - let lengthBytes = lengthInBits.bytes(totalBytes: self.blockSize / 8) // A 64-bit/128-bit representation of b. blockSize fit by accident. + if isLast { + let lengthInBits = (processedBytesTotalCount + self.accumulated.count) * 8 + let lengthBytes = lengthInBits.bytes(totalBytes: self.blockSize / 8) + // A 64-bit/128-bit representation of b. blockSize fit by accident. - // Step 1. Append padding - bitPadding(to: &self.accumulated, blockSize: self.blockSize, allowance: self.blockSize / 8) + // Step 1. Append padding + bitPadding(to: &self.accumulated, blockSize: self.blockSize, allowance: self.blockSize / 8) - // Step 2. Append Length a 64-bit representation of lengthInBits - self.accumulated += lengthBytes - } - - var processedBytes = 0 - for chunk in self.accumulated.batched(by: self.blockSize) { - if isLast || (self.accumulated.count - processedBytes) >= self.blockSize { - switch self.variant { - case .sha224, .sha256: - self.process32(block: chunk, currentHash: &self.accumulatedHash32) - case .sha384, .sha512: - self.process64(block: chunk, currentHash: &self.accumulatedHash64) - } - processedBytes += chunk.count - } - } - self.accumulated.removeFirst(processedBytes) - self.processedBytesTotalCount += processedBytes - - // output current hash - var result = Array(repeating: 0, count: variant.digestLength) - switch self.variant { - case .sha224, .sha256: - var pos = 0 - for idx in 0..> 24) & 0xff) - result[pos + 1] = UInt8((h >> 16) & 0xff) - result[pos + 2] = UInt8((h >> 8) & 0xff) - result[pos + 3] = UInt8(h & 0xff) - pos += 4 + // Step 2. Append Length a 64-bit representation of lengthInBits + self.accumulated += lengthBytes } - case .sha384, .sha512: - var pos = 0 - for idx in 0..> 56) & 0xff) - result[pos + 1] = UInt8((h >> 48) & 0xff) - result[pos + 2] = UInt8((h >> 40) & 0xff) - result[pos + 3] = UInt8((h >> 32) & 0xff) - result[pos + 4] = UInt8((h >> 24) & 0xff) - result[pos + 5] = UInt8((h >> 16) & 0xff) - result[pos + 6] = UInt8((h >> 8) & 0xff) - result[pos + 7] = UInt8(h & 0xff) - pos += 8 + + var processedBytes = 0 + for chunk in self.accumulated.batched(by: self.blockSize) { + if isLast || (self.accumulated.count - processedBytes) >= self.blockSize { + switch self.variant { + case .sha224, .sha256: + self.process32(block: chunk, currentHash: &self.accumulatedHash32) + case .sha384, .sha512: + self.process64(block: chunk, currentHash: &self.accumulatedHash64) + } + processedBytes += chunk.count + } } - } + self.accumulated.removeFirst(processedBytes) + self.processedBytesTotalCount += processedBytes - // reset hash value for instance - if isLast { - switch self.variant { + // output current hash + var result = [UInt8](repeating: 0, count: variant.digestLength) + switch self.variant { case .sha224, .sha256: - self.accumulatedHash32 = self.variant.h.lazy.map { UInt32($0) } // FIXME: UInt64 for process64 + var pos = 0 + for idx in 0..> 24) & 0xff) + result[pos + 1] = UInt8((h >> 16) & 0xff) + result[pos + 2] = UInt8((h >> 8) & 0xff) + result[pos + 3] = UInt8(h & 0xff) + pos += 4 + } case .sha384, .sha512: - self.accumulatedHash64 = self.variant.h - } - } + var pos = 0 + for idx in 0..> 56) & 0xff) + result[pos + 1] = UInt8((h >> 48) & 0xff) + result[pos + 2] = UInt8((h >> 40) & 0xff) + result[pos + 3] = UInt8((h >> 32) & 0xff) + result[pos + 4] = UInt8((h >> 24) & 0xff) + result[pos + 5] = UInt8((h >> 16) & 0xff) + result[pos + 6] = UInt8((h >> 8) & 0xff) + result[pos + 7] = UInt8(h & 0xff) + pos += 8 + } + } + + // reset hash value for instance + if isLast { + switch self.variant { + case .sha224, .sha256: + // FIXME: UInt64 for process64 + self.accumulatedHash32 = self.variant.h.lazy.map { UInt32($0) } + case .sha384, .sha512: + self.accumulatedHash64 = self.variant.h + } + } - return result - } -} \ No newline at end of file + return result + } +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA3.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA3.swift index 24027062c..6fceac6d3 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA3.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA3.swift @@ -40,278 +40,277 @@ import ucrt #endif public final class SHA3: DigestType { - let round_constants: Array = [ - 0x0000000000000001, 0x0000000000008082, 0x800000000000808a, 0x8000000080008000, - 0x000000000000808b, 0x0000000080000001, 0x8000000080008081, 0x8000000000008009, - 0x000000000000008a, 0x0000000000000088, 0x0000000080008009, 0x000000008000000a, - 0x000000008000808b, 0x800000000000008b, 0x8000000000008089, 0x8000000000008003, - 0x8000000000008002, 0x8000000000000080, 0x000000000000800a, 0x800000008000000a, - 0x8000000080008081, 0x8000000000008080, 0x0000000080000001, 0x8000000080008008 - ] - - public let blockSize: Int - public let digestLength: Int - public let markByte: UInt8 - - @usableFromInline - var accumulated = Array() - + let round_constants: [UInt64] = [ + 0x0000_0000_0000_0001, 0x0000_0000_0000_8082, 0x8000_0000_0000_808a, 0x8000_0000_8000_8000, + 0x0000_0000_0000_808b, 0x0000_0000_8000_0001, 0x8000_0000_8000_8081, 0x8000_0000_0000_8009, + 0x0000_0000_0000_008a, 0x0000_0000_0000_0088, 0x0000_0000_8000_8009, 0x0000_0000_8000_000a, + 0x0000_0000_8000_808b, 0x8000_0000_0000_008b, 0x8000_0000_0000_8089, 0x8000_0000_0000_8003, + 0x8000_0000_0000_8002, 0x8000_0000_0000_0080, 0x0000_0000_0000_800a, 0x8000_0000_8000_000a, + 0x8000_0000_8000_8081, 0x8000_0000_0000_8080, 0x0000_0000_8000_0001, 0x8000_0000_8000_8008, + ] + + public let blockSize: Int + public let digestLength: Int + public let markByte: UInt8 + + @usableFromInline + var accumulated = [UInt8]() + + @usableFromInline + var accumulatedHash: [UInt64] + + public enum Variant { + case sha224, sha256, sha384, sha512, keccak224, keccak256, keccak384, keccak512 + + var digestLength: Int { + 100 - (self.blockSize / 2) + } - @usableFromInline - var accumulatedHash: Array + var blockSize: Int { + (1600 - self.outputLength * 2) / 8 + } - public enum Variant { - case sha224, sha256, sha384, sha512, keccak224, keccak256, keccak384, keccak512 + var markByte: UInt8 { + switch self { + case .sha224, .sha256, .sha384, .sha512: + return 0x06 // 0x1F for SHAKE + case .keccak224, .keccak256, .keccak384, .keccak512: + return 0x01 + } + } - var digestLength: Int { - 100 - (self.blockSize / 2) + public var outputLength: Int { + switch self { + case .sha224, .keccak224: + return 224 + case .sha256, .keccak256: + return 256 + case .sha384, .keccak384: + return 384 + case .sha512, .keccak512: + return 512 + } + } } - var blockSize: Int { - (1600 - self.outputLength * 2) / 8 + public init(variant: SHA3.Variant) { + self.blockSize = variant.blockSize + self.digestLength = variant.digestLength + self.markByte = variant.markByte + self.accumulatedHash = [UInt64](repeating: 0, count: self.digestLength) } - var markByte: UInt8 { - switch self { - case .sha224, .sha256, .sha384, .sha512: - return 0x06 // 0x1F for SHAKE - case .keccak224, .keccak256, .keccak384, .keccak512: - return 0x01 - } + @inlinable + public func calculate(for bytes: [UInt8]) -> [UInt8] { + do { + return try update(withBytes: bytes.slice, isLast: true) + } catch { + return [] + } } - public var outputLength: Int { - switch self { - case .sha224, .keccak224: - return 224 - case .sha256, .keccak256: - return 256 - case .sha384, .keccak384: - return 384 - case .sha512, .keccak512: - return 512 - } + public func callAsFunction(_ bytes: [UInt8]) -> [UInt8] { + calculate(for: bytes) } - } - - public init(variant: SHA3.Variant) { - self.blockSize = variant.blockSize - self.digestLength = variant.digestLength - self.markByte = variant.markByte - self.accumulatedHash = Array(repeating: 0, count: self.digestLength) - } - - @inlinable - public func calculate(for bytes: Array) -> Array { - do { - return try update(withBytes: bytes.slice, isLast: true) - } catch { - return [] - } - } - - public func callAsFunction(_ bytes: Array) -> Array { - calculate(for: bytes) - } - - /// 1. For all pairs (x,z) such that 0≤x<5 and 0≤z) { - let c = UnsafeMutablePointer.allocate(capacity: 5) - c.initialize(repeating: 0, count: 5) - defer { - c.deinitialize(count: 5) - c.deallocate() - } - let d = UnsafeMutablePointer.allocate(capacity: 5) - d.initialize(repeating: 0, count: 5) - defer { - d.deinitialize(count: 5) - d.deallocate() + + /// 1. For all pairs (x,z) such that 0≤x<5 and 0≤z.allocate(capacity: 5) + c.initialize(repeating: 0, count: 5) + defer { + c.deinitialize(count: 5) + c.deallocate() + } + let d = UnsafeMutablePointer.allocate(capacity: 5) + d.initialize(repeating: 0, count: 5) + defer { + d.deinitialize(count: 5) + d.deallocate() + } + + for i in 0..<5 { + c[i] = a[i] ^ a[i &+ 5] ^ a[i &+ 10] ^ a[i &+ 15] ^ a[i &+ 20] + } + + d[0] = rotateLeft(c[1], by: 1) ^ c[4] + d[1] = rotateLeft(c[2], by: 1) ^ c[0] + d[2] = rotateLeft(c[3], by: 1) ^ c[1] + d[3] = rotateLeft(c[4], by: 1) ^ c[2] + d[4] = rotateLeft(c[0], by: 1) ^ c[3] + + for i in 0..<5 { + a[i] ^= d[i] + a[i &+ 5] ^= d[i] + a[i &+ 10] ^= d[i] + a[i &+ 15] ^= d[i] + a[i &+ 20] ^= d[i] + } } - for i in 0..<5 { - c[i] = a[i] ^ a[i &+ 5] ^ a[i &+ 10] ^ a[i &+ 15] ^ a[i &+ 20] + /// A′[x, y, z]=A[(x &+ 3y) mod 5, x, z] + private func π(_ a: inout [UInt64]) { + let a1 = a[1] + a[1] = a[6] + a[6] = a[9] + a[9] = a[22] + a[22] = a[14] + a[14] = a[20] + a[20] = a[2] + a[2] = a[12] + a[12] = a[13] + a[13] = a[19] + a[19] = a[23] + a[23] = a[15] + a[15] = a[4] + a[4] = a[24] + a[24] = a[21] + a[21] = a[8] + a[8] = a[16] + a[16] = a[5] + a[5] = a[3] + a[3] = a[18] + a[18] = a[17] + a[17] = a[11] + a[11] = a[7] + a[7] = a[10] + a[10] = a1 } - d[0] = rotateLeft(c[1], by: 1) ^ c[4] - d[1] = rotateLeft(c[2], by: 1) ^ c[0] - d[2] = rotateLeft(c[3], by: 1) ^ c[1] - d[3] = rotateLeft(c[4], by: 1) ^ c[2] - d[4] = rotateLeft(c[0], by: 1) ^ c[3] - - for i in 0..<5 { - a[i] ^= d[i] - a[i &+ 5] ^= d[i] - a[i &+ 10] ^= d[i] - a[i &+ 15] ^= d[i] - a[i &+ 20] ^= d[i] + /// For all triples (x, y, z) such that 0≤x<5, 0≤y<5, and 0≤z) { - let a1 = a[1] - a[1] = a[6] - a[6] = a[9] - a[9] = a[22] - a[22] = a[14] - a[14] = a[20] - a[20] = a[2] - a[2] = a[12] - a[12] = a[13] - a[13] = a[19] - a[19] = a[23] - a[23] = a[15] - a[15] = a[4] - a[4] = a[24] - a[24] = a[21] - a[21] = a[8] - a[8] = a[16] - a[16] = a[5] - a[5] = a[3] - a[3] = a[18] - a[18] = a[17] - a[17] = a[11] - a[11] = a[7] - a[7] = a[10] - a[10] = a1 - } - - /// For all triples (x, y, z) such that 0≤x<5, 0≤y<5, and 0≤z) { - for i in stride(from: 0, to: 25, by: 5) { - let a0 = a[0 &+ i] - let a1 = a[1 &+ i] - a[0 &+ i] ^= ~a1 & a[2 &+ i] - a[1 &+ i] ^= ~a[2 &+ i] & a[3 &+ i] - a[2 &+ i] ^= ~a[3 &+ i] & a[4 &+ i] - a[3 &+ i] ^= ~a[4 &+ i] & a0 - a[4 &+ i] ^= ~a0 & a1 + + private func ι(_ a: inout [UInt64], round: Int) { + a[0] ^= self.round_constants[round] } - } - - private func ι(_ a: inout Array, round: Int) { - a[0] ^= self.round_constants[round] - } - - @usableFromInline - func process(block chunk: ArraySlice, currentHash hh: inout Array) { - // expand - hh[0] ^= chunk[0].littleEndian - hh[1] ^= chunk[1].littleEndian - hh[2] ^= chunk[2].littleEndian - hh[3] ^= chunk[3].littleEndian - hh[4] ^= chunk[4].littleEndian - hh[5] ^= chunk[5].littleEndian - hh[6] ^= chunk[6].littleEndian - hh[7] ^= chunk[7].littleEndian - hh[8] ^= chunk[8].littleEndian - if self.blockSize > 72 { // 72 / 8, sha-512 - hh[9] ^= chunk[9].littleEndian - hh[10] ^= chunk[10].littleEndian - hh[11] ^= chunk[11].littleEndian - hh[12] ^= chunk[12].littleEndian - if self.blockSize > 104 { // 104 / 8, sha-384 - hh[13] ^= chunk[13].littleEndian - hh[14] ^= chunk[14].littleEndian - hh[15] ^= chunk[15].littleEndian - hh[16] ^= chunk[16].littleEndian - if self.blockSize > 136 { // 136 / 8, sha-256 - hh[17] ^= chunk[17].littleEndian - // FULL_SHA3_FAMILY_SUPPORT - if self.blockSize > 144 { // 144 / 8, sha-224 - hh[18] ^= chunk[18].littleEndian - hh[19] ^= chunk[19].littleEndian - hh[20] ^= chunk[20].littleEndian - hh[21] ^= chunk[21].littleEndian - hh[22] ^= chunk[22].littleEndian - hh[23] ^= chunk[23].littleEndian - hh[24] ^= chunk[24].littleEndian - } + + @usableFromInline + func process(block chunk: ArraySlice, currentHash hh: inout [UInt64]) { + // expand + hh[0] ^= chunk[0].littleEndian + hh[1] ^= chunk[1].littleEndian + hh[2] ^= chunk[2].littleEndian + hh[3] ^= chunk[3].littleEndian + hh[4] ^= chunk[4].littleEndian + hh[5] ^= chunk[5].littleEndian + hh[6] ^= chunk[6].littleEndian + hh[7] ^= chunk[7].littleEndian + hh[8] ^= chunk[8].littleEndian + if self.blockSize > 72 { // 72 / 8, sha-512 + hh[9] ^= chunk[9].littleEndian + hh[10] ^= chunk[10].littleEndian + hh[11] ^= chunk[11].littleEndian + hh[12] ^= chunk[12].littleEndian + if self.blockSize > 104 { // 104 / 8, sha-384 + hh[13] ^= chunk[13].littleEndian + hh[14] ^= chunk[14].littleEndian + hh[15] ^= chunk[15].littleEndian + hh[16] ^= chunk[16].littleEndian + if self.blockSize > 136 { // 136 / 8, sha-256 + hh[17] ^= chunk[17].littleEndian + // FULL_SHA3_FAMILY_SUPPORT + if self.blockSize > 144 { // 144 / 8, sha-224 + hh[18] ^= chunk[18].littleEndian + hh[19] ^= chunk[19].littleEndian + hh[20] ^= chunk[20].littleEndian + hh[21] ^= chunk[21].littleEndian + hh[22] ^= chunk[22].littleEndian + hh[23] ^= chunk[23].littleEndian + hh[24] ^= chunk[24].littleEndian + } + } + } } - } - } - // Keccak-f - for round in 0..<24 { - self.θ(&hh) - - hh[1] = rotateLeft(hh[1], by: 1) - hh[2] = rotateLeft(hh[2], by: 62) - hh[3] = rotateLeft(hh[3], by: 28) - hh[4] = rotateLeft(hh[4], by: 27) - hh[5] = rotateLeft(hh[5], by: 36) - hh[6] = rotateLeft(hh[6], by: 44) - hh[7] = rotateLeft(hh[7], by: 6) - hh[8] = rotateLeft(hh[8], by: 55) - hh[9] = rotateLeft(hh[9], by: 20) - hh[10] = rotateLeft(hh[10], by: 3) - hh[11] = rotateLeft(hh[11], by: 10) - hh[12] = rotateLeft(hh[12], by: 43) - hh[13] = rotateLeft(hh[13], by: 25) - hh[14] = rotateLeft(hh[14], by: 39) - hh[15] = rotateLeft(hh[15], by: 41) - hh[16] = rotateLeft(hh[16], by: 45) - hh[17] = rotateLeft(hh[17], by: 15) - hh[18] = rotateLeft(hh[18], by: 21) - hh[19] = rotateLeft(hh[19], by: 8) - hh[20] = rotateLeft(hh[20], by: 18) - hh[21] = rotateLeft(hh[21], by: 2) - hh[22] = rotateLeft(hh[22], by: 61) - hh[23] = rotateLeft(hh[23], by: 56) - hh[24] = rotateLeft(hh[24], by: 14) - - self.π(&hh) - self.χ(&hh) - self.ι(&hh, round: round) + // Keccak-f + for round in 0..<24 { + self.θ(&hh) + + hh[1] = rotateLeft(hh[1], by: 1) + hh[2] = rotateLeft(hh[2], by: 62) + hh[3] = rotateLeft(hh[3], by: 28) + hh[4] = rotateLeft(hh[4], by: 27) + hh[5] = rotateLeft(hh[5], by: 36) + hh[6] = rotateLeft(hh[6], by: 44) + hh[7] = rotateLeft(hh[7], by: 6) + hh[8] = rotateLeft(hh[8], by: 55) + hh[9] = rotateLeft(hh[9], by: 20) + hh[10] = rotateLeft(hh[10], by: 3) + hh[11] = rotateLeft(hh[11], by: 10) + hh[12] = rotateLeft(hh[12], by: 43) + hh[13] = rotateLeft(hh[13], by: 25) + hh[14] = rotateLeft(hh[14], by: 39) + hh[15] = rotateLeft(hh[15], by: 41) + hh[16] = rotateLeft(hh[16], by: 45) + hh[17] = rotateLeft(hh[17], by: 15) + hh[18] = rotateLeft(hh[18], by: 21) + hh[19] = rotateLeft(hh[19], by: 8) + hh[20] = rotateLeft(hh[20], by: 18) + hh[21] = rotateLeft(hh[21], by: 2) + hh[22] = rotateLeft(hh[22], by: 61) + hh[23] = rotateLeft(hh[23], by: 56) + hh[24] = rotateLeft(hh[24], by: 14) + + self.π(&hh) + self.χ(&hh) + self.ι(&hh, round: round) + } } - } } extension SHA3: Updatable { - @inlinable - public func update(withBytes bytes: ArraySlice, isLast: Bool = false) throws -> Array { - self.accumulated += bytes + @inlinable + public func update(withBytes bytes: ArraySlice, isLast: Bool = false) throws -> [UInt8] { + self.accumulated += bytes - if isLast { - // Add padding - let markByteIndex = self.accumulated.count + if isLast { + // Add padding + let markByteIndex = self.accumulated.count - // We need to always pad the input. Even if the input is a multiple of blockSize. - let r = self.blockSize * 8 - let q = (r / 8) - (accumulated.count % (r / 8)) - self.accumulated += Array(repeating: 0, count: q) + // We need to always pad the input. Even if the input is a multiple of blockSize. + let r = self.blockSize * 8 + let q = (r / 8) - (accumulated.count % (r / 8)) + self.accumulated += [UInt8](repeating: 0, count: q) - self.accumulated[markByteIndex] |= self.markByte - self.accumulated[self.accumulated.count - 1] |= 0x80 - } + self.accumulated[markByteIndex] |= self.markByte + self.accumulated[self.accumulated.count - 1] |= 0x80 + } - var processedBytes = 0 - for chunk in self.accumulated.batched(by: self.blockSize) { - if isLast || (self.accumulated.count - processedBytes) >= self.blockSize { - self.process(block: chunk.toUInt64Array().slice, currentHash: &self.accumulatedHash) - processedBytes += chunk.count - } - } - self.accumulated.removeFirst(processedBytes) + var processedBytes = 0 + for chunk in self.accumulated.batched(by: self.blockSize) { + if isLast || (self.accumulated.count - processedBytes) >= self.blockSize { + self.process(block: chunk.toUInt64Array().slice, currentHash: &self.accumulatedHash) + processedBytes += chunk.count + } + } + self.accumulated.removeFirst(processedBytes) - // TODO: verify performance, reduce vs for..in - let result = self.accumulatedHash.reduce(into: Array()) { (result, value) in - result += value.bigEndian.bytes() - } + // TODO: verify performance, reduce vs for..in + let result = self.accumulatedHash.reduce(into: [UInt8]()) { (result, value) in + result += value.bigEndian.bytes() + } - // reset hash value for instance - if isLast { - self.accumulatedHash = Array(repeating: 0, count: self.digestLength) - } + // reset hash value for instance + if isLast { + self.accumulatedHash = [UInt64](repeating: 0, count: self.digestLength) + } - return Array(result[0..) - init(bytes: T) where T.Element == UInt8, T.Index == Int { - self = UInt16(bytes: bytes, fromIndex: bytes.startIndex) - } - - @_specialize(where T == ArraySlice) - init(bytes: T, fromIndex index: T.Index) where T.Element == UInt8, T.Index == Int { - if bytes.isEmpty { - self = 0 - return + @_specialize(where T == ArraySlice) + init(bytes: T) where T.Element == UInt8, T.Index == Int { + self = UInt16(bytes: bytes, fromIndex: bytes.startIndex) } - let count = bytes.count + @_specialize(where T == ArraySlice) + init(bytes: T, fromIndex index: T.Index) where T.Element == UInt8, T.Index == Int { + if bytes.isEmpty { + self = 0 + return + } + + let count = bytes.count - let val0 = count > 0 ? UInt16(bytes[index.advanced(by: 0)]) << 8 : 0 - let val1 = count > 1 ? UInt16(bytes[index.advanced(by: 1)]) : 0 + let val0 = count > 0 ? UInt16(bytes[index.advanced(by: 0)]) << 8 : 0 + let val1 = count > 1 ? UInt16(bytes[index.advanced(by: 1)]) : 0 - self = val0 | val1 - } + self = val0 | val1 + } } diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt32+Extension.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt32+Extension.swift index 18e1599a9..e0e6434fa 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt32+Extension.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt32+Extension.swift @@ -38,28 +38,28 @@ import ucrt protocol _UInt32Type {} extension UInt32: _UInt32Type {} -/** array of bytes */ +/// array of bytes extension UInt32 { - @_specialize(where T == ArraySlice) - init(bytes: T) where T.Element == UInt8, T.Index == Int { - self = UInt32(bytes: bytes, fromIndex: bytes.startIndex) - } - - @_specialize(where T == ArraySlice) - @inlinable - init(bytes: T, fromIndex index: T.Index) where T.Element == UInt8, T.Index == Int { - if bytes.isEmpty { - self = 0 - return + @_specialize(where T == ArraySlice) + init(bytes: T) where T.Element == UInt8, T.Index == Int { + self = UInt32(bytes: bytes, fromIndex: bytes.startIndex) } - let count = bytes.count + @_specialize(where T == ArraySlice) + @inlinable + init(bytes: T, fromIndex index: T.Index) where T.Element == UInt8, T.Index == Int { + if bytes.isEmpty { + self = 0 + return + } + + let count = bytes.count - let val0 = count > 0 ? UInt32(bytes[index.advanced(by: 0)]) << 24 : 0 - let val1 = count > 1 ? UInt32(bytes[index.advanced(by: 1)]) << 16 : 0 - let val2 = count > 2 ? UInt32(bytes[index.advanced(by: 2)]) << 8 : 0 - let val3 = count > 3 ? UInt32(bytes[index.advanced(by: 3)]) : 0 + let val0 = count > 0 ? UInt32(bytes[index.advanced(by: 0)]) << 24 : 0 + let val1 = count > 1 ? UInt32(bytes[index.advanced(by: 1)]) << 16 : 0 + let val2 = count > 2 ? UInt32(bytes[index.advanced(by: 2)]) << 8 : 0 + let val3 = count > 3 ? UInt32(bytes[index.advanced(by: 3)]) : 0 - self = val0 | val1 | val2 | val3 - } + self = val0 | val1 | val2 | val3 + } } diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt64+Extension.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt64+Extension.swift index 1bbdc79bf..afcd7e47b 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt64+Extension.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt64+Extension.swift @@ -27,32 +27,32 @@ // - This notice may not be removed or altered from any source or binary distribution. // -/** array of bytes */ +/// array of bytes extension UInt64 { - @_specialize(where T == ArraySlice) - init(bytes: T) where T.Element == UInt8, T.Index == Int { - self = UInt64(bytes: bytes, fromIndex: bytes.startIndex) - } - - @_specialize(where T == ArraySlice) - @inlinable - init(bytes: T, fromIndex index: T.Index) where T.Element == UInt8, T.Index == Int { - if bytes.isEmpty { - self = 0 - return + @_specialize(where T == ArraySlice) + init(bytes: T) where T.Element == UInt8, T.Index == Int { + self = UInt64(bytes: bytes, fromIndex: bytes.startIndex) } - let count = bytes.count + @_specialize(where T == ArraySlice) + @inlinable + init(bytes: T, fromIndex index: T.Index) where T.Element == UInt8, T.Index == Int { + if bytes.isEmpty { + self = 0 + return + } + + let count = bytes.count - let val0 = count > 0 ? UInt64(bytes[index.advanced(by: 0)]) << 56 : 0 - let val1 = count > 1 ? UInt64(bytes[index.advanced(by: 1)]) << 48 : 0 - let val2 = count > 2 ? UInt64(bytes[index.advanced(by: 2)]) << 40 : 0 - let val3 = count > 3 ? UInt64(bytes[index.advanced(by: 3)]) << 32 : 0 - let val4 = count > 4 ? UInt64(bytes[index.advanced(by: 4)]) << 24 : 0 - let val5 = count > 5 ? UInt64(bytes[index.advanced(by: 5)]) << 16 : 0 - let val6 = count > 6 ? UInt64(bytes[index.advanced(by: 6)]) << 8 : 0 - let val7 = count > 7 ? UInt64(bytes[index.advanced(by: 7)]) : 0 + let val0 = count > 0 ? UInt64(bytes[index.advanced(by: 0)]) << 56 : 0 + let val1 = count > 1 ? UInt64(bytes[index.advanced(by: 1)]) << 48 : 0 + let val2 = count > 2 ? UInt64(bytes[index.advanced(by: 2)]) << 40 : 0 + let val3 = count > 3 ? UInt64(bytes[index.advanced(by: 3)]) << 32 : 0 + let val4 = count > 4 ? UInt64(bytes[index.advanced(by: 4)]) << 24 : 0 + let val5 = count > 5 ? UInt64(bytes[index.advanced(by: 5)]) << 16 : 0 + let val6 = count > 6 ? UInt64(bytes[index.advanced(by: 6)]) << 8 : 0 + let val7 = count > 7 ? UInt64(bytes[index.advanced(by: 7)]) : 0 - self = val0 | val1 | val2 | val3 | val4 | val5 | val6 | val7 - } + self = val0 | val1 | val2 | val3 | val4 | val5 | val6 | val7 + } } diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt8+Extension.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt8+Extension.swift index b65ed5f5e..f66510135 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt8+Extension.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt8+Extension.swift @@ -38,51 +38,51 @@ import ucrt public protocol _UInt8Type {} extension UInt8: _UInt8Type {} -/** casting */ +/// casting extension UInt8 { - /** cast because UInt8() because std initializer crash if value is > byte */ - static func with(value: UInt64) -> UInt8 { - let tmp = value & 0xff - return UInt8(tmp) - } + // cast because UInt8() because std initializer crash if value is > byte + static func with(value: UInt64) -> UInt8 { + let tmp = value & 0xff + return UInt8(tmp) + } - static func with(value: UInt32) -> UInt8 { - let tmp = value & 0xff - return UInt8(tmp) - } + static func with(value: UInt32) -> UInt8 { + let tmp = value & 0xff + return UInt8(tmp) + } - static func with(value: UInt16) -> UInt8 { - let tmp = value & 0xff - return UInt8(tmp) - } + static func with(value: UInt16) -> UInt8 { + let tmp = value & 0xff + return UInt8(tmp) + } } -/** Bits */ +/// Bits extension UInt8 { - /** array of bits */ - public func bits() -> [Bit] { - let totalBitsCount = MemoryLayout.size * 8 + // array of bits + public func bits() -> [Bit] { + let totalBitsCount = MemoryLayout.size * 8 - var bitsArray = [Bit](repeating: Bit.zero, count: totalBitsCount) + var bitsArray = [Bit](repeating: Bit.zero, count: totalBitsCount) - for j in 0.. String { - var s = String() - let arr: [Bit] = self.bits() - for idx in arr.indices { - s += (arr[idx] == Bit.one ? "1" : "0") - if idx.advanced(by: 1) % 8 == 0 { s += " " } + public func bits() -> String { + var s = String() + let arr: [Bit] = self.bits() + for idx in arr.indices { + s += (arr[idx] == Bit.one ? "1" : "0") + if idx.advanced(by: 1) % 8 == 0 { s += " " } + } + return s } - return s - } } diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Updatable.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Updatable.swift index e54fc60df..94d0efaa8 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Updatable.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Updatable.swift @@ -30,92 +30,100 @@ /// A type that supports incremental updates. For example Digest or Cipher may be updatable /// and calculate result incerementally. public protocol Updatable { - /// Update given bytes in chunks. - /// - /// - parameter bytes: Bytes to process. - /// - parameter isLast: Indicate if given chunk is the last one. No more updates after this call. - /// - returns: Processed partial result data or empty array. - mutating func update(withBytes bytes: ArraySlice, isLast: Bool) throws -> Array + /// Update given bytes in chunks. + /// + /// - parameter bytes: Bytes to process. + /// - parameter isLast: Indicate if given chunk is the last one. No more updates after this call. + /// - returns: Processed partial result data or empty array. + mutating func update(withBytes bytes: ArraySlice, isLast: Bool) throws -> [UInt8] - /// Update given bytes in chunks. - /// - /// - Parameters: - /// - bytes: Bytes to process. - /// - isLast: Indicate if given chunk is the last one. No more updates after this call. - /// - output: Resulting bytes callback. - /// - Returns: Processed partial result data or empty array. - mutating func update(withBytes bytes: ArraySlice, isLast: Bool, output: (_ bytes: Array) -> Void) throws + /// Update given bytes in chunks. + /// + /// - Parameters: + /// - bytes: Bytes to process. + /// - isLast: Indicate if given chunk is the last one. No more updates after this call. + /// - output: Resulting bytes callback. + /// - Returns: Processed partial result data or empty array. + mutating func update(withBytes bytes: ArraySlice, isLast: Bool, output: (_ bytes: [UInt8]) -> Void) throws } extension Updatable { - @inlinable - public mutating func update(withBytes bytes: ArraySlice, isLast: Bool = false, output: (_ bytes: Array) -> Void) throws { - let processed = try update(withBytes: bytes, isLast: isLast) - if !processed.isEmpty { - output(processed) + @inlinable + public mutating func update( + withBytes bytes: ArraySlice, + isLast: Bool = false, + output: (_ bytes: [UInt8]) -> Void + ) throws { + let processed = try update(withBytes: bytes, isLast: isLast) + if !processed.isEmpty { + output(processed) + } } - } - @inlinable - public mutating func update(withBytes bytes: ArraySlice, isLast: Bool = false) throws -> Array { - try self.update(withBytes: bytes, isLast: isLast) - } + @inlinable + public mutating func update(withBytes bytes: ArraySlice, isLast: Bool = false) throws -> [UInt8] { + try self.update(withBytes: bytes, isLast: isLast) + } - @inlinable - public mutating func update(withBytes bytes: Array, isLast: Bool = false) throws -> Array { - try self.update(withBytes: bytes.slice, isLast: isLast) - } + @inlinable + public mutating func update(withBytes bytes: [UInt8], isLast: Bool = false) throws -> [UInt8] { + try self.update(withBytes: bytes.slice, isLast: isLast) + } - @inlinable - public mutating func update(withBytes bytes: Array, isLast: Bool = false, output: (_ bytes: Array) -> Void) throws { - try self.update(withBytes: bytes.slice, isLast: isLast, output: output) - } + @inlinable + public mutating func update( + withBytes bytes: [UInt8], + isLast: Bool = false, + output: (_ bytes: [UInt8]) -> Void + ) throws { + try self.update(withBytes: bytes.slice, isLast: isLast, output: output) + } - /// Finish updates. This may apply padding. - /// - parameter bytes: Bytes to process - /// - returns: Processed data. - @inlinable - public mutating func finish(withBytes bytes: ArraySlice) throws -> Array { - try self.update(withBytes: bytes, isLast: true) - } + /// Finish updates. This may apply padding. + /// - parameter bytes: Bytes to process + /// - returns: Processed data. + @inlinable + public mutating func finish(withBytes bytes: ArraySlice) throws -> [UInt8] { + try self.update(withBytes: bytes, isLast: true) + } - @inlinable - public mutating func finish(withBytes bytes: Array) throws -> Array { - try self.finish(withBytes: bytes.slice) - } + @inlinable + public mutating func finish(withBytes bytes: [UInt8]) throws -> [UInt8] { + try self.finish(withBytes: bytes.slice) + } - /// Finish updates. May add padding. - /// - /// - Returns: Processed data - /// - Throws: Error - @inlinable - public mutating func finish() throws -> Array { - try self.update(withBytes: [], isLast: true) - } + /// Finish updates. May add padding. + /// + /// - Returns: Processed data + /// - Throws: Error + @inlinable + public mutating func finish() throws -> [UInt8] { + try self.update(withBytes: [], isLast: true) + } - /// Finish updates. This may apply padding. - /// - parameter bytes: Bytes to process - /// - parameter output: Resulting data - /// - returns: Processed data. - @inlinable - public mutating func finish(withBytes bytes: ArraySlice, output: (_ bytes: Array) -> Void) throws { - let processed = try update(withBytes: bytes, isLast: true) - if !processed.isEmpty { - output(processed) + /// Finish updates. This may apply padding. + /// - parameter bytes: Bytes to process + /// - parameter output: Resulting data + /// - returns: Processed data. + @inlinable + public mutating func finish(withBytes bytes: ArraySlice, output: (_ bytes: [UInt8]) -> Void) throws { + let processed = try update(withBytes: bytes, isLast: true) + if !processed.isEmpty { + output(processed) + } } - } - @inlinable - public mutating func finish(withBytes bytes: Array, output: (_ bytes: Array) -> Void) throws { - try self.finish(withBytes: bytes.slice, output: output) - } + @inlinable + public mutating func finish(withBytes bytes: [UInt8], output: (_ bytes: [UInt8]) -> Void) throws { + try self.finish(withBytes: bytes.slice, output: output) + } - /// Finish updates. May add padding. - /// - /// - Parameter output: Processed data - /// - Throws: Error - @inlinable - public mutating func finish(output: (Array) -> Void) throws { - try self.finish(withBytes: [], output: output) - } -} \ No newline at end of file + /// Finish updates. May add padding. + /// + /// - Parameter output: Processed data + /// - Throws: Error + @inlinable + public mutating func finish(output: ([UInt8]) -> Void) throws { + try self.finish(withBytes: [], output: output) + } +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Utils.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Utils.swift index 6a5ad9fd5..8a4ea7761 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Utils.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Utils.swift @@ -29,103 +29,117 @@ @inlinable func rotateLeft(_ value: UInt8, by: UInt8) -> UInt8 { - ((value << by) & 0xff) | (value >> (8 - by)) + ((value << by) & 0xff) | (value >> (8 - by)) } @inlinable func rotateLeft(_ value: UInt16, by: UInt16) -> UInt16 { - ((value << by) & 0xffff) | (value >> (16 - by)) + ((value << by) & 0xffff) | (value >> (16 - by)) } @inlinable func rotateLeft(_ value: UInt32, by: UInt32) -> UInt32 { - ((value << by) & 0xffffffff) | (value >> (32 - by)) + ((value << by) & 0xffff_ffff) | (value >> (32 - by)) } @inlinable func rotateLeft(_ value: UInt64, by: UInt64) -> UInt64 { - (value << by) | (value >> (64 - by)) + (value << by) | (value >> (64 - by)) } @inlinable func rotateRight(_ value: UInt16, by: UInt16) -> UInt16 { - (value >> by) | (value << (16 - by)) + (value >> by) | (value << (16 - by)) } @inlinable func rotateRight(_ value: UInt32, by: UInt32) -> UInt32 { - (value >> by) | (value << (32 - by)) + (value >> by) | (value << (32 - by)) } @inlinable func rotateRight(_ value: UInt64, by: UInt64) -> UInt64 { - ((value >> by) | (value << (64 - by))) + ((value >> by) | (value << (64 - by))) } @inlinable func reversed(_ uint8: UInt8) -> UInt8 { - var v = uint8 - v = (v & 0xf0) >> 4 | (v & 0x0f) << 4 - v = (v & 0xcc) >> 2 | (v & 0x33) << 2 - v = (v & 0xaa) >> 1 | (v & 0x55) << 1 - return v + var v = uint8 + v = (v & 0xf0) >> 4 | (v & 0x0f) << 4 + v = (v & 0xcc) >> 2 | (v & 0x33) << 2 + v = (v & 0xaa) >> 1 | (v & 0x55) << 1 + return v } @inlinable func reversed(_ uint32: UInt32) -> UInt32 { - var v = uint32 - v = ((v >> 1) & 0x55555555) | ((v & 0x55555555) << 1) - v = ((v >> 2) & 0x33333333) | ((v & 0x33333333) << 2) - v = ((v >> 4) & 0x0f0f0f0f) | ((v & 0x0f0f0f0f) << 4) - v = ((v >> 8) & 0x00ff00ff) | ((v & 0x00ff00ff) << 8) - v = ((v >> 16) & 0xffff) | ((v & 0xffff) << 16) - return v + var v = uint32 + v = ((v >> 1) & 0x5555_5555) | ((v & 0x5555_5555) << 1) + v = ((v >> 2) & 0x3333_3333) | ((v & 0x3333_3333) << 2) + v = ((v >> 4) & 0x0f0f_0f0f) | ((v & 0x0f0f_0f0f) << 4) + v = ((v >> 8) & 0x00ff_00ff) | ((v & 0x00ff_00ff) << 8) + v = ((v >> 16) & 0xffff) | ((v & 0xffff) << 16) + return v } @inlinable -func xor(_ left: T, _ right: V) -> ArraySlice where T: RandomAccessCollection, V: RandomAccessCollection, T.Element == UInt8, T.Index == Int, V.Element == UInt8, V.Index == Int { - return xor(left, right).slice +func xor(_ left: T, _ right: V) -> ArraySlice +where + T: RandomAccessCollection, + V: RandomAccessCollection, + T.Element == UInt8, + T.Index == Int, + V.Element == UInt8, + V.Index == Int +{ + xor(left, right).slice } @inlinable -func xor(_ left: T, _ right: V) -> Array where T: RandomAccessCollection, V: RandomAccessCollection, T.Element == UInt8, T.Index == Int, V.Element == UInt8, V.Index == Int { - let length = Swift.min(left.count, right.count) - - let buf = UnsafeMutablePointer.allocate(capacity: length) - buf.initialize(repeating: 0, count: length) - defer { - buf.deinitialize(count: length) - buf.deallocate() - } - - // xor - for i in 0..(_ left: T, _ right: V) -> [UInt8] +where + T: RandomAccessCollection, + V: RandomAccessCollection, + T.Element == UInt8, + T.Index == Int, + V.Element == UInt8, + V.Index == Int +{ + let length = Swift.min(left.count, right.count) + + let buf = UnsafeMutablePointer.allocate(capacity: length) + buf.initialize(repeating: 0, count: length) + defer { + buf.deinitialize(count: length) + buf.deallocate() + } + + // xor + for i in 0.., blockSize: Int, allowance: Int = 0) { - let msgLength = data.count - // Step 1. Append Padding Bits - // append one bit (UInt8 with one bit) to message - data.append(0x80) - - // Step 2. append "0" bit until message length in bits ≡ 448 (mod 512) - let max = blockSize - allowance // 448, 986 - if msgLength % blockSize < max { // 448 - data += Array(repeating: 0, count: max - 1 - (msgLength % blockSize)) - } else { - data += Array(repeating: 0, count: blockSize + max - 1 - (msgLength % blockSize)) - } -} \ No newline at end of file +func bitPadding(to data: inout [UInt8], blockSize: Int, allowance: Int = 0) { + let msgLength = data.count + // Step 1. Append Padding Bits + // append one bit (UInt8 with one bit) to message + data.append(0x80) + + // Step 2. append "0" bit until message length in bits ≡ 448 (mod 512) + let max = blockSize - allowance // 448, 986 + if msgLength % blockSize < max { // 448 + data += [UInt8](repeating: 0, count: max - 1 - (msgLength % blockSize)) + } else { + data += [UInt8](repeating: 0, count: blockSize + max - 1 - (msgLength % blockSize)) + } +} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/ZeroPadding.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/ZeroPadding.swift index f6846a45f..346075982 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/ZeroPadding.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/ZeroPadding.swift @@ -30,25 +30,25 @@ /// All the bytes that are required to be padded are padded with zero. /// Zero padding may not be reversible if the original file ends with one or more zero bytes. struct ZeroPadding: PaddingProtocol { - init() { - } + init() { + } - @inlinable - func add(to bytes: Array, blockSize: Int) -> Array { - let paddingCount = blockSize - (bytes.count % blockSize) - if paddingCount > 0 { - return bytes + Array(repeating: 0, count: paddingCount) + @inlinable + func add(to bytes: [UInt8], blockSize: Int) -> [UInt8] { + let paddingCount = blockSize - (bytes.count % blockSize) + if paddingCount > 0 { + return bytes + [UInt8](repeating: 0, count: paddingCount) + } + return bytes } - return bytes - } - @inlinable - func remove(from bytes: Array, blockSize _: Int?) -> Array { - for (idx, value) in bytes.reversed().enumerated() { - if value != 0 { - return Array(bytes[0.. [UInt8] { + for (idx, value) in bytes.reversed().enumerated() { + if value != 0 { + return Array(bytes[0.. HTTPHeaders { + public func signHeaders( + url: URL, + method: HTTPMethod = .GET, + headers: HTTPHeaders = HTTPHeaders(), + body: BodyData? = nil, + date: Date = Date() + ) -> HTTPHeaders { let bodyHash = AWSSigner.hashedPayload(body) let dateString = AWSSigner.timestamp(date) var headers = headers @@ -72,14 +80,23 @@ public struct AWSSigner { headers.add(name: "x-amz-security-token", value: sessionToken) } - // construct signing data. Do this after adding the headers as it uses data from the headers - let signingData = AWSSigner.SigningData(url: url, method: method, headers: headers, body: body, bodyHash: bodyHash, date: dateString, signer: self) + // construct signing data. + // Do this after adding the headers as it uses data from the headers + let signingData = AWSSigner.SigningData( + url: url, + method: method, + headers: headers, + body: body, + bodyHash: bodyHash, + date: dateString, + signer: self + ) // construct authorization string - let authorization = "AWS4-HMAC-SHA256 " + - "Credential=\(credentials.accessKeyId)/\(signingData.date)/\(region)/\(name)/aws4_request, " + - "SignedHeaders=\(signingData.signedHeaders), " + - "Signature=\(signature(signingData: signingData))" + let authorization = + "AWS4-HMAC-SHA256 " + + "Credential=\(credentials.accessKeyId)/\(signingData.date)/\(region)/\(name)/aws4_request, " + + "SignedHeaders=\(signingData.signedHeaders), " + "Signature=\(signature(signingData: signingData))" // add Authorization header headers.add(name: "Authorization", value: authorization) @@ -88,11 +105,25 @@ public struct AWSSigner { } /// Generate a signed URL, for a HTTP request - public func signURL(url: URL, method: HTTPMethod = .GET, body: BodyData? = nil, date: Date = Date(), expires: Int = 86400) -> URL { + public func signURL( + url: URL, + method: HTTPMethod = .GET, + body: BodyData? = nil, + date: Date = Date(), + expires: Int = 86400 + ) -> URL { let headers = HTTPHeaders([("host", url.host ?? "")]) // Create signing data - var signingData = AWSSigner.SigningData(url: url, method: method, headers: headers, body: body, date: AWSSigner.timestamp(date), signer: self) - // Construct query string. Start with original query strings and append all the signing info. + var signingData = AWSSigner.SigningData( + url: url, + method: method, + headers: headers, + body: body, + date: AWSSigner.timestamp(date), + signer: self + ) + // Construct query string. + // Start with original query strings and append all the signing info. var query = url.query ?? "" if query.count > 0 { query += "&" @@ -111,29 +142,39 @@ public struct AWSSigner { .joined(separator: "&") .queryEncode() - // update unsignedURL in the signingData so when the canonical request is constructed it includes all the signing query items - signingData.unsignedURL = URL(string: url.absoluteString.split(separator: "?")[0]+"?"+query)! // NEED TO DEAL WITH SITUATION WHERE THIS FAILS + // update unsignedURL in the signingData + // so when the canonical request is constructed it includes all the signing query items + signingData.unsignedURL = URL(string: url.absoluteString.split(separator: "?")[0] + "?" + query)! + // FIXME: NEED TO DEAL WITH SITUATION WHERE THIS FAILS query += "&X-Amz-Signature=\(signature(signingData: signingData))" // Add signature to query items and build a new Request - let signedURL = URL(string: url.absoluteString.split(separator: "?")[0]+"?"+query)! + let signedURL = URL(string: url.absoluteString.split(separator: "?")[0] + "?" + query)! return signedURL } /// structure used to store data used throughout the signing process struct SigningData { - let url : URL - let method : HTTPMethod - let hashedPayload : String - let datetime : String + let url: URL + let method: HTTPMethod + let hashedPayload: String + let datetime: String let headersToSign: [String: String] - let signedHeaders : String - var unsignedURL : URL - - var date : String { return String(datetime.prefix(8))} - - init(url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: BodyData? = nil, bodyHash: String? = nil, date: String, signer: AWSSigner) { + let signedHeaders: String + var unsignedURL: URL + + var date: String { String(datetime.prefix(8)) } + + init( + url: URL, + method: HTTPMethod = .GET, + headers: HTTPHeaders = HTTPHeaders(), + body: BodyData? = nil, + bodyHash: String? = nil, + date: String, + signer: AWSSigner + ) { if url.path == "" { //URL has to have trailing slash self.url = url.appendingPathComponent("/") @@ -169,16 +210,20 @@ public struct AWSSigner { } } - // Stage 3 Calculating signature as in https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html + // Stage 3 Calculating signature as in + // https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html func signature(signingData: SigningData) -> String { var signature = "" do { - let kDate = try HMAC.authenticate(for: Data(signingData.date.utf8), using: "AWS4\(credentials.secretAccessKey)".array) + let kDate = try HMAC.authenticate( + for: Data(signingData.date.utf8), + using: "AWS4\(credentials.secretAccessKey)".array + ) let kRegion = try HMAC.authenticate(for: Data(region.utf8), using: kDate) let kService = try HMAC.authenticate(for: Data(name.utf8), using: kRegion) let kSigning = try HMAC.authenticate(for: Data("aws4_request".utf8), using: kService) - let kSignature = try HMAC.authenticate(for: stringToSign(signingData: signingData), using: kSigning) + let kSignature = try HMAC.authenticate(for: stringToSign(signingData: signingData), using: kSigning) signature = kSignature.toHexString() } catch { @@ -188,43 +233,45 @@ public struct AWSSigner { return signature // original code from Adam Fowler, using swift-crypto package: - /* - let kDate = HMAC.authenticationCode(for: Data(signingData.date.utf8), using: SymmetricKey(data: Array("AWS4\(credentials.secretAccessKey)".utf8))) - let kRegion = HMAC.authenticationCode(for: Data(region.utf8), using: SymmetricKey(data: kDate)) - let kService = HMAC.authenticationCode(for: Data(name.utf8), using: SymmetricKey(data: kRegion)) - let kSigning = HMAC.authenticationCode(for: Data("aws4_request".utf8), using: SymmetricKey(data: kService)) - let kSignature = HMAC.authenticationCode(for: stringToSign(signingData: signingData), using: SymmetricKey(data: kSigning)) - return kSignature.hexDigest() - */ + + // let kDate = HMAC.authenticationCode(for: Data(signingData.date.utf8), using: SymmetricKey(data: Array("AWS4\(credentials.secretAccessKey)".utf8))) + // let kRegion = HMAC.authenticationCode(for: Data(region.utf8), using: SymmetricKey(data: kDate)) + // let kService = HMAC.authenticationCode(for: Data(name.utf8), using: SymmetricKey(data: kRegion)) + // let kSigning = HMAC.authenticationCode(for: Data("aws4_request".utf8), using: SymmetricKey(data: kService)) + // let kSignature = HMAC.authenticationCode(for: stringToSign(signingData: signingData), using: SymmetricKey(data: kSigning)) + // return kSignature.hexDigest() + } - /// Stage 2 Create the string to sign as in https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html + /// Stage 2 Create the string to sign as in + // https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html func stringToSign(signingData: SigningData) -> Data { - let stringToSign = "AWS4-HMAC-SHA256\n" + - "\(signingData.datetime)\n" + - "\(signingData.date)/\(region)/\(name)/aws4_request\n" + - SHA256.hash(data: canonicalRequest(signingData: signingData)).hexDigest() + let stringToSign = + "AWS4-HMAC-SHA256\n" + "\(signingData.datetime)\n" + "\(signingData.date)/\(region)/\(name)/aws4_request\n" + + SHA256.hash(data: canonicalRequest(signingData: signingData)).hexDigest() return Data(stringToSign.utf8) } - /// Stage 1 Create the canonical request as in https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html + /// Stage 1 Create the canonical request as in + // https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html func canonicalRequest(signingData: SigningData) -> Data { - let canonicalHeaders = signingData.headersToSign.map { return "\($0.key.lowercased()):\($0.value.trimmingCharacters(in: CharacterSet.whitespaces))" } - .sorted() - .joined(separator: "\n") - let canonicalRequest = "\(signingData.method.rawValue)\n" + - "\(signingData.unsignedURL.path.uriEncodeWithSlash())\n" + - "\(signingData.unsignedURL.query ?? "")\n" + // should really uriEncode all the query string values - "\(canonicalHeaders)\n\n" + - "\(signingData.signedHeaders)\n" + - signingData.hashedPayload + let canonicalHeaders = signingData.headersToSign.map { + "\($0.key.lowercased()):\($0.value.trimmingCharacters(in: CharacterSet.whitespaces))" + } + .sorted() + .joined(separator: "\n") + let canonicalRequest = + "\(signingData.method.rawValue)\n" + "\(signingData.unsignedURL.path.uriEncodeWithSlash())\n" + // should really uriEncode all the query string values + + "\(signingData.unsignedURL.query ?? "")\n" + + "\(canonicalHeaders)\n\n" + "\(signingData.signedHeaders)\n" + signingData.hashedPayload return Data(canonicalRequest.utf8) } /// Create a SHA256 hash of the Requests body static func hashedPayload(_ payload: BodyData?) -> String { guard let payload = payload else { return hashedEmptyBody } - let hash : String? + let hash: String? switch payload { case .string(let string): hash = SHA256.hash(data: Data(string.utf8)).hexDigest() @@ -233,7 +280,7 @@ public struct AWSSigner { case .byteBuffer(let byteBuffer): let byteBufferView = byteBuffer.readableBytesView hash = byteBufferView.withContiguousStorageIfAvailable { bytes in - return SHA256.hash(data: bytes).hexDigest() + SHA256.hash(data: bytes).hexDigest() } } if let hash = hash { @@ -245,7 +292,7 @@ public struct AWSSigner { /// return a hexEncoded string buffer from an array of bytes static func hexEncoded(_ buffer: [UInt8]) -> String { - return buffer.map{String(format: "%02x", $0)}.joined(separator: "") + buffer.map { String(format: "%02x", $0) }.joined(separator: "") } /// create timestamp dateformatter static private func createTimeStampDateFormatter() -> DateFormatter { @@ -258,31 +305,35 @@ public struct AWSSigner { /// return a timestamp formatted for signing requests static func timestamp(_ date: Date) -> String { - return timeStampDateFormatter.string(from: date) + timeStampDateFormatter.string(from: date) } } extension String { func queryEncode() -> String { - return addingPercentEncoding(withAllowedCharacters: String.queryAllowedCharacters) ?? self + addingPercentEncoding(withAllowedCharacters: String.queryAllowedCharacters) ?? self } func uriEncode() -> String { - return addingPercentEncoding(withAllowedCharacters: String.uriAllowedCharacters) ?? self + addingPercentEncoding(withAllowedCharacters: String.uriAllowedCharacters) ?? self } func uriEncodeWithSlash() -> String { - return addingPercentEncoding(withAllowedCharacters: String.uriAllowedWithSlashCharacters) ?? self + addingPercentEncoding(withAllowedCharacters: String.uriAllowedWithSlashCharacters) ?? self } - static let uriAllowedWithSlashCharacters = CharacterSet(charactersIn:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~/") - static let uriAllowedCharacters = CharacterSet(charactersIn:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~") - static let queryAllowedCharacters = CharacterSet(charactersIn:"/;+").inverted + static let uriAllowedWithSlashCharacters = CharacterSet( + charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~/" + ) + static let uriAllowedCharacters = CharacterSet( + charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" + ) + static let queryAllowedCharacters = CharacterSet(charactersIn: "/;+").inverted } -public extension Sequence where Element == UInt8 { +extension Sequence where Element == UInt8 { /// return a hexEncoded string buffer from an array of bytes - func hexDigest() -> String { - return self.map{String(format: "%02x", $0)}.joined(separator: "") + public func hexDigest() -> String { + self.map { String(format: "%02x", $0) }.joined(separator: "") } } diff --git a/Sources/AWSLambdaPluginHelper/main.swift b/Sources/AWSLambdaPluginHelper/main.swift index c28454c65..3ed1e5b5b 100644 --- a/Sources/AWSLambdaPluginHelper/main.swift +++ b/Sources/AWSLambdaPluginHelper/main.swift @@ -1 +1 @@ -print("Deployer") \ No newline at end of file +print("Deployer") diff --git a/Tests/AWSLambdaPluginHelperTests/AWSSignerTests.swift b/Tests/AWSLambdaPluginHelperTests/AWSSignerTests.swift index 4220e2355..4cf5727dd 100644 --- a/Tests/AWSLambdaPluginHelperTests/AWSSignerTests.swift +++ b/Tests/AWSLambdaPluginHelperTests/AWSSignerTests.swift @@ -25,33 +25,65 @@ import Foundation @Suite struct SignerTests { - let credentials : Credential = StaticCredential(accessKeyId: "MYACCESSKEY", secretAccessKey: "MYSECRETACCESSKEY") + let credentials: Credential = StaticCredential(accessKeyId: "MYACCESSKEY", secretAccessKey: "MYSECRETACCESSKEY") - @Test + @Test func testSignGetHeaders() { - let signer = AWSSigner(credentials: credentials, name: "glacier", region:"us-east-1") - let headers = signer.signHeaders(url: URL(string:"https://glacier.us-east-1.amazonaws.com/-/vaults")!, method: .GET, headers: ["x-amz-glacier-version":"2012-06-01"], date: Date(timeIntervalSinceReferenceDate: 2000000)) - #expect(headers["Authorization"].first == "AWS4-HMAC-SHA256 Credential=MYACCESSKEY/20010124/us-east-1/glacier/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-glacier-version, Signature=acfa9b03fca6b098d7b88bfd9bbdb4687f5b34e944a9c6ed9f4814c1b0b06d62") + let signer = AWSSigner(credentials: credentials, name: "glacier", region: "us-east-1") + let headers = signer.signHeaders( + url: URL(string: "https://glacier.us-east-1.amazonaws.com/-/vaults")!, + method: .GET, + headers: ["x-amz-glacier-version": "2012-06-01"], + date: Date(timeIntervalSinceReferenceDate: 2_000_000) + ) + #expect( + headers["Authorization"].first + == "AWS4-HMAC-SHA256 Credential=MYACCESSKEY/20010124/us-east-1/glacier/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-glacier-version, Signature=acfa9b03fca6b098d7b88bfd9bbdb4687f5b34e944a9c6ed9f4814c1b0b06d62" + ) } - - @Test + + @Test func testSignPutHeaders() { - let signer = AWSSigner(credentials: credentials, name: "sns", region:"eu-west-1") - let headers = signer.signHeaders(url: URL(string: "https://sns.eu-west-1.amazonaws.com/")!, method: .POST, headers: ["Content-Type": "application/x-www-form-urlencoded; charset=utf-8"], body: .string("Action=ListTopics&Version=2010-03-31"), date: Date(timeIntervalSinceReferenceDate: 200)) - #expect(headers["Authorization"].first == "AWS4-HMAC-SHA256 Credential=MYACCESSKEY/20010101/eu-west-1/sns/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=1d29943055a8ad094239e8de06082100f2426ebbb2c6a5bbcbb04c63e6a3f274") + let signer = AWSSigner(credentials: credentials, name: "sns", region: "eu-west-1") + let headers = signer.signHeaders( + url: URL(string: "https://sns.eu-west-1.amazonaws.com/")!, + method: .POST, + headers: ["Content-Type": "application/x-www-form-urlencoded; charset=utf-8"], + body: .string("Action=ListTopics&Version=2010-03-31"), + date: Date(timeIntervalSinceReferenceDate: 200) + ) + #expect( + headers["Authorization"].first + == "AWS4-HMAC-SHA256 Credential=MYACCESSKEY/20010101/eu-west-1/sns/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=1d29943055a8ad094239e8de06082100f2426ebbb2c6a5bbcbb04c63e6a3f274" + ) } - - @Test + + @Test func testSignS3GetURL() { - let signer = AWSSigner(credentials: credentials, name: "s3", region:"us-east-1") - let url = signer.signURL(url: URL(string: "https://s3.us-east-1.amazonaws.com/")!, method: .GET, date:Date(timeIntervalSinceReferenceDate: 100000)) - #expect(url.absoluteString == "https://s3.us-east-1.amazonaws.com/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=MYACCESSKEY%2F20010102%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20010102T034640Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=27957103c8bfdff3560372b1d85976ed29c944f34295eca2d4fdac7fc02c375a") + let signer = AWSSigner(credentials: credentials, name: "s3", region: "us-east-1") + let url = signer.signURL( + url: URL(string: "https://s3.us-east-1.amazonaws.com/")!, + method: .GET, + date: Date(timeIntervalSinceReferenceDate: 100000) + ) + #expect( + url.absoluteString + == "https://s3.us-east-1.amazonaws.com/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=MYACCESSKEY%2F20010102%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20010102T034640Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=27957103c8bfdff3560372b1d85976ed29c944f34295eca2d4fdac7fc02c375a" + ) } - - @Test + + @Test func testSignS3PutURL() { - let signer = AWSSigner(credentials: credentials, name: "s3", region:"eu-west-1") - let url = signer.signURL(url: URL(string: "https://test-bucket.s3.amazonaws.com/test-put.txt")!, method: .PUT, body: .string("Testing signed URLs"), date:Date(timeIntervalSinceReferenceDate: 100000)) - #expect(url.absoluteString == "https://test-bucket.s3.amazonaws.com/test-put.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=MYACCESSKEY%2F20010102%2Feu-west-1%2Fs3%2Faws4_request&X-Amz-Date=20010102T034640Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=13d665549a6ea5eb6a1615ede83440eaed3e0ee25c964e62d188c896d916d96f") + let signer = AWSSigner(credentials: credentials, name: "s3", region: "eu-west-1") + let url = signer.signURL( + url: URL(string: "https://test-bucket.s3.amazonaws.com/test-put.txt")!, + method: .PUT, + body: .string("Testing signed URLs"), + date: Date(timeIntervalSinceReferenceDate: 100000) + ) + #expect( + url.absoluteString + == "https://test-bucket.s3.amazonaws.com/test-put.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=MYACCESSKEY%2F20010102%2Feu-west-1%2Fs3%2Faws4_request&X-Amz-Date=20010102T034640Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=13d665549a6ea5eb6a1615ede83440eaed3e0ee25c964e62d188c896d916d96f" + ) } -} \ No newline at end of file +} diff --git a/Tests/AWSLambdaPluginHelperTests/CryptoTests.swift b/Tests/AWSLambdaPluginHelperTests/CryptoTests.swift index 3e88285a9..7a65e3048 100644 --- a/Tests/AWSLambdaPluginHelperTests/CryptoTests.swift +++ b/Tests/AWSLambdaPluginHelperTests/CryptoTests.swift @@ -26,53 +26,53 @@ import Foundation @Suite struct CryptoTests { - @Test - func testSHA256() { - - // given - let input = "hello world" - let expected = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" - - // when - let result = Digest.sha256(input.array).toHexString() - - // then - #expect(result == expected) - } - - @Test - func testHMAC() throws { - - // given - let input = "hello world" - let secret = "secretkey" - let expected = "ae6cd2605d622316564d1f76bfc0c04f89d9fafb14f45b3e18c2a3e28bdef29d" - - // when - let authenticator = HMAC(key: secret.array, variant: .sha2(.sha256)) - - #expect(throws: Never.self) { - let result = try authenticator.authenticate(input.array).toHexString() - // then - #expect(result == expected) - } - - } - - @Test - func testHMACExtension() throws { - - // given - let input = "hello world" - let secret = "secretkey" - let expected = "ae6cd2605d622316564d1f76bfc0c04f89d9fafb14f45b3e18c2a3e28bdef29d" - - // when - let result = try HMAC.authenticate(for: input.array, using: secret.array).toHexString() - - // then - #expect(result == expected) - - } - -} \ No newline at end of file + @Test + func testSHA256() { + + // given + let input = "hello world" + let expected = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" + + // when + let result = Digest.sha256(input.array).toHexString() + + // then + #expect(result == expected) + } + + @Test + func testHMAC() throws { + + // given + let input = "hello world" + let secret = "secretkey" + let expected = "ae6cd2605d622316564d1f76bfc0c04f89d9fafb14f45b3e18c2a3e28bdef29d" + + // when + let authenticator = HMAC(key: secret.array, variant: .sha2(.sha256)) + + #expect(throws: Never.self) { + let result = try authenticator.authenticate(input.array).toHexString() + // then + #expect(result == expected) + } + + } + + @Test + func testHMACExtension() throws { + + // given + let input = "hello world" + let secret = "secretkey" + let expected = "ae6cd2605d622316564d1f76bfc0c04f89d9fafb14f45b3e18c2a3e28bdef29d" + + // when + let result = try HMAC.authenticate(for: input.array, using: secret.array).toHexString() + + // then + #expect(result == expected) + + } + +} From 2bdd645a4b542eb0dbf00ce1c85ab67cda354a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sun, 17 Nov 2024 22:06:26 +0100 Subject: [PATCH 06/27] WIP : migrate archive plugin to new lambda-build --- Package.swift | 57 +- Plugins/AWSLambdaBuilder/Plugin.swift | 179 ++++++ Plugins/AWSLambdaDeployer/Plugin.swift | 68 +-- Plugins/AWSLambdaDeployer/PluginUtils.swift | 156 ------ Plugins/AWSLambdaInitializer/Plugin.swift | 86 +-- Plugins/AWSLambdaPackager/Plugin.swift | 513 ------------------ .../AWSLambdaPluginHelper.swift | 57 ++ .../AWSLambdaPluginHelper/Process.swift | 1 - .../Vendored/crypto/Padding.swift | 27 +- .../Vendored/signer/AWSSigner.swift | 24 +- .../Vendored/spm/ArgumentExtractor.swift | 92 ++++ .../lambda-build/Builder.swift | 423 +++++++++++++++ .../lambda-deploy/Deployer.swift | 76 +++ .../lambda-init/Initializer.swift | 97 ++++ .../lambda-init}/Template.swift | 2 - Sources/AWSLambdaPluginHelper/main.swift | 1 - 16 files changed, 1031 insertions(+), 828 deletions(-) create mode 100644 Plugins/AWSLambdaBuilder/Plugin.swift delete mode 100644 Plugins/AWSLambdaDeployer/PluginUtils.swift delete mode 100644 Plugins/AWSLambdaPackager/Plugin.swift create mode 100644 Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift rename Plugins/AWSLambdaPackager/PluginUtils.swift => Sources/AWSLambdaPluginHelper/Process.swift (99%) create mode 100644 Sources/AWSLambdaPluginHelper/Vendored/spm/ArgumentExtractor.swift create mode 100644 Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift create mode 100644 Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift create mode 100644 Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift rename {Plugins/AWSLambdaInitializer => Sources/AWSLambdaPluginHelper/lambda-init}/Template.swift (98%) delete mode 100644 Sources/AWSLambdaPluginHelper/main.swift diff --git a/Package.swift b/Package.swift index 5f6f2d824..280977d47 100644 --- a/Package.swift +++ b/Package.swift @@ -12,32 +12,44 @@ let package = Package( name: "swift-aws-lambda-runtime", platforms: platforms, products: [ + + /* + The runtime library targets + */ + // this library exports `AWSLambdaRuntimeCore` and adds Foundation convenience methods .library(name: "AWSLambdaRuntime", targets: ["AWSLambdaRuntime"]), // this has all the main functionality for lambda and it does not link Foundation .library(name: "AWSLambdaRuntimeCore", targets: ["AWSLambdaRuntimeCore"]), + /* + The plugins + 'lambda-init' creates a new Lambda function + '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 - // requires Linux or at least macOS v15 - .plugin(name: "AWSLambdaPackager", targets: ["AWSLambdaPackager"]), + .plugin(name: "AWSLambdaBuilder", targets: ["AWSLambdaBuilder"]), // plugin to deploy a Lambda function .plugin(name: "AWSLambdaDeployer", targets: ["AWSLambdaDeployer"]), - // an executable that implements the business logic for the plugins - .executable(name: "AWSLambdaPluginHelper", targets: ["AWSLambdaPluginHelper"]), - + /* + Testing targets + */ // for testing only .library(name: "AWSLambdaTesting", targets: ["AWSLambdaTesting"]), ], dependencies: [ .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"), - // .package(url: "https://github.com/apple/swift-crypto.git", from: "3.9.1"), ], targets: [ .target( @@ -69,10 +81,32 @@ let package = Package( 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: "AWSLambdaPackager", + name: "AWSLambdaBuilder", capability: .command( intent: .custom( verb: "lambda-build", @@ -85,13 +119,16 @@ let package = Package( reason: "This plugin uses Docker to create the AWS Lambda ZIP package." ) ] - ) + ), + dependencies: [ + .target(name: "AWSLambdaPluginHelper") + ] ), .plugin( name: "AWSLambdaDeployer", capability: .command( intent: .custom( - verb: "deploy", + verb: "lambda-deploy", description: "Deploy the Lambda function. You must have an AWS account and know an access key and secret access key." ), diff --git a/Plugins/AWSLambdaBuilder/Plugin.swift b/Plugins/AWSLambdaBuilder/Plugin.swift new file mode 100644 index 000000000..8dd8a81ca --- /dev/null +++ b/Plugins/AWSLambdaBuilder/Plugin.swift @@ -0,0 +1,179 @@ +//===----------------------------------------------------------------------===// +// +// 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 PackagePlugin + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@main +struct AWSLambdaPackager: CommandPlugin { + + func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws { + + // values to pass to the AWSLambdaPluginHelper + let outputDirectory: URL + let products: [Product] + let buildConfiguration: PackageManager.BuildConfiguration + let packageID: String = context.package.id + let packageDisplayName = context.package.displayName + let packageDirectory = context.package.directoryURL + let dockerToolPath = try context.tool(named: "docker").url + let zipToolPath = try context.tool(named: "zip").url + + // extract arguments that require PluginContext to fully resolve + // resolve them here and pass them to the AWSLambdaPluginHelper as arguments + var argumentExtractor = ArgumentExtractor(arguments) + + let outputPathArgument = argumentExtractor.extractOption(named: "output-path") + let productsArgument = argumentExtractor.extractOption(named: "products") + let configurationArgument = argumentExtractor.extractOption(named: "configuration") + + if let outputPath = outputPathArgument.first { + #if os(Linux) + var isDirectory: Bool = false + #else + var isDirectory: ObjCBool = false + #endif + guard FileManager.default.fileExists(atPath: outputPath, isDirectory: &isDirectory) + else { + throw BuilderErrors.invalidArgument("invalid output directory '\(outputPath)'") + } + outputDirectory = URL(string: outputPath)! + } else { + outputDirectory = context.pluginWorkDirectoryURL.appending(path: "\(AWSLambdaPackager.self)") + } + + let explicitProducts = !productsArgument.isEmpty + if explicitProducts { + let _products = try context.package.products(named: productsArgument) + for product in _products { + guard product is ExecutableProduct else { + throw BuilderErrors.invalidArgument("product named '\(product.name)' is not an executable product") + } + } + products = _products + + } else { + products = context.package.products.filter { $0 is ExecutableProduct } + } + + if let _buildConfigurationName = configurationArgument.first { + guard let _buildConfiguration = PackageManager.BuildConfiguration(rawValue: _buildConfigurationName) else { + throw BuilderErrors.invalidArgument("invalid build configuration named '\(_buildConfigurationName)'") + } + buildConfiguration = _buildConfiguration + } else { + buildConfiguration = .release + } + + // TODO: When running on Amazon Linux 2, we have to build directly from the plugin + // launch the build, then call the helper just for the ZIP part + + let tool = try context.tool(named: "AWSLambdaPluginHelper") + let args = [ + "build", + "--output-path", outputDirectory.path(), + "--products", products.map { $0.name }.joined(separator: ","), + "--configuration", buildConfiguration.rawValue, + "--package-id", packageID, + "--package-display-name", packageDisplayName, + "--package-directory", packageDirectory.path(), + "--docker-tool-path", dockerToolPath.path, + "--zip-tool-path", zipToolPath.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)") + } + } + + // TODO: When running on Amazon Linux 2, we have to build directly from the plugin +// private func build( +// packageIdentity: Package.ID, +// products: [Product], +// buildConfiguration: PackageManager.BuildConfiguration, +// verboseLogging: Bool +// ) throws -> [LambdaProduct: URL] { +// print("-------------------------------------------------------------------------") +// print("building \"\(packageIdentity)\"") +// print("-------------------------------------------------------------------------") +// +// var results = [LambdaProduct: URL]() +// for product in products { +// print("building \"\(product.name)\"") +// var parameters = PackageManager.BuildParameters() +// parameters.configuration = buildConfiguration +// parameters.otherSwiftcFlags = ["-static-stdlib"] +// parameters.logging = verboseLogging ? .verbose : .concise +// +// let result = try packageManager.build( +// .product(product.name), +// parameters: parameters +// ) +// guard let artifact = result.executableArtifact(for: product) else { +// throw Errors.productExecutableNotFound(product.name) +// } +// results[.init(product)] = artifact.url +// } +// return results +// } + +// private func isAmazonLinux2() -> Bool { +// 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 enum BuilderErrors: Error, CustomStringConvertible { + case invalidArgument(String) + + var description: String { + switch self { + case .invalidArgument(let description): + return description + } + } +} + +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.url.lastPathComponent == product.name + } + guard !executables.isEmpty else { + return nil + } + guard executables.count == 1, let executable = executables.first else { + return nil + } + return executable + } +} diff --git a/Plugins/AWSLambdaDeployer/Plugin.swift b/Plugins/AWSLambdaDeployer/Plugin.swift index 08cdc2696..9ae59b0b3 100644 --- a/Plugins/AWSLambdaDeployer/Plugin.swift +++ b/Plugins/AWSLambdaDeployer/Plugin.swift @@ -12,67 +12,33 @@ // //===----------------------------------------------------------------------===// -import Foundation import PackagePlugin +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + @main -@available(macOS 15.0, *) struct AWSLambdaDeployer: CommandPlugin { func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws { - let configuration = try Configuration(context: context, arguments: arguments) - - if configuration.help { - self.displayHelpMessage() - return - } - let tool = try context.tool(named: "AWSLambdaDeployerHelper") - try Utils.execute(executable: tool.url, arguments: [], logLevel: .debug) - } + let tool = try context.tool(named: "AWSLambdaPluginHelper") - private func displayHelpMessage() { - print( - """ - OVERVIEW: A SwiftPM plugin to deploy a Lambda function. + let args = ["deploy"] + arguments - USAGE: swift package lambda-deploy - [--with-url] - [--help] [--verbose] + // 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() - OPTIONS: - --with-url Add an URL to access the Lambda function - --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) + // 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)") } - """ } + } diff --git a/Plugins/AWSLambdaDeployer/PluginUtils.swift b/Plugins/AWSLambdaDeployer/PluginUtils.swift deleted file mode 100644 index 52d1b2beb..000000000 --- a/Plugins/AWSLambdaDeployer/PluginUtils.swift +++ /dev/null @@ -1,156 +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 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: " "))") - } - - 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 workingDirectory = customWorkingDirectory { - process.currentDirectoryURL = URL(fileURLWithPath: workingDirectory.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/AWSLambdaInitializer/Plugin.swift b/Plugins/AWSLambdaInitializer/Plugin.swift index 21713c0f1..bbfa90e27 100644 --- a/Plugins/AWSLambdaInitializer/Plugin.swift +++ b/Plugins/AWSLambdaInitializer/Plugin.swift @@ -12,80 +12,32 @@ // //===----------------------------------------------------------------------===// -import Foundation import PackagePlugin +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + @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 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)") } - - 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/AWSLambdaPackager/Plugin.swift b/Plugins/AWSLambdaPackager/Plugin.swift deleted file mode 100644 index a86939458..000000000 --- a/Plugins/AWSLambdaPackager/Plugin.swift +++ /dev/null @@ -1,513 +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 Foundation -import PackagePlugin - -@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: "', '"))'" - ) - } - - let builtProducts: [LambdaProduct: URL] - if self.isAmazonLinux2() { - // build directly on the machine - builtProducts = try self.build( - packageIdentity: context.package.id, - products: configuration.products, - buildConfiguration: configuration.buildConfiguration, - verboseLogging: configuration.verboseLogging - ) - } else { - // build with docker - builtProducts = try self.buildInDocker( - packageIdentity: context.package.id, - packageDirectory: context.package.directoryURL, - products: configuration.products, - toolsProvider: { name in try context.tool(named: name).url }, - outputDirectory: configuration.outputDirectory, - baseImage: configuration.baseDockerImage, - disableDockerImageUpdate: configuration.disableDockerImageUpdate, - buildConfiguration: configuration.buildConfiguration, - verboseLogging: configuration.verboseLogging - ) - } - - // create the archive - let archives = try self.package( - packageName: context.package.displayName, - products: builtProducts, - 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" - ) - for (product, archivePath) in archives { - print(" * \(product.name) at \(archivePath.path())") - } - } - - private func buildInDocker( - packageIdentity: Package.ID, - packageDirectory: URL, - products: [Product], - toolsProvider: (String) throws -> URL, - outputDirectory: URL, - baseImage: String, - disableDockerImageUpdate: Bool, - buildConfiguration: PackageManager.BuildConfiguration, - verboseLogging: Bool - ) throws -> [LambdaProduct: URL] { - let dockerToolPath = try toolsProvider("docker") - - print("-------------------------------------------------------------------------") - print("building \"\(packageIdentity)\" in docker") - print("-------------------------------------------------------------------------") - - if !disableDockerImageUpdate { - // update the underlying docker image, if necessary - print("updating \"\(baseImage)\" docker image") - try Utils.execute( - executable: dockerToolPath, - arguments: ["pull", baseImage], - logLevel: verboseLogging ? .debug : .output - ) - } - - // get the build output path - let buildOutputPathCommand = "swift build -c \(buildConfiguration.rawValue) --show-bin-path" - let dockerBuildOutputPath = try Utils.execute( - executable: dockerToolPath, - 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 = URL( - string: buildPathOutput.replacingOccurrences(of: "/workspace/", with: packageDirectory.description) - )! - - // build the products - 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" - 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 - } - return builtProducts - } - - private func build( - packageIdentity: Package.ID, - products: [Product], - buildConfiguration: PackageManager.BuildConfiguration, - verboseLogging: Bool - ) throws -> [LambdaProduct: URL] { - print("-------------------------------------------------------------------------") - print("building \"\(packageIdentity)\"") - print("-------------------------------------------------------------------------") - - var results = [LambdaProduct: URL]() - for product in products { - print("building \"\(product.name)\"") - var parameters = PackageManager.BuildParameters() - parameters.configuration = buildConfiguration - parameters.otherSwiftcFlags = ["-static-stdlib"] - parameters.logging = verboseLogging ? .verbose : .concise - - let result = try packageManager.build( - .product(product.name), - parameters: parameters - ) - guard let artifact = result.executableArtifact(for: product) else { - throw Errors.productExecutableNotFound(product.name) - } - results[.init(product)] = artifact.url - } - return results - } - - // TODO: explore using ziplib or similar instead of shelling out - private func package( - packageName: String, - products: [LambdaProduct: URL], - toolsProvider: (String) throws -> URL, - outputDirectory: URL, - verboseLogging: Bool - ) throws -> [LambdaProduct: URL] { - let zipToolPath = try toolsProvider("zip") - - var archives = [LambdaProduct: URL]() - for (product, artifactPath) in products { - print("-------------------------------------------------------------------------") - print("archiving \"\(product.name)\"") - print("-------------------------------------------------------------------------") - - // prep zipfile location - 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.path(), withIntermediateDirectories: true) - - // rename artifact to "bootstrap" - let relocatedArtifactPath = workingDirectory.appending(path: "bootstrap") - try FileManager.default.copyItem(atPath: artifactPath.path(), toPath: relocatedArtifactPath.path()) - - var arguments: [String] = [] - #if os(macOS) || os(Linux) - arguments = [ - "--recurse-paths", - "--symlinks", - zipfilePath.lastPathComponent, - relocatedArtifactPath.lastPathComponent, - ] - #else - 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()) { - try FileManager.default.copyItem( - atPath: artifactURL.path(), - toPath: relocatedResourcesDirectory.path() - ) - arguments.append(resourcesDirectoryName) - } - } - - // run the zip tool - try Utils.execute( - executable: zipToolPath, - arguments: arguments, - customWorkingDirectory: workingDirectory, - logLevel: verboseLogging ? .debug : .silent - ) - - archives[product] = zipfilePath - } - return archives - } - - private func isAmazonLinux2() -> Bool { - 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-directory ] - [--products ] - [--configuration debug | release] - [--swift-version ] - [--base-docker-image ] - [--disable-docker-image-update] - - - OPTIONS: - --verbose Produce verbose output for debugging. - --output-directory 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 help: Bool - public let outputDirectory: URL - public let products: [Product] - public let explicitProducts: Bool - public let buildConfiguration: PackageManager.BuildConfiguration - public let verboseLogging: Bool - public let baseDockerImage: String - public let disableDockerImageUpdate: Bool - - public init( - context: PluginContext, - arguments: [String] - ) throws { - var argumentExtractor = ArgumentExtractor(arguments) - let verboseArgument = argumentExtractor.extractFlag(named: "verbose") > 0 - let outputPathArgument = argumentExtractor.extractOption(named: "output-path") - let productsArgument = argumentExtractor.extractOption(named: "products") - let configurationArgument = argumentExtractor.extractOption(named: "configuration") - 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 - #endif - guard FileManager.default.fileExists(atPath: outputPath, isDirectory: &isDirectory) - else { - throw Errors.invalidArgument("invalid output directory '\(outputPath)'") - } - self.outputDirectory = URL(string: outputPath)! - } else { - self.outputDirectory = context.pluginWorkDirectoryURL.appending(path: "\(AWSLambdaPackager.self)") - } - - self.explicitProducts = !productsArgument.isEmpty - if self.explicitProducts { - let products = try context.package.products(named: productsArgument) - for product in products { - guard product is ExecutableProduct else { - throw Errors.invalidArgument("product named '\(product.name)' is not an executable product") - } - } - self.products = products - - } else { - self.products = context.package.products.filter { $0 is ExecutableProduct } - } - - if let buildConfigurationName = configurationArgument.first { - guard let buildConfiguration = PackageManager.BuildConfiguration(rawValue: buildConfigurationName) else { - throw Errors.invalidArgument("invalid build configuration named '\(buildConfigurationName)'") - } - self.buildConfiguration = buildConfiguration - } else { - self.buildConfiguration = .release - } - - guard !(!swiftVersionArgument.isEmpty && !baseDockerImageArgument.isEmpty) else { - 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" - - self.disableDockerImageUpdate = disableDockerImageUpdateArgument - - if self.verboseLogging { - print("-------------------------------------------------------------------------") - print("configuration") - print("-------------------------------------------------------------------------") - print(self) - } - } - - var description: String { - """ - { - outputDirectory: \(self.outputDirectory) - products: \(self.products.map(\.name)) - buildConfiguration: \(self.buildConfiguration) - baseDockerImage: \(self.baseDockerImage) - disableDockerImageUpdate: \(self.disableDockerImageUpdate) - } - """ - } -} - -private 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 - } -} - -private enum Errors: Error, CustomStringConvertible { - case invalidArgument(String) - case unsupportedPlatform(String) - case unknownProduct(String) - case productExecutableNotFound(String) - case failedWritingDockerfile - case failedParsingDockerOutput(String) - case processFailed([String], Int32) - - var description: String { - switch self { - case .invalidArgument(let description): - return description - case .unsupportedPlatform(let description): - return description - case .unknownProduct(let description): - return description - case .productExecutableNotFound(let product): - return "product executable not found '\(product)'" - case .failedWritingDockerfile: - return "failed writing dockerfile" - case .failedParsingDockerOutput(let output): - return "failed parsing docker output: '\(output)'" - case .processFailed(let arguments, let code): - return "\(arguments.joined(separator: " ")) failed with code \(code)" - } - } -} - -private struct LambdaProduct: Hashable { - let underlying: Product - - init(_ underlying: Product) { - self.underlying = underlying - } - - var name: String { - self.underlying.name - } - - func hash(into hasher: inout Hasher) { - self.underlying.id.hash(into: &hasher) - } - - static func == (lhs: Self, rhs: Self) -> Bool { - lhs.underlying.id == rhs.underlying.id - } -} - -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.url.lastPathComponent == product.name - } - guard !executables.isEmpty else { - return nil - } - guard executables.count == 1, let executable = executables.first else { - return nil - } - return executable - } -} diff --git a/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift b/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift new file mode 100644 index 000000000..71b5af7ed --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.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 +// +//===----------------------------------------------------------------------===// + +@main +struct AWSLambdaPluginHelper { + + private enum Command: String { + case `init` + case build + case deploy + } + + 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) + } + } + + private func command(from arguments: [String]) throws -> Command { + let args = CommandLine.arguments + + guard args.count > 2 else { + throw AWSLambdaPluginHelperError.noCommand + } + let commandName = args[1] + guard let command = Command(rawValue: commandName) else { + throw AWSLambdaPluginHelperError.invalidCommand(commandName) + } + + return command + } +} + +private enum AWSLambdaPluginHelperError: Error { + case noCommand + case invalidCommand(String) +} + diff --git a/Plugins/AWSLambdaPackager/PluginUtils.swift b/Sources/AWSLambdaPluginHelper/Process.swift similarity index 99% rename from Plugins/AWSLambdaPackager/PluginUtils.swift rename to Sources/AWSLambdaPluginHelper/Process.swift index f4e8cb023..235ae0566 100644 --- a/Plugins/AWSLambdaPackager/PluginUtils.swift +++ b/Sources/AWSLambdaPluginHelper/Process.swift @@ -14,7 +14,6 @@ import Dispatch import Foundation -import PackagePlugin import Synchronization @available(macOS 15.0, *) diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift index be90ba373..679e161f6 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift @@ -33,26 +33,13 @@ public protocol PaddingProtocol { } public enum Padding: PaddingProtocol { - case noPadding, zeroPadding //, pkcs7, pkcs5, eme_pkcs1v15, emsa_pkcs1v15, iso78164, iso10126 - + case noPadding, zeroPadding public func add(to: [UInt8], blockSize: Int) -> [UInt8] { switch self { case .noPadding: return to // NoPadding().add(to: to, blockSize: blockSize) case .zeroPadding: return ZeroPadding().add(to: to, blockSize: blockSize) - // case .pkcs7: - // return PKCS7.Padding().add(to: to, blockSize: blockSize) - // case .pkcs5: - // return PKCS5.Padding().add(to: to, blockSize: blockSize) - // case .eme_pkcs1v15: - // return EMEPKCS1v15Padding().add(to: to, blockSize: blockSize) - // case .emsa_pkcs1v15: - // return EMSAPKCS1v15Padding().add(to: to, blockSize: blockSize) - // case .iso78164: - // return ISO78164Padding().add(to: to, blockSize: blockSize) - // case .iso10126: - // return ISO10126Padding().add(to: to, blockSize: blockSize) } } @@ -62,18 +49,6 @@ public enum Padding: PaddingProtocol { return from //NoPadding().remove(from: from, blockSize: blockSize) case .zeroPadding: return ZeroPadding().remove(from: from, blockSize: blockSize) - // case .pkcs7: - // return PKCS7.Padding().remove(from: from, blockSize: blockSize) - // case .pkcs5: - // return PKCS5.Padding().remove(from: from, blockSize: blockSize) - // case .eme_pkcs1v15: - // return EMEPKCS1v15Padding().remove(from: from, blockSize: blockSize) - // case .emsa_pkcs1v15: - // return EMSAPKCS1v15Padding().remove(from: from, blockSize: blockSize) - // case .iso78164: - // return ISO78164Padding().remove(from: from, blockSize: blockSize) - // case .iso10126: - // return ISO10126Padding().remove(from: from, blockSize: blockSize) } } } diff --git a/Sources/AWSLambdaPluginHelper/Vendored/signer/AWSSigner.swift b/Sources/AWSLambdaPluginHelper/Vendored/signer/AWSSigner.swift index e06d5b305..7196982ef 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/signer/AWSSigner.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/signer/AWSSigner.swift @@ -22,7 +22,6 @@ // https://docs.aws.amazon.com/general/latest/gr/signing_aws_api_requests.html // -import Crypto import NIO import NIOHTTP1 @@ -34,6 +33,29 @@ import struct Foundation.Locale import struct Foundation.TimeZone import struct Foundation.URL +// adapter for the vendored SHA256 hashing function +private struct SHA256 { + static func hash(data: [UInt8]) -> [UInt8] { + Digest.sha256(data) + } + + static func hash(data: Data) -> [UInt8] { + SHA256.hash(data: data.bytes) + } + + static func hash(data: UnsafeBufferPointer) -> [UInt8] { + SHA256.hash(data: [UInt8](data)) + } +} + +// adapter for the vendored hexDigest function +extension Array where Element == UInt8 { + fileprivate func hexDigest() -> String { + // call the hexEncoded function from the Array+Extensions.swift file + self.toHexString() + } +} + /// Amazon Web Services V4 Signer public struct AWSSigner { /// security credentials for accessing AWS services diff --git a/Sources/AWSLambdaPluginHelper/Vendored/spm/ArgumentExtractor.swift b/Sources/AWSLambdaPluginHelper/Vendored/spm/ArgumentExtractor.swift new file mode 100644 index 000000000..8406692c7 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Vendored/spm/ArgumentExtractor.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// A rudimentary helper for extracting options and flags from a string list representing command line arguments. The idea is to extract all known options and flags, leaving just the positional arguments. This does not handle well the case in which positional arguments (or option argument values) happen to have the same name as an option or a flag. It only handles the long `--` form of options, but it does respect `--` as an indication that all remaining arguments are positional. +public struct ArgumentExtractor { + private var args: [String] + private let literals: [String] + + /// Initializes a ArgumentExtractor with a list of strings from which to extract flags and options. If the list contains `--`, any arguments that follow it are considered to be literals. + public init(_ arguments: [String]) { + // Split the array on the first `--`, if there is one. Everything after that is a literal. + let parts = arguments.split(separator: "--", maxSplits: 1, omittingEmptySubsequences: false) + self.args = Array(parts[0]) + self.literals = Array(parts.count == 2 ? parts[1] : []) + } + + /// Extracts options of the form `-- ` or `--=` from the remaining arguments, and returns the extracted values. + public mutating func extractOption(named name: String) -> [String] { + var values: [String] = [] + var idx = 0 + while idx < args.count { + var arg = args[idx] + if arg == "--\(name)" { + args.remove(at: idx) + if idx < args.count { + let val = args[idx] + values.append(val) + args.remove(at: idx) + } + } + else if arg.starts(with: "--\(name)=") { + arg.removeFirst(2 + name.count + 1) + values.append(arg) + args.remove(at: idx) + } + else { + idx += 1 + } + } + return values + } + + /// Extracts flags of the form `--` from the remaining arguments, and returns the count. + public mutating func extractFlag(named name: String) -> Int { + var count = 0 + var idx = 0 + while idx < args.count { + let arg = args[idx] + if arg == "--\(name)" { + args.remove(at: idx) + count += 1 + } + else { + idx += 1 + } + } + return count + } + + /// Returns any unextracted flags or options (based strictly on whether remaining arguments have a "--" prefix). + public var unextractedOptionsOrFlags: [String] { + return args.filter{ $0.hasPrefix("--") } + } + + /// Returns all remaining arguments, including any literals after the first `--` if there is one. + public var remainingArguments: [String] { + return args + literals + } +} \ No newline at end of file diff --git a/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift b/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift new file mode 100644 index 000000000..fb8ff73a6 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift @@ -0,0 +1,423 @@ +//===----------------------------------------------------------------------===// +// +// 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 canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@available(macOS 15.0, *) +struct Builder { + func build(arguments: [String]) async throws { + let configuration = try BuilderConfiguration(arguments: arguments) + + if configuration.help { + self.displayHelpMessage() + return + } + + let builtProducts: [String: URL] + + // build with docker + // TODO: check if dockerToolPath is provided + // When not provided, it means we're building on Amazon Linux 2 + builtProducts = try self.buildInDocker( + packageIdentity: configuration.packageID, + packageDirectory: configuration.packageDirectory, + products: configuration.products, + dockerToolPath: configuration.dockerToolPath, + outputDirectory: configuration.outputDirectory, + baseImage: configuration.baseDockerImage, + disableDockerImageUpdate: configuration.disableDockerImageUpdate, + buildConfiguration: configuration.buildConfiguration, + verboseLogging: configuration.verboseLogging + ) + + // create the archive + let archives = try self.package( + packageName: configuration.packageDisplayName, + products: builtProducts, + zipToolPath: configuration.zipToolPath, + outputDirectory: configuration.outputDirectory, + verboseLogging: configuration.verboseLogging + ) + + print( + "\(archives.count > 0 ? archives.count.description : "no") archive\(archives.count != 1 ? "s" : "") created" + ) + for (product, archivePath) in archives { + print(" * \(product) at \(archivePath.path())") + } + } + + private func buildInDocker( + packageIdentity: String, + packageDirectory: URL, + products: [String], + dockerToolPath: URL, + outputDirectory: URL, + baseImage: String, + disableDockerImageUpdate: Bool, + buildConfiguration: BuildConfiguration, + verboseLogging: Bool + ) throws -> [String: URL] { + + print("-------------------------------------------------------------------------") + print("building \"\(packageIdentity)\" in docker") + print("-------------------------------------------------------------------------") + + if !disableDockerImageUpdate { + // update the underlying docker image, if necessary + print("updating \"\(baseImage)\" docker image") + try Utils.execute( + executable: dockerToolPath, + arguments: ["pull", baseImage], + logLevel: verboseLogging ? .debug : .silent + ) + } + + // get the build output path + let buildOutputPathCommand = "swift build -c \(buildConfiguration.rawValue) --show-bin-path" + let dockerBuildOutputPath = try Utils.execute( + executable: dockerToolPath, + 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 BuilderErrors.failedParsingDockerOutput(dockerBuildOutputPath) + } + let buildOutputPath = URL( + string: buildPathOutput.replacingOccurrences(of: "/workspace/", with: packageDirectory.description) + )! + + // build the products + var builtProducts = [String: URL]() + for product in products { + print("building \"\(product)\"") + let buildCommand = + "swift build -c \(buildConfiguration.rawValue) --product \(product) --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) + + guard FileManager.default.fileExists(atPath: productPath.path()) else { + print("expected '\(product)' binary at \"\(productPath.path())\"") + throw BuilderErrors.productExecutableNotFound(product) + } + builtProducts[.init(product)] = productPath + } + return builtProducts + } + + // TODO: explore using ziplib or similar instead of shelling out + private func package( + packageName: String, + products: [String: URL], + zipToolPath: URL, + outputDirectory: URL, + verboseLogging: Bool + ) throws -> [String: URL] { + + var archives = [String: URL]() + for (product, artifactPath) in products { + print("-------------------------------------------------------------------------") + print("archiving \"\(product)\"") + print("-------------------------------------------------------------------------") + + // prep zipfile location + let workingDirectory = outputDirectory.appending(path: product) + let zipfilePath = workingDirectory.appending(path: "\(product).zip") + if FileManager.default.fileExists(atPath: workingDirectory.path()) { + try FileManager.default.removeItem(atPath: workingDirectory.path()) + } + try FileManager.default.createDirectory(atPath: workingDirectory.path(), withIntermediateDirectories: true) + + // rename artifact to "bootstrap" + let relocatedArtifactPath = workingDirectory.appending(path: "bootstrap") + try FileManager.default.copyItem(atPath: artifactPath.path(), toPath: relocatedArtifactPath.path()) + + var arguments: [String] = [] +#if os(macOS) || os(Linux) + arguments = [ + "--recurse-paths", + "--symlinks", + zipfilePath.lastPathComponent, + relocatedArtifactPath.lastPathComponent, + ] +#else + 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()) { + try FileManager.default.copyItem( + atPath: artifactURL.path(), + toPath: relocatedResourcesDirectory.path() + ) + arguments.append(resourcesDirectoryName) + } + } + + // run the zip tool + try Utils.execute( + executable: zipToolPath, + arguments: arguments, + customWorkingDirectory: workingDirectory, + logLevel: verboseLogging ? .debug : .silent + ) + + archives[product] = zipfilePath + } + return archives + } + + 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-directory ] + [--products ] + [--configuration debug | release] + [--swift-version ] + [--base-docker-image ] + [--disable-docker-image-update] + + + OPTIONS: + --verbose Produce verbose output for debugging. + --output-directory 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. + """ + ) + } +} + +private struct BuilderConfiguration: CustomStringConvertible { + + // passed by the user + public let help: Bool + public let outputDirectory: URL + public let products: [String] + public let buildConfiguration: BuildConfiguration + public let verboseLogging: Bool + public let baseDockerImage: String + public let disableDockerImageUpdate: Bool + + // passed by the plugin + public let packageID: String + public let packageDisplayName: String + public let packageDirectory: URL + public let dockerToolPath: URL + public let zipToolPath: URL + + public init(arguments: [String]) throws { + var argumentExtractor = ArgumentExtractor(arguments) + + let verboseArgument = argumentExtractor.extractFlag(named: "verbose") > 0 + let outputPathArgument = argumentExtractor.extractOption(named: "output-path") + let packageIDArgument = argumentExtractor.extractOption(named: "package-id") + let packageDisplayNameArgument = argumentExtractor.extractOption(named: "package-display-name") + let packageDirectoryArgument = argumentExtractor.extractOption(named: "package-directory") + let dockerToolPathArgument = argumentExtractor.extractOption(named: "docker-tool-path") + let zipToolPathArgument = argumentExtractor.extractOption(named: "zip-tool-path") + let productsArgument = argumentExtractor.extractOption(named: "products") + let configurationArgument = argumentExtractor.extractOption(named: "configuration") + 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 + + // package id + guard !packageIDArgument.isEmpty else { + throw BuilderErrors.invalidArgument("--package-id argument is required") + } + self.packageID = packageIDArgument.first! + + // package display name + guard !packageDisplayNameArgument.isEmpty else { + throw BuilderErrors.invalidArgument("--package-display-name argument is required") + } + self.packageDisplayName = packageDisplayNameArgument.first! + + // package directory + guard !packageDirectoryArgument.isEmpty else { + throw BuilderErrors.invalidArgument("--package-directory argument is required") + } + self.packageDirectory = URL(fileURLWithPath: packageDirectoryArgument.first!) + + // docker tool path + guard !dockerToolPathArgument.isEmpty else { + throw BuilderErrors.invalidArgument("--docker-tool-path argument is required") + } + self.dockerToolPath = URL(fileURLWithPath: dockerToolPathArgument.first!) + + // zip tool path + guard !zipToolPathArgument.isEmpty else { + throw BuilderErrors.invalidArgument("--zip-tool-path argument is required") + } + self.zipToolPath = URL(fileURLWithPath: zipToolPathArgument.first!) + + // output directory + guard !outputPathArgument.isEmpty else { + throw BuilderErrors.invalidArgument("--output-path is required") + } + self.outputDirectory = URL(fileURLWithPath: outputPathArgument.first!) + + // products + guard !productsArgument.isEmpty else { + throw BuilderErrors.invalidArgument("--products argument is required") + } + self.products = productsArgument.flatMap { $0.split(separator: ",").map(String.init) } + + // build configuration + guard let buildConfigurationName = configurationArgument.first else { + throw BuilderErrors.invalidArgument("--configuration argument is equired") + } + guard let _buildConfiguration = BuildConfiguration(rawValue: buildConfigurationName) else { + throw BuilderErrors.invalidArgument("invalid build configuration named '\(buildConfigurationName)'") + } + self.buildConfiguration = _buildConfiguration + + guard !(!swiftVersionArgument.isEmpty && !baseDockerImageArgument.isEmpty) else { + throw BuilderErrors.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" + + self.disableDockerImageUpdate = disableDockerImageUpdateArgument + + if self.verboseLogging { + print("-------------------------------------------------------------------------") + print("configuration") + print("-------------------------------------------------------------------------") + print(self) + } + } + + var description: String { + """ + { + outputDirectory: \(self.outputDirectory) + products: \(self.products) + buildConfiguration: \(self.buildConfiguration) + dockerToolPath: \(self.dockerToolPath) + baseDockerImage: \(self.baseDockerImage) + disableDockerImageUpdate: \(self.disableDockerImageUpdate) + zipToolPath: \(self.zipToolPath) + packageID: \(self.packageID) + packageDisplayName: \(self.packageDisplayName) + packageDirectory: \(self.packageDirectory) + } + """ + } +} + +private enum BuilderErrors: Error, CustomStringConvertible { + case invalidArgument(String) + case unsupportedPlatform(String) + case unknownProduct(String) + case productExecutableNotFound(String) + case failedWritingDockerfile + case failedParsingDockerOutput(String) + case processFailed([String], Int32) + + var description: String { + switch self { + case .invalidArgument(let description): + return description + case .unsupportedPlatform(let description): + return description + case .unknownProduct(let description): + return description + case .productExecutableNotFound(let product): + return "product executable not found '\(product)'" + case .failedWritingDockerfile: + return "failed writing dockerfile" + case .failedParsingDockerOutput(let output): + return "failed parsing docker output: '\(output)'" + case .processFailed(let arguments, let code): + return "\(arguments.joined(separator: " ")) failed with code \(code)" + } + } +} + +private enum BuildConfiguration: String { + case debug + case release +} diff --git a/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift b/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift new file mode 100644 index 000000000..a9e2462f8 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// 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 canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +struct Deployer { + + func deploy(arguments: [String]) async throws { + let configuration = try DeployerConfiguration(arguments: arguments) + + if configuration.help { + self.displayHelpMessage() + return + } + + //FIXME: use Logger + print("TODO: deploy") + } + + private func displayHelpMessage() { + print( + """ + OVERVIEW: A SwiftPM plugin to deploy a Lambda function. + + USAGE: swift package lambda-deploy + [--with-url] + [--help] [--verbose] + + OPTIONS: + --with-url Add an URL to access the Lambda function + --verbose Produce verbose output for debugging. + --help Show help information. + """ + ) + } +} + +private struct DeployerConfiguration: CustomStringConvertible { + public let help: Bool + public let verboseLogging: Bool + + public init(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/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift b/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift new file mode 100644 index 000000000..8515d5867 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift @@ -0,0 +1,97 @@ +//===----------------------------------------------------------------------===// +// +// 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 canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +struct Initializer { + + private let destFileName = "Sources/main.swift" + + func initialize(arguments: [String]) async throws { + + let configuration = try InitializerConfiguration(arguments: arguments) + + if configuration.help { + self.displayHelpMessage() + return + } + + let destFileURL = configuration.destinationDir.appendingPathComponent(destFileName) + do { + try functionWithUrlTemplate.write(to: destFileURL, atomically: true, encoding: .utf8) + + if configuration.verboseLogging { + print("File created at: \(destFileURL)") + } + + print("✅ Lambda function written to \(destFileName)") + print("📦 You can now package with: 'swift package archive'") + } catch { + print("🛑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 InitializerConfiguration: CustomStringConvertible { + public let help: Bool + public let verboseLogging: Bool + public let destinationDir: URL + + public init(arguments: [String]) throws { + var argumentExtractor = ArgumentExtractor(arguments) + let verboseArgument = argumentExtractor.extractFlag(named: "verbose") > 0 + let helpArgument = argumentExtractor.extractFlag(named: "help") > 0 + let destDirArgument = argumentExtractor.extractOption(named: "dest-dir") + + // help required ? + self.help = helpArgument + + // verbose logging required ? + self.verboseLogging = verboseArgument + + // dest dir + self.destinationDir = URL(fileURLWithPath: destDirArgument[0]) + } + + var description: String { + """ + { + verboseLogging: \(self.verboseLogging) + destinationDir: \(self.destinationDir) + } + """ + } +} diff --git a/Plugins/AWSLambdaInitializer/Template.swift b/Sources/AWSLambdaPluginHelper/lambda-init/Template.swift similarity index 98% rename from Plugins/AWSLambdaInitializer/Template.swift rename to Sources/AWSLambdaPluginHelper/lambda-init/Template.swift index 133e50334..0ca37ace7 100644 --- a/Plugins/AWSLambdaInitializer/Template.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-init/Template.swift @@ -12,8 +12,6 @@ // //===----------------------------------------------------------------------===// -import Foundation - let functionWithUrlTemplate = #""" import AWSLambdaRuntime import AWSLambdaEvents diff --git a/Sources/AWSLambdaPluginHelper/main.swift b/Sources/AWSLambdaPluginHelper/main.swift deleted file mode 100644 index 3ed1e5b5b..000000000 --- a/Sources/AWSLambdaPluginHelper/main.swift +++ /dev/null @@ -1 +0,0 @@ -print("Deployer") From 47dd2453f4dfd98b3dfe10666794d5a34a7931cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sun, 17 Nov 2024 22:14:28 +0100 Subject: [PATCH 07/27] use Foundation to access Process on Linux --- Plugins/AWSLambdaBuilder/Plugin.swift | 7 +------ Plugins/AWSLambdaDeployer/Plugin.swift | 7 +------ Plugins/AWSLambdaInitializer/Plugin.swift | 7 +------ 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/Plugins/AWSLambdaBuilder/Plugin.swift b/Plugins/AWSLambdaBuilder/Plugin.swift index 8dd8a81ca..e6158b8b6 100644 --- a/Plugins/AWSLambdaBuilder/Plugin.swift +++ b/Plugins/AWSLambdaBuilder/Plugin.swift @@ -12,13 +12,8 @@ // //===----------------------------------------------------------------------===// -import PackagePlugin - -#if canImport(FoundationEssentials) -import FoundationEssentials -#else import Foundation -#endif +import PackagePlugin @main struct AWSLambdaPackager: CommandPlugin { diff --git a/Plugins/AWSLambdaDeployer/Plugin.swift b/Plugins/AWSLambdaDeployer/Plugin.swift index 9ae59b0b3..b8a0915e0 100644 --- a/Plugins/AWSLambdaDeployer/Plugin.swift +++ b/Plugins/AWSLambdaDeployer/Plugin.swift @@ -12,13 +12,8 @@ // //===----------------------------------------------------------------------===// -import PackagePlugin - -#if canImport(FoundationEssentials) -import FoundationEssentials -#else import Foundation -#endif +import PackagePlugin @main struct AWSLambdaDeployer: CommandPlugin { diff --git a/Plugins/AWSLambdaInitializer/Plugin.swift b/Plugins/AWSLambdaInitializer/Plugin.swift index bbfa90e27..026e70cd4 100644 --- a/Plugins/AWSLambdaInitializer/Plugin.swift +++ b/Plugins/AWSLambdaInitializer/Plugin.swift @@ -12,13 +12,8 @@ // //===----------------------------------------------------------------------===// -import PackagePlugin - -#if canImport(FoundationEssentials) -import FoundationEssentials -#else import Foundation -#endif +import PackagePlugin @main struct AWSLambdaPackager: CommandPlugin { From 06654302d57c075247e4b68290ef8cb138d4c953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sun, 24 Nov 2024 20:40:18 +0100 Subject: [PATCH 08/27] add --with-url option --- .../lambda-init/Initializer.swift | 54 +++++++++++++------ .../lambda-init/Template.swift | 29 ++++++++++ 2 files changed, 68 insertions(+), 15 deletions(-) diff --git a/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift b/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift index 8515d5867..029543176 100644 --- a/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift @@ -21,42 +21,48 @@ import Foundation struct Initializer { private let destFileName = "Sources/main.swift" - + func initialize(arguments: [String]) async throws { let configuration = try InitializerConfiguration(arguments: arguments) - + if configuration.help { self.displayHelpMessage() return } - + let destFileURL = configuration.destinationDir.appendingPathComponent(destFileName) do { - try functionWithUrlTemplate.write(to: destFileURL, atomically: true, encoding: .utf8) - + + let template = TemplateType.template(for: configuration.templateType) + try template.write(to: destFileURL, atomically: true, encoding: .utf8) + if configuration.verboseLogging { print("File created at: \(destFileURL)") } - + print("✅ Lambda function written to \(destFileName)") - print("📦 You can now package with: 'swift package archive'") + print("📦 You can now package with: 'swift package lambda-build'") } catch { print("🛑Failed to create the Lambda function file: \(error)") } } - - + + private func displayHelpMessage() { print( """ OVERVIEW: A SwiftPM plugin to scaffold a HelloWorld 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 with an URL --allow-writing-to-package-directory Don't ask for permissions to write files. --verbose Produce verbose output for debugging. --help Show help information. @@ -65,32 +71,50 @@ struct Initializer { } } +private enum TemplateType { + case `default` + case url + + static func template(for type: TemplateType) -> String { + switch type { + case .default: return functionWithJSONTemplate + case .url: return functionWithUrlTemplate + } + } +} + private struct InitializerConfiguration: CustomStringConvertible { public let help: Bool public let verboseLogging: Bool public let destinationDir: URL - + public let templateType: TemplateType + public init(arguments: [String]) throws { var argumentExtractor = ArgumentExtractor(arguments) let verboseArgument = argumentExtractor.extractFlag(named: "verbose") > 0 let helpArgument = argumentExtractor.extractFlag(named: "help") > 0 let destDirArgument = argumentExtractor.extractOption(named: "dest-dir") - + let templateURLArgument = argumentExtractor.extractFlag(named: "with-url") > 0 + // help required ? self.help = helpArgument - + // verbose logging required ? self.verboseLogging = verboseArgument // dest dir self.destinationDir = URL(fileURLWithPath: destDirArgument[0]) + + // template type. Default is the JSON one + self.templateType = templateURLArgument ? .url : .default } - + var description: String { """ { verboseLogging: \(self.verboseLogging) destinationDir: \(self.destinationDir) + templateType: \(self.templateType) } """ } diff --git a/Sources/AWSLambdaPluginHelper/lambda-init/Template.swift b/Sources/AWSLambdaPluginHelper/lambda-init/Template.swift index 0ca37ace7..865a122a8 100644 --- a/Sources/AWSLambdaPluginHelper/lambda-init/Template.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-init/Template.swift @@ -31,3 +31,32 @@ let functionWithUrlTemplate = #""" try await runtime.run() """# + +let functionWithJSONTemplate = #""" + 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() + """# From 0bdc8135277cdba860db106bc633384c6477a924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 25 Nov 2024 15:27:06 +0100 Subject: [PATCH 09/27] swift format --- Package.swift | 67 +++---- Plugins/AWSLambdaBuilder/Plugin.swift | 125 ++++++------ Plugins/AWSLambdaInitializer/Plugin.swift | 3 +- .../AWSLambdaPluginHelper.swift | 11 +- .../Vendored/crypto/Padding.swift | 2 +- .../Vendored/spm/ArgumentExtractor.swift | 15 +- .../lambda-build/Builder.swift | 180 +++++++++--------- .../lambda-deploy/Deployer.swift | 2 +- .../lambda-init/Initializer.swift | 37 ++-- .../lambda-init/Template.swift | 2 +- 10 files changed, 220 insertions(+), 224 deletions(-) diff --git a/Package.swift b/Package.swift index 280977d47..5548b313b 100644 --- a/Package.swift +++ b/Package.swift @@ -12,26 +12,26 @@ let package = Package( name: "swift-aws-lambda-runtime", platforms: platforms, products: [ - - /* - The runtime library targets - */ - + + // + // The runtime library targets + // + // this library exports `AWSLambdaRuntimeCore` and adds Foundation convenience methods .library(name: "AWSLambdaRuntime", targets: ["AWSLambdaRuntime"]), // this has all the main functionality for lambda and it does not link Foundation .library(name: "AWSLambdaRuntimeCore", targets: ["AWSLambdaRuntimeCore"]), - /* - The plugins - 'lambda-init' creates a new Lambda function - 'lambda-build' packages the Lambda function - 'lambda-deploy' deploys the Lambda function - - Plugins requires Linux or at least macOS v15 + // + // 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"]), @@ -41,9 +41,10 @@ let package = Package( // plugin to deploy a Lambda function .plugin(name: "AWSLambdaDeployer", targets: ["AWSLambdaDeployer"]), - /* - Testing targets - */ + // + // Testing targets + // + // for testing only .library(name: "AWSLambdaTesting", targets: ["AWSLambdaTesting"]), ], @@ -88,23 +89,23 @@ let package = Package( ), // 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: "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( diff --git a/Plugins/AWSLambdaBuilder/Plugin.swift b/Plugins/AWSLambdaBuilder/Plugin.swift index e6158b8b6..fdb97efba 100644 --- a/Plugins/AWSLambdaBuilder/Plugin.swift +++ b/Plugins/AWSLambdaBuilder/Plugin.swift @@ -17,9 +17,9 @@ import PackagePlugin @main struct AWSLambdaPackager: CommandPlugin { - + func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws { - + // values to pass to the AWSLambdaPluginHelper let outputDirectory: URL let products: [Product] @@ -29,15 +29,15 @@ struct AWSLambdaPackager: CommandPlugin { let packageDirectory = context.package.directoryURL let dockerToolPath = try context.tool(named: "docker").url let zipToolPath = try context.tool(named: "zip").url - + // extract arguments that require PluginContext to fully resolve // resolve them here and pass them to the AWSLambdaPluginHelper as arguments var argumentExtractor = ArgumentExtractor(arguments) - + let outputPathArgument = argumentExtractor.extractOption(named: "output-path") let productsArgument = argumentExtractor.extractOption(named: "products") let configurationArgument = argumentExtractor.extractOption(named: "configuration") - + if let outputPath = outputPathArgument.first { #if os(Linux) var isDirectory: Bool = false @@ -52,7 +52,7 @@ struct AWSLambdaPackager: CommandPlugin { } else { outputDirectory = context.pluginWorkDirectoryURL.appending(path: "\(AWSLambdaPackager.self)") } - + let explicitProducts = !productsArgument.isEmpty if explicitProducts { let _products = try context.package.products(named: productsArgument) @@ -62,11 +62,11 @@ struct AWSLambdaPackager: CommandPlugin { } } products = _products - + } else { products = context.package.products.filter { $0 is ExecutableProduct } } - + if let _buildConfigurationName = configurationArgument.first { guard let _buildConfiguration = PackageManager.BuildConfiguration(rawValue: _buildConfigurationName) else { throw BuilderErrors.invalidArgument("invalid build configuration named '\(_buildConfigurationName)'") @@ -75,22 +75,23 @@ struct AWSLambdaPackager: CommandPlugin { } else { buildConfiguration = .release } - + // TODO: When running on Amazon Linux 2, we have to build directly from the plugin // launch the build, then call the helper just for the ZIP part - + let tool = try context.tool(named: "AWSLambdaPluginHelper") - let args = [ - "build", - "--output-path", outputDirectory.path(), - "--products", products.map { $0.name }.joined(separator: ","), - "--configuration", buildConfiguration.rawValue, - "--package-id", packageID, - "--package-display-name", packageDisplayName, - "--package-directory", packageDirectory.path(), - "--docker-tool-path", dockerToolPath.path, - "--zip-tool-path", zipToolPath.path - ] + arguments + let args = + [ + "build", + "--output-path", outputDirectory.path(), + "--products", products.map { $0.name }.joined(separator: ","), + "--configuration", buildConfiguration.rawValue, + "--package-id", packageID, + "--package-display-name", packageDisplayName, + "--package-directory", packageDirectory.path(), + "--docker-tool-path", dockerToolPath.path, + "--zip-tool-path", zipToolPath.path, + ] + arguments // Invoke the plugin helper on the target directory, passing a configuration // file from the package directory. @@ -103,52 +104,52 @@ struct AWSLambdaPackager: CommandPlugin { Diagnostics.error("AWSLambdaPluginHelper invocation failed: \(problem)") } } - + // TODO: When running on Amazon Linux 2, we have to build directly from the plugin -// private func build( -// packageIdentity: Package.ID, -// products: [Product], -// buildConfiguration: PackageManager.BuildConfiguration, -// verboseLogging: Bool -// ) throws -> [LambdaProduct: URL] { -// print("-------------------------------------------------------------------------") -// print("building \"\(packageIdentity)\"") -// print("-------------------------------------------------------------------------") -// -// var results = [LambdaProduct: URL]() -// for product in products { -// print("building \"\(product.name)\"") -// var parameters = PackageManager.BuildParameters() -// parameters.configuration = buildConfiguration -// parameters.otherSwiftcFlags = ["-static-stdlib"] -// parameters.logging = verboseLogging ? .verbose : .concise -// -// let result = try packageManager.build( -// .product(product.name), -// parameters: parameters -// ) -// guard let artifact = result.executableArtifact(for: product) else { -// throw Errors.productExecutableNotFound(product.name) -// } -// results[.init(product)] = artifact.url -// } -// return results -// } - -// private func isAmazonLinux2() -> Bool { -// 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 build( + // packageIdentity: Package.ID, + // products: [Product], + // buildConfiguration: PackageManager.BuildConfiguration, + // verboseLogging: Bool + // ) throws -> [LambdaProduct: URL] { + // print("-------------------------------------------------------------------------") + // print("building \"\(packageIdentity)\"") + // print("-------------------------------------------------------------------------") + // + // var results = [LambdaProduct: URL]() + // for product in products { + // print("building \"\(product.name)\"") + // var parameters = PackageManager.BuildParameters() + // parameters.configuration = buildConfiguration + // parameters.otherSwiftcFlags = ["-static-stdlib"] + // parameters.logging = verboseLogging ? .verbose : .concise + // + // let result = try packageManager.build( + // .product(product.name), + // parameters: parameters + // ) + // guard let artifact = result.executableArtifact(for: product) else { + // throw Errors.productExecutableNotFound(product.name) + // } + // results[.init(product)] = artifact.url + // } + // return results + // } + + // private func isAmazonLinux2() -> Bool { + // 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 enum BuilderErrors: Error, CustomStringConvertible { case invalidArgument(String) - + var description: String { switch self { case .invalidArgument(let description): diff --git a/Plugins/AWSLambdaInitializer/Plugin.swift b/Plugins/AWSLambdaInitializer/Plugin.swift index 026e70cd4..5b508ff3c 100644 --- a/Plugins/AWSLambdaInitializer/Plugin.swift +++ b/Plugins/AWSLambdaInitializer/Plugin.swift @@ -20,7 +20,7 @@ struct AWSLambdaPackager: 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 @@ -35,4 +35,3 @@ struct AWSLambdaPackager: CommandPlugin { } } } - diff --git a/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift b/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift index 71b5af7ed..2c124b5f3 100644 --- a/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift +++ b/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift @@ -14,13 +14,13 @@ @main struct AWSLambdaPluginHelper { - + private enum Command: String { case `init` case build case deploy } - + public static func main() async throws { let args = CommandLine.arguments let helper = AWSLambdaPluginHelper() @@ -34,10 +34,10 @@ struct AWSLambdaPluginHelper { try await Deployer().deploy(arguments: args) } } - + private func command(from arguments: [String]) throws -> Command { let args = CommandLine.arguments - + guard args.count > 2 else { throw AWSLambdaPluginHelperError.noCommand } @@ -45,7 +45,7 @@ struct AWSLambdaPluginHelper { guard let command = Command(rawValue: commandName) else { throw AWSLambdaPluginHelperError.invalidCommand(commandName) } - + return command } } @@ -54,4 +54,3 @@ private enum AWSLambdaPluginHelperError: Error { case noCommand case invalidCommand(String) } - diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift index 679e161f6..fb714a4f5 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift @@ -33,7 +33,7 @@ public protocol PaddingProtocol { } public enum Padding: PaddingProtocol { - case noPadding, zeroPadding + case noPadding, zeroPadding public func add(to: [UInt8], blockSize: Int) -> [UInt8] { switch self { case .noPadding: diff --git a/Sources/AWSLambdaPluginHelper/Vendored/spm/ArgumentExtractor.swift b/Sources/AWSLambdaPluginHelper/Vendored/spm/ArgumentExtractor.swift index 8406692c7..cee229a07 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/spm/ArgumentExtractor.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/spm/ArgumentExtractor.swift @@ -50,13 +50,11 @@ public struct ArgumentExtractor { values.append(val) args.remove(at: idx) } - } - else if arg.starts(with: "--\(name)=") { + } else if arg.starts(with: "--\(name)=") { arg.removeFirst(2 + name.count + 1) values.append(arg) args.remove(at: idx) - } - else { + } else { idx += 1 } } @@ -72,8 +70,7 @@ public struct ArgumentExtractor { if arg == "--\(name)" { args.remove(at: idx) count += 1 - } - else { + } else { idx += 1 } } @@ -82,11 +79,11 @@ public struct ArgumentExtractor { /// Returns any unextracted flags or options (based strictly on whether remaining arguments have a "--" prefix). public var unextractedOptionsOrFlags: [String] { - return args.filter{ $0.hasPrefix("--") } + args.filter { $0.hasPrefix("--") } } /// Returns all remaining arguments, including any literals after the first `--` if there is one. public var remainingArguments: [String] { - return args + literals + args + literals } -} \ No newline at end of file +} diff --git a/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift b/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift index fb8ff73a6..acd01c281 100644 --- a/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift @@ -22,14 +22,14 @@ import Foundation struct Builder { func build(arguments: [String]) async throws { let configuration = try BuilderConfiguration(arguments: arguments) - + if configuration.help { self.displayHelpMessage() return } - + let builtProducts: [String: URL] - + // build with docker // TODO: check if dockerToolPath is provided // When not provided, it means we're building on Amazon Linux 2 @@ -44,7 +44,7 @@ struct Builder { buildConfiguration: configuration.buildConfiguration, verboseLogging: configuration.verboseLogging ) - + // create the archive let archives = try self.package( packageName: configuration.packageDisplayName, @@ -53,7 +53,7 @@ struct Builder { outputDirectory: configuration.outputDirectory, verboseLogging: configuration.verboseLogging ) - + print( "\(archives.count > 0 ? archives.count.description : "no") archive\(archives.count != 1 ? "s" : "") created" ) @@ -61,7 +61,7 @@ struct Builder { print(" * \(product) at \(archivePath.path())") } } - + private func buildInDocker( packageIdentity: String, packageDirectory: URL, @@ -73,11 +73,11 @@ struct Builder { buildConfiguration: BuildConfiguration, verboseLogging: Bool ) throws -> [String: URL] { - + print("-------------------------------------------------------------------------") print("building \"\(packageIdentity)\" in docker") print("-------------------------------------------------------------------------") - + if !disableDockerImageUpdate { // update the underlying docker image, if necessary print("updating \"\(baseImage)\" docker image") @@ -87,7 +87,7 @@ struct Builder { logLevel: verboseLogging ? .debug : .silent ) } - + // get the build output path let buildOutputPathCommand = "swift build -c \(buildConfiguration.rawValue) --show-bin-path" let dockerBuildOutputPath = try Utils.execute( @@ -104,13 +104,13 @@ struct Builder { let buildOutputPath = URL( string: buildPathOutput.replacingOccurrences(of: "/workspace/", with: packageDirectory.description) )! - + // build the products var builtProducts = [String: URL]() for product in products { print("building \"\(product)\"") let buildCommand = - "swift build -c \(buildConfiguration.rawValue) --product \(product) --static-swift-stdlib" + "swift build -c \(buildConfiguration.rawValue) --product \(product) --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 ../.. @@ -136,7 +136,7 @@ struct Builder { ) } let productPath = buildOutputPath.appending(path: product) - + guard FileManager.default.fileExists(atPath: productPath.path()) else { print("expected '\(product)' binary at \"\(productPath.path())\"") throw BuilderErrors.productExecutableNotFound(product) @@ -145,7 +145,7 @@ struct Builder { } return builtProducts } - + // TODO: explore using ziplib or similar instead of shelling out private func package( packageName: String, @@ -154,13 +154,13 @@ struct Builder { outputDirectory: URL, verboseLogging: Bool ) throws -> [String: URL] { - + var archives = [String: URL]() for (product, artifactPath) in products { print("-------------------------------------------------------------------------") print("archiving \"\(product)\"") print("-------------------------------------------------------------------------") - + // prep zipfile location let workingDirectory = outputDirectory.appending(path: product) let zipfilePath = workingDirectory.appending(path: "\(product).zip") @@ -168,23 +168,23 @@ struct Builder { try FileManager.default.removeItem(atPath: workingDirectory.path()) } try FileManager.default.createDirectory(atPath: workingDirectory.path(), withIntermediateDirectories: true) - + // rename artifact to "bootstrap" let relocatedArtifactPath = workingDirectory.appending(path: "bootstrap") try FileManager.default.copyItem(atPath: artifactPath.path(), toPath: relocatedArtifactPath.path()) - + var arguments: [String] = [] -#if os(macOS) || os(Linux) + #if os(macOS) || os(Linux) arguments = [ "--recurse-paths", "--symlinks", zipfilePath.lastPathComponent, relocatedArtifactPath.lastPathComponent, ] -#else + #else throw Errors.unsupportedPlatform("can't or don't know how to create a zip file on this platform") -#endif - + #endif + // add resources var artifactPathComponents = artifactPath.pathComponents _ = artifactPathComponents.removeFirst() // Get rid of beginning "/" @@ -194,7 +194,7 @@ struct Builder { guard let artifactURL = URL(string: "\(artifactDirectory)/\(fileInArtifactDirectory)") else { continue } - + guard artifactURL.pathExtension == "resources" else { continue // Not resources, so don't copy } @@ -208,7 +208,7 @@ struct Builder { arguments.append(resourcesDirectoryName) } } - + // run the zip tool try Utils.execute( executable: zipToolPath, @@ -216,52 +216,52 @@ struct Builder { customWorkingDirectory: workingDirectory, logLevel: verboseLogging ? .debug : .silent ) - + archives[product] = zipfilePath } return archives } - + 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-directory ] - [--products ] - [--configuration debug | release] - [--swift-version ] - [--base-docker-image ] - [--disable-docker-image-update] - - - OPTIONS: - --verbose Produce verbose output for debugging. - --output-directory 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. - """ + """ + 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-directory ] + [--products ] + [--configuration debug | release] + [--swift-version ] + [--base-docker-image ] + [--disable-docker-image-update] + + + OPTIONS: + --verbose Produce verbose output for debugging. + --output-directory 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. + """ ) } } private struct BuilderConfiguration: CustomStringConvertible { - + // passed by the user public let help: Bool public let outputDirectory: URL @@ -280,7 +280,7 @@ private struct BuilderConfiguration: CustomStringConvertible { public init(arguments: [String]) throws { var argumentExtractor = ArgumentExtractor(arguments) - + let verboseArgument = argumentExtractor.extractFlag(named: "verbose") > 0 let outputPathArgument = argumentExtractor.extractOption(named: "output-path") let packageIDArgument = argumentExtractor.extractOption(named: "package-id") @@ -294,55 +294,55 @@ private struct BuilderConfiguration: CustomStringConvertible { 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 - + // package id guard !packageIDArgument.isEmpty else { throw BuilderErrors.invalidArgument("--package-id argument is required") } self.packageID = packageIDArgument.first! - + // package display name guard !packageDisplayNameArgument.isEmpty else { throw BuilderErrors.invalidArgument("--package-display-name argument is required") } self.packageDisplayName = packageDisplayNameArgument.first! - + // package directory guard !packageDirectoryArgument.isEmpty else { throw BuilderErrors.invalidArgument("--package-directory argument is required") } self.packageDirectory = URL(fileURLWithPath: packageDirectoryArgument.first!) - + // docker tool path guard !dockerToolPathArgument.isEmpty else { throw BuilderErrors.invalidArgument("--docker-tool-path argument is required") } self.dockerToolPath = URL(fileURLWithPath: dockerToolPathArgument.first!) - + // zip tool path guard !zipToolPathArgument.isEmpty else { throw BuilderErrors.invalidArgument("--zip-tool-path argument is required") } self.zipToolPath = URL(fileURLWithPath: zipToolPathArgument.first!) - + // output directory guard !outputPathArgument.isEmpty else { throw BuilderErrors.invalidArgument("--output-path is required") } self.outputDirectory = URL(fileURLWithPath: outputPathArgument.first!) - + // products guard !productsArgument.isEmpty else { throw BuilderErrors.invalidArgument("--products argument is required") } self.products = productsArgument.flatMap { $0.split(separator: ",").map(String.init) } - + // build configuration guard let buildConfigurationName = configurationArgument.first else { throw BuilderErrors.invalidArgument("--configuration argument is equired") @@ -351,17 +351,17 @@ private struct BuilderConfiguration: CustomStringConvertible { throw BuilderErrors.invalidArgument("invalid build configuration named '\(buildConfigurationName)'") } self.buildConfiguration = _buildConfiguration - + guard !(!swiftVersionArgument.isEmpty && !baseDockerImageArgument.isEmpty) else { throw BuilderErrors.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" - + baseDockerImageArgument.first ?? "swift:\(swiftVersion.map { $0 + "-" } ?? "")amazonlinux2" + self.disableDockerImageUpdate = disableDockerImageUpdateArgument - + if self.verboseLogging { print("-------------------------------------------------------------------------") print("configuration") @@ -369,22 +369,22 @@ private struct BuilderConfiguration: CustomStringConvertible { print(self) } } - + var description: String { - """ - { - outputDirectory: \(self.outputDirectory) - products: \(self.products) - buildConfiguration: \(self.buildConfiguration) - dockerToolPath: \(self.dockerToolPath) - baseDockerImage: \(self.baseDockerImage) - disableDockerImageUpdate: \(self.disableDockerImageUpdate) - zipToolPath: \(self.zipToolPath) - packageID: \(self.packageID) - packageDisplayName: \(self.packageDisplayName) - packageDirectory: \(self.packageDirectory) - } - """ + """ + { + outputDirectory: \(self.outputDirectory) + products: \(self.products) + buildConfiguration: \(self.buildConfiguration) + dockerToolPath: \(self.dockerToolPath) + baseDockerImage: \(self.baseDockerImage) + disableDockerImageUpdate: \(self.disableDockerImageUpdate) + zipToolPath: \(self.zipToolPath) + packageID: \(self.packageID) + packageDisplayName: \(self.packageDisplayName) + packageDirectory: \(self.packageDirectory) + } + """ } } @@ -396,7 +396,7 @@ private enum BuilderErrors: Error, CustomStringConvertible { case failedWritingDockerfile case failedParsingDockerOutput(String) case processFailed([String], Int32) - + var description: String { switch self { case .invalidArgument(let description): diff --git a/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift b/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift index a9e2462f8..480ff2d49 100644 --- a/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift @@ -47,7 +47,7 @@ struct Deployer { --help Show help information. """ ) - } + } } private struct DeployerConfiguration: CustomStringConvertible { diff --git a/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift b/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift index 029543176..8c3d0f283 100644 --- a/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift @@ -19,48 +19,47 @@ import Foundation #endif struct Initializer { - + private let destFileName = "Sources/main.swift" - + func initialize(arguments: [String]) async throws { - + let configuration = try InitializerConfiguration(arguments: arguments) - + if configuration.help { self.displayHelpMessage() return } - + let destFileURL = configuration.destinationDir.appendingPathComponent(destFileName) do { - + let template = TemplateType.template(for: configuration.templateType) try template.write(to: destFileURL, atomically: true, encoding: .utf8) - + if configuration.verboseLogging { print("File created at: \(destFileURL)") } - + print("✅ Lambda function written to \(destFileName)") print("📦 You can now package with: 'swift package lambda-build'") } catch { print("🛑Failed to create the Lambda function file: \(error)") } } - - + private func displayHelpMessage() { print( """ OVERVIEW: A SwiftPM plugin to scaffold a HelloWorld 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 with an URL --allow-writing-to-package-directory Don't ask for permissions to write files. @@ -74,7 +73,7 @@ struct Initializer { private enum TemplateType { case `default` case url - + static func template(for type: TemplateType) -> String { switch type { case .default: return functionWithJSONTemplate @@ -88,27 +87,27 @@ private struct InitializerConfiguration: CustomStringConvertible { public let verboseLogging: Bool public let destinationDir: URL public let templateType: TemplateType - + public init(arguments: [String]) throws { var argumentExtractor = ArgumentExtractor(arguments) let verboseArgument = argumentExtractor.extractFlag(named: "verbose") > 0 let helpArgument = argumentExtractor.extractFlag(named: "help") > 0 let destDirArgument = argumentExtractor.extractOption(named: "dest-dir") let templateURLArgument = argumentExtractor.extractFlag(named: "with-url") > 0 - + // help required ? self.help = helpArgument - + // verbose logging required ? self.verboseLogging = verboseArgument - + // dest dir self.destinationDir = URL(fileURLWithPath: destDirArgument[0]) - + // template type. Default is the JSON one self.templateType = templateURLArgument ? .url : .default } - + var description: String { """ { diff --git a/Sources/AWSLambdaPluginHelper/lambda-init/Template.swift b/Sources/AWSLambdaPluginHelper/lambda-init/Template.swift index 865a122a8..404d743bf 100644 --- a/Sources/AWSLambdaPluginHelper/lambda-init/Template.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-init/Template.swift @@ -45,7 +45,7 @@ let functionWithJSONTemplate = #""" struct HelloResponse: Encodable { let greetings: String } - + // in this example we receive a HelloRequest JSON and we return a HelloResponse JSON // the Lambda runtime From e3a579c6c0054c3c2cf5f32a1aa0e22e549ffea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 30 Jul 2025 20:51:35 +0400 Subject: [PATCH 10/27] minor wording changes --- Package.swift | 9 ++++----- Package@swift-6.0.swift | 9 ++++----- Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Package.swift b/Package.swift index b83b04c8f..555085320 100644 --- a/Package.swift +++ b/Package.swift @@ -101,12 +101,12 @@ let package = Package( 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." + "Compile and archive (zip) 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." + reason: "This plugin uses Docker to compile code for Amazon Linux." ) ] ), @@ -120,7 +120,7 @@ let package = Package( 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." + "Deploy the Lambda function. You must have an AWS account and an access key and secret access key." ), permissions: [ .allowNetworkConnections( @@ -138,8 +138,7 @@ let package = Package( dependencies: [ .product(name: "NIOHTTP1", package: "swift-nio"), .product(name: "NIOCore", package: "swift-nio"), - ], - swiftSettings: [.swiftLanguageMode(.v6)] + ] ), .testTarget( name: "AWSLambdaRuntimeTests", diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 726af1085..52aea491e 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -90,12 +90,12 @@ let package = Package( 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." + "Compile and archive (zip) 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." + reason: "This plugin uses Docker to compile code for Amazon Linux." ) ] ), @@ -109,7 +109,7 @@ let package = Package( 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." + "Deploy the Lambda function. You must have an AWS account and an access key and secret access key." ), permissions: [ .allowNetworkConnections( @@ -127,8 +127,7 @@ let package = Package( dependencies: [ .product(name: "NIOHTTP1", package: "swift-nio"), .product(name: "NIOCore", package: "swift-nio"), - ], - swiftSettings: [.swiftLanguageMode(.v6)] + ] ), .testTarget( name: "AWSLambdaRuntimeTests", diff --git a/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift b/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift index 9a3c33fea..c1f8b45a6 100644 --- a/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift @@ -247,7 +247,7 @@ struct Builder { REQUIREMENTS: To use this plugin, you must have docker installed and started. - USAGE: swift package --allow-network-connections docker archive + USAGE: swift package --allow-network-connections docker lambda-build [--help] [--verbose] [--output-path ] [--products ] From 174c9671425b95407a64c2d14b2015a714f5dcea Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 2 Nov 2025 22:04:31 +0100 Subject: [PATCH 11/27] fix license headers --- Plugins/AWSLambdaBuilder/Plugin.swift | 3 ++- Plugins/AWSLambdaDeployer/Plugin.swift | 3 ++- Plugins/AWSLambdaInitializer/Plugin.swift | 3 ++- .../AWSLambdaPluginHelper.swift | 3 ++- Sources/AWSLambdaPluginHelper/Extensions.swift | 3 ++- .../Vendored/crypto/Array+Extensions.swift | 16 ++++++++++++++++ .../Vendored/crypto/Authenticator.swift | 15 +++++++++++++++ .../Vendored/crypto/BatchedCollections.swift | 15 +++++++++++++++ .../Vendored/crypto/Bit.swift | 15 +++++++++++++++ .../Vendored/crypto/Collections+Extensions.swift | 4 ++-- .../Vendored/crypto/Digest.swift | 3 ++- .../Vendored/crypto/DigestType.swift | 3 ++- .../Vendored/crypto/Generics.swift | 3 ++- .../Vendored/crypto/HMAC.swift | 5 +++-- .../Vendored/crypto/Int+Extension.swift | 3 ++- .../Vendored/crypto/NoPadding.swift | 3 ++- .../Vendored/crypto/Padding.swift | 3 ++- .../Vendored/crypto/SHA1.swift | 3 ++- .../Vendored/crypto/SHA2.swift | 5 +++-- .../Vendored/crypto/SHA3.swift | 3 ++- .../Vendored/crypto/UInt16+Extension.swift | 3 ++- .../Vendored/crypto/UInt32+Extension.swift | 3 ++- .../Vendored/crypto/UInt64+Extension.swift | 3 ++- .../Vendored/crypto/UInt8+Extension.swift | 3 ++- .../Vendored/crypto/Updatable.swift | 3 ++- .../Vendored/crypto/Utils.swift | 3 ++- .../Vendored/crypto/ZeroPadding.swift | 3 ++- .../Vendored/signer/AWSCredentials.swift | 3 ++- .../Vendored/signer/AWSSigner.swift | 3 ++- .../Vendored/spm/ArgumentExtractor.swift | 3 ++- .../lambda-deploy/Deployer.swift | 4 ++-- .../lambda-init/Initializer.swift | 4 ++-- .../lambda-init/Template.swift | 3 ++- 33 files changed, 121 insertions(+), 34 deletions(-) diff --git a/Plugins/AWSLambdaBuilder/Plugin.swift b/Plugins/AWSLambdaBuilder/Plugin.swift index fdb97efba..c2da2f06e 100644 --- a/Plugins/AWSLambdaBuilder/Plugin.swift +++ b/Plugins/AWSLambdaBuilder/Plugin.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Plugins/AWSLambdaDeployer/Plugin.swift b/Plugins/AWSLambdaDeployer/Plugin.swift index b8a0915e0..60b9c163e 100644 --- a/Plugins/AWSLambdaDeployer/Plugin.swift +++ b/Plugins/AWSLambdaDeployer/Plugin.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2023 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Plugins/AWSLambdaInitializer/Plugin.swift b/Plugins/AWSLambdaInitializer/Plugin.swift index 5b508ff3c..88cfc0eea 100644 --- a/Plugins/AWSLambdaInitializer/Plugin.swift +++ b/Plugins/AWSLambdaInitializer/Plugin.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift b/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift index 2c124b5f3..5d6a16a8a 100644 --- a/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift +++ b/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Extensions.swift b/Sources/AWSLambdaPluginHelper/Extensions.swift index 67a8202b1..c3e4da5b1 100644 --- a/Sources/AWSLambdaPluginHelper/Extensions.swift +++ b/Sources/AWSLambdaPluginHelper/Extensions.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Array+Extensions.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Array+Extensions.swift index 000100185..d1b6b7d43 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Array+Extensions.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Array+Extensions.swift @@ -1,3 +1,19 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// 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 +// +//===----------------------------------------------------------------------===// +// + // // CryptoSwift // diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Authenticator.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Authenticator.swift index d45363e63..2af6a306c 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Authenticator.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Authenticator.swift @@ -1,3 +1,18 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// 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 +// +//===----------------------------------------------------------------------===// + // // CryptoSwift // diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/BatchedCollections.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/BatchedCollections.swift index 1dd0fd0e4..c075ad299 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/BatchedCollections.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/BatchedCollections.swift @@ -1,3 +1,18 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// 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 +// +//===----------------------------------------------------------------------===// + // // CryptoSwift // diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Bit.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Bit.swift index c2e29de60..6c9b25dbf 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Bit.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Bit.swift @@ -1,3 +1,18 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// 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 +// +//===----------------------------------------------------------------------===// + // // CryptoSwift // diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Collections+Extensions.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Collections+Extensions.swift index a205b2dfe..21beb5f64 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Collections+Extensions.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Collections+Extensions.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -11,7 +12,6 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - // // CryptoSwift // diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Digest.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Digest.swift index 020df5919..dc655cc9f 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Digest.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Digest.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/DigestType.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/DigestType.swift index 351387868..12e3963a5 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/DigestType.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/DigestType.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Generics.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Generics.swift index 38c6e3b97..cccc4114c 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Generics.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Generics.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift index b6fffc887..cb57cffd3 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift @@ -1,8 +1,9 @@ -//===----------------------------------------------------------------------===// +///===----------------------------------------------------------------------===// // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Int+Extension.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Int+Extension.swift index 33dddb516..a2021878c 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Int+Extension.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Int+Extension.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/NoPadding.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/NoPadding.swift index a6c519130..17a5f3859 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/NoPadding.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/NoPadding.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift index fb714a4f5..d2ea35278 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA1.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA1.swift index b5fcd3fba..c4dce58dc 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA1.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA1.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift index 4f54e39e7..ad5018f4a 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift @@ -1,8 +1,9 @@ -//===----------------------------------------------------------------------===// +///===----------------------------------------------------------------------===// // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA3.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA3.swift index 6fceac6d3..0662d6ba1 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA3.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA3.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt16+Extension.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt16+Extension.swift index 098e91a91..8efbd8d62 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt16+Extension.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt16+Extension.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt32+Extension.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt32+Extension.swift index e0e6434fa..ef71dd957 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt32+Extension.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt32+Extension.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt64+Extension.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt64+Extension.swift index afcd7e47b..652cce653 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt64+Extension.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt64+Extension.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt8+Extension.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt8+Extension.swift index f66510135..4ff0c225a 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt8+Extension.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt8+Extension.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Updatable.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Updatable.swift index 94d0efaa8..bb525da88 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Updatable.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Updatable.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Utils.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Utils.swift index 8a4ea7761..6c7b8118e 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Utils.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Utils.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/ZeroPadding.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/ZeroPadding.swift index 346075982..dccfdfd44 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/ZeroPadding.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/ZeroPadding.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/signer/AWSCredentials.swift b/Sources/AWSLambdaPluginHelper/Vendored/signer/AWSCredentials.swift index ae25f1699..f2c9ddc2e 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/signer/AWSCredentials.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/signer/AWSCredentials.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/signer/AWSSigner.swift b/Sources/AWSLambdaPluginHelper/Vendored/signer/AWSSigner.swift index 7196982ef..09965489d 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/signer/AWSSigner.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/signer/AWSSigner.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/Vendored/spm/ArgumentExtractor.swift b/Sources/AWSLambdaPluginHelper/Vendored/spm/ArgumentExtractor.swift index cee229a07..15e460d47 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/spm/ArgumentExtractor.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/spm/ArgumentExtractor.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift b/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift index 480ff2d49..3bba4169d 100644 --- a/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -11,7 +12,6 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - #if canImport(FoundationEssentials) import FoundationEssentials #else diff --git a/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift b/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift index 8c3d0f283..83ec14b09 100644 --- a/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -11,7 +12,6 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - #if canImport(FoundationEssentials) import FoundationEssentials #else diff --git a/Sources/AWSLambdaPluginHelper/lambda-init/Template.swift b/Sources/AWSLambdaPluginHelper/lambda-init/Template.swift index 404d743bf..3d5f057b8 100644 --- a/Sources/AWSLambdaPluginHelper/lambda-init/Template.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-init/Template.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information From 90ff596a5689221f25f700594f5f9ef97e61e509 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 2 Nov 2025 22:07:47 +0100 Subject: [PATCH 12/27] fix shell check --- .../_MyFirstFunction/create_and_deploy_function.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Examples/_MyFirstFunction/create_and_deploy_function.sh b/Examples/_MyFirstFunction/create_and_deploy_function.sh index fe3ae57dd..f7dc90c41 100755 --- a/Examples/_MyFirstFunction/create_and_deploy_function.sh +++ b/Examples/_MyFirstFunction/create_and_deploy_function.sh @@ -31,11 +31,12 @@ check_prerequisites() { 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 + printf "Are you ready to create your first Lambda function in Swift? [y/n] " + read -r continue + case $continue in + [Yy]*) ;; + *) echo "OK, try again later when you feel ready"; exit 1 ;; + esac } create_lambda_execution_role() { From 6502638f4a3dbe916cf77fcb4256096bdd4b32a8 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 2 Nov 2025 22:10:14 +0100 Subject: [PATCH 13/27] more license header fixes --- Tests/AWSLambdaPluginHelperTests/AWSSignerTests.swift | 3 ++- Tests/AWSLambdaPluginHelperTests/CryptoTests.swift | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/AWSLambdaPluginHelperTests/AWSSignerTests.swift b/Tests/AWSLambdaPluginHelperTests/AWSSignerTests.swift index 4cf5727dd..e9c70cfcf 100644 --- a/Tests/AWSLambdaPluginHelperTests/AWSSignerTests.swift +++ b/Tests/AWSLambdaPluginHelperTests/AWSSignerTests.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Tests/AWSLambdaPluginHelperTests/CryptoTests.swift b/Tests/AWSLambdaPluginHelperTests/CryptoTests.swift index 7a65e3048..3f9e7aec7 100644 --- a/Tests/AWSLambdaPluginHelperTests/CryptoTests.swift +++ b/Tests/AWSLambdaPluginHelperTests/CryptoTests.swift @@ -2,7 +2,8 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information From 77d29af7a6153db9ed0da9f05191648a1bdee7fd Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 2 Nov 2025 22:11:25 +0100 Subject: [PATCH 14/27] more fixes fro shell checks --- Examples/_MyFirstFunction/create_function.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Examples/_MyFirstFunction/create_function.sh b/Examples/_MyFirstFunction/create_function.sh index 71fd4ad7e..f42ac73c6 100755 --- a/Examples/_MyFirstFunction/create_function.sh +++ b/Examples/_MyFirstFunction/create_function.sh @@ -1,8 +1,7 @@ #!/bin/sh # check if docker is installed -which docker > /dev/null -if [[ $? != 0 ]]; then +if ! which docker > /dev/null; then echo "Docker is not installed. Please install Docker and try again." exit 1 fi @@ -13,11 +12,12 @@ 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 +printf "Are you ready to create your first Lambda function in Swift? [y/n] " +read -r continue +case $continue in + [Yy]*) ;; + *) echo "OK, try again later when you feel ready"; exit 1 ;; +esac echo "⚡️ Create your Swift command line project" swift package init --type executable --name MyLambda From da47411f65272f94881ad1a55411328d5aa7ce89 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 2 Nov 2025 22:16:57 +0100 Subject: [PATCH 15/27] mor elicense fixes --- Examples/_MyFirstFunction/create_function.sh | 14 ++++++++++++++ .../Vendored/crypto/SHA2.swift | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Examples/_MyFirstFunction/create_function.sh b/Examples/_MyFirstFunction/create_function.sh index f42ac73c6..87d9be21a 100755 --- a/Examples/_MyFirstFunction/create_function.sh +++ b/Examples/_MyFirstFunction/create_function.sh @@ -1,4 +1,18 @@ #!/bin/sh +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright SwiftAWSLambdaRuntime project authors +## Copyright (c) Amazon.com, Inc. or its affiliates. +## 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 +## +##===----------------------------------------------------------------------===## # check if docker is installed if ! which docker > /dev/null; then diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift index ad5018f4a..d2be9d49f 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift @@ -1,4 +1,4 @@ -///===----------------------------------------------------------------------===// +//===----------------------------------------------------------------------===// // // This source file is part of the SwiftAWSLambdaRuntime open source project // From bf73943759325af214f63f1790f5159dcb9faf8c Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 2 Nov 2025 22:30:37 +0100 Subject: [PATCH 16/27] more license header fix --- Examples/_MyFirstFunction/create_function.sh | 2 +- Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/_MyFirstFunction/create_function.sh b/Examples/_MyFirstFunction/create_function.sh index 87d9be21a..f0b9315cb 100755 --- a/Examples/_MyFirstFunction/create_function.sh +++ b/Examples/_MyFirstFunction/create_function.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash ##===----------------------------------------------------------------------===## ## ## This source file is part of the SwiftAWSLambdaRuntime open source project diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift index cb57cffd3..a3977ed2e 100644 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift +++ b/Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift @@ -1,4 +1,4 @@ -///===----------------------------------------------------------------------===// +//===----------------------------------------------------------------------===// // // This source file is part of the SwiftAWSLambdaRuntime open source project // From 71c6a896986ad2a9a2994973dcc7896e4562fa10 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 2 Nov 2025 22:35:04 +0100 Subject: [PATCH 17/27] fix unit test errors on nightly-main --- Package.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 2b51e8a5e..57b6cfd8b 100644 --- a/Package.swift +++ b/Package.swift @@ -171,7 +171,8 @@ let package = Package( .testTarget( name: "AWSLambdaPluginHelperTests", dependencies: [ - .byName(name: "AWSLambdaPluginHelper") + .byName(name: "AWSLambdaPluginHelper"), + .product(name: "Logging", package: "swift-log"), ] ), From 030a6c01428624c468e8db67e8c20b8bb7fedcc9 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 2 Nov 2025 22:50:21 +0100 Subject: [PATCH 18/27] fix plugin package --- Package.swift | 6 ++++-- Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 57b6cfd8b..e4a717131 100644 --- a/Package.swift +++ b/Package.swift @@ -145,7 +145,8 @@ let package = Package( dependencies: [ .product(name: "NIOHTTP1", package: "swift-nio"), .product(name: "NIOCore", package: "swift-nio"), - ] + ], + swiftSettings: defaultSwiftSettings ), .testTarget( name: "AWSLambdaRuntimeTests", @@ -173,7 +174,8 @@ let package = Package( dependencies: [ .byName(name: "AWSLambdaPluginHelper"), .product(name: "Logging", package: "swift-log"), - ] + ], + swiftSettings: defaultSwiftSettings ), ] diff --git a/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift b/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift index 5d6a16a8a..18de7f8de 100644 --- a/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift +++ b/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift @@ -14,6 +14,7 @@ //===----------------------------------------------------------------------===// @main +@available(LambdaSwift 2.0, *) struct AWSLambdaPluginHelper { private enum Command: String { From 728150cd6fabc0d7c49ba79ea7276b7a1fa97219 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 2 Nov 2025 23:03:45 +0100 Subject: [PATCH 19/27] fix 6.0 language mode --- Package@swift-6.0.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index bdcff8e92..4a3c53292 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -131,7 +131,8 @@ let package = Package( dependencies: [ .product(name: "NIOHTTP1", package: "swift-nio"), .product(name: "NIOCore", package: "swift-nio"), - ] + ], + swiftSettings: defaultSwiftSettings ), .testTarget( name: "AWSLambdaRuntimeTests", From 37a30a4a1ba896a7b8cb4d8f82d945f0baced9ce Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Tue, 17 Feb 2026 17:07:20 +0100 Subject: [PATCH 20/27] Update Plugin Spec doc to add Apple container --- .../Proposals/0001-v2-plugins.md | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/Plugins/Documentation.docc/Proposals/0001-v2-plugins.md b/Plugins/Documentation.docc/Proposals/0001-v2-plugins.md index 6e58d0df5..f7c21e91f 100644 --- a/Plugins/Documentation.docc/Proposals/0001-v2-plugins.md +++ b/Plugins/Documentation.docc/Proposals/0001-v2-plugins.md @@ -11,7 +11,7 @@ This document describes a proposal for the v2 plugins for `swift-aws-lambda-runt Versions: * v1 (2024-12-25): Initial version -* v2 (2025-03-13): +* 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** @@ -19,6 +19,9 @@ Versions: - [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. +* v3 (2026-02-17): +- [build] Add `--container-cli` option to support Apple's `container` CLI as an alternative to Docker (addresses [#644](https://github.com/awslabs/swift-aws-lambda-runtime/issues/644)). +- [build] Add the section **Container CLI options** ## Motivation @@ -160,7 +163,8 @@ The plugin interface is based on the existing `archive` plugin, with the additio ```text OVERVIEW: A SwiftPM plugin to build and package your Lambda function. -REQUIREMENTS: To use this plugin, Docker must be installed and running. +REQUIREMENTS: To use this plugin, Docker or Apple container must be installed and running + (when using docker or container cross-compilation methods). USAGE: swift package archive [--help] [--verbose] @@ -172,6 +176,7 @@ USAGE: swift package archive [--disable-docker-image-update] [--no-strip] [--cross-compile ] + [--container-cli ] [--allow-network-connections docker] OPTIONS: @@ -192,6 +197,9 @@ OPTIONS: --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 +--container-cli Specify the container CLI to use for Docker-based builds. + (default: docker) Accepted values are: docker, container + This parameter is only used when --cross-compile is set to docker. ``` #### Cross compiling options @@ -205,6 +213,40 @@ For an ideal developer experience, we would imagine the following sequence: - 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 +#### Container CLI options + +The plugin supports using different container CLIs to build Lambda packages. By default, it uses Docker, but it can also use Apple's `container` CLI on macOS, which provides native support for OCI (Open Container Initiative) images. + +**Supported Container CLIs:** + +- **Docker** (default): Uses the standard Docker CLI + - Pull images: `docker pull ` + - Run containers: `docker run --rm -v -w bash -cl ""` + +- **Apple container**: Uses Apple's native container CLI (available on macOS) + - Pull images: `container image pull ` + - Run containers: `container run --rm -v -w bash -cl ""` + +**Configuration:** + +The container CLI can be specified via the command-line flag: `--container-cli docker` or `--container-cli container`. If not specified, the plugin defaults to `docker`. + +**Example usage:** + +```bash +# Use Docker (default) +swift package lambda-build + +# Use Apple container +swift package lambda-build --container-cli container +``` + +**Requirements:** + +- When using `docker`, Docker Desktop or Docker Engine must be installed and running +- When using `container`, Apple's container CLI must be installed and running +- The `--container-cli` option is only applicable when using Docker-based cross-compilation (`--cross-compile docker`) + ### 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. From c89cb8c693b5d987d07c86602daf29fae336df40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 3 Jun 2026 19:14:35 +0200 Subject: [PATCH 21/27] add .kiro directory for project history --- .gitignore | 1 - .kiro/hooks/swift-syntax-check.kiro.hook | 18 ++ .kiro/settings/lsp.json | 198 ++++++++++++++++ .../al2-deprecation-warning/.config.kiro | 1 + .kiro/specs/al2-deprecation-warning/design.md | 116 +++++++++ .../al2-deprecation-warning/requirements.md | 92 ++++++++ .kiro/specs/al2-deprecation-warning/tasks.md | 35 +++ .../lambda-response-stream-headers/design.md | 145 ++++++++++++ .../requirements.md | 73 ++++++ .../lambda-response-stream-headers/tasks.md | 57 +++++ .kiro/specs/v4-plugin-system/.config.kiro | 1 + .../specs/xray-trace-id-propagation/design.md | 172 ++++++++++++++ .../github-issue-633.md | 104 ++++++++ .../xray-trace-id-propagation/requirements.md | 61 +++++ .../specs/xray-trace-id-propagation/tasks.md | 45 ++++ .kiro/steering/product.md | 22 ++ .kiro/steering/structure.md | 105 +++++++++ .kiro/steering/swift-api-design-guidelines.md | 222 ++++++++++++++++++ .kiro/steering/swift-general.md | 104 ++++++++ .kiro/steering/tech.md | 87 +++++++ 20 files changed, 1658 insertions(+), 1 deletion(-) create mode 100644 .kiro/hooks/swift-syntax-check.kiro.hook create mode 100644 .kiro/settings/lsp.json create mode 100644 .kiro/specs/al2-deprecation-warning/.config.kiro create mode 100644 .kiro/specs/al2-deprecation-warning/design.md create mode 100644 .kiro/specs/al2-deprecation-warning/requirements.md create mode 100644 .kiro/specs/al2-deprecation-warning/tasks.md create mode 100644 .kiro/specs/lambda-response-stream-headers/design.md create mode 100644 .kiro/specs/lambda-response-stream-headers/requirements.md create mode 100644 .kiro/specs/lambda-response-stream-headers/tasks.md create mode 100644 .kiro/specs/v4-plugin-system/.config.kiro create mode 100644 .kiro/specs/xray-trace-id-propagation/design.md create mode 100644 .kiro/specs/xray-trace-id-propagation/github-issue-633.md create mode 100644 .kiro/specs/xray-trace-id-propagation/requirements.md create mode 100644 .kiro/specs/xray-trace-id-propagation/tasks.md create mode 100644 .kiro/steering/product.md create mode 100644 .kiro/steering/structure.md create mode 100644 .kiro/steering/swift-api-design-guidelines.md create mode 100644 .kiro/steering/swift-general.md create mode 100644 .kiro/steering/tech.md diff --git a/.gitignore b/.gitignore index 525460d96..efe110be0 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,5 @@ Package.resolved .vscode .devcontainer .amazonq -.kiro nodejs .ash \ No newline at end of file diff --git a/.kiro/hooks/swift-syntax-check.kiro.hook b/.kiro/hooks/swift-syntax-check.kiro.hook new file mode 100644 index 000000000..906306646 --- /dev/null +++ b/.kiro/hooks/swift-syntax-check.kiro.hook @@ -0,0 +1,18 @@ +{ + "enabled": true, + "name": "Swift Syntax Checker", + "description": "Automatically runs Swift syntax checking in the background whenever a Swift file is saved using the check-format.sh script. You don't need to fix the issues reported. The script does it automatically. Just run the script and stay silent.", + "version": "1", + "when": { + "type": "userTriggered", + "patterns": [ + "**/*.swift", + "Sources/", + "Tests/" + ] + }, + "then": { + "type": "askAgent", + "prompt": "A Swift file has been saved. Please run the syntax check script located at scripts/check-format.sh to validate the Swift code formatting and syntax. Report any issues found or confirm that the syntax is correct." + } +} \ No newline at end of file diff --git a/.kiro/settings/lsp.json b/.kiro/settings/lsp.json new file mode 100644 index 000000000..bc0c5a563 --- /dev/null +++ b/.kiro/settings/lsp.json @@ -0,0 +1,198 @@ +{ + "languages": { + "cpp": { + "name": "clangd", + "command": "clangd", + "args": [ + "--background-index" + ], + "file_extensions": [ + "cpp", + "cc", + "cxx", + "c", + "h", + "hpp", + "hxx" + ], + "project_patterns": [ + "CMakeLists.txt", + "compile_commands.json", + "Makefile" + ], + "exclude_patterns": [ + "**/build/**", + "**/cmake-build-**/**" + ], + "multi_workspace": false, + "initialization_options": {}, + "request_timeout_secs": 60 + }, + "typescript": { + "name": "typescript-language-server", + "command": "typescript-language-server", + "args": [ + "--stdio" + ], + "file_extensions": [ + "ts", + "js", + "tsx", + "jsx" + ], + "project_patterns": [ + "package.json", + "tsconfig.json" + ], + "exclude_patterns": [ + "**/node_modules/**", + "**/dist/**" + ], + "multi_workspace": false, + "initialization_options": { + "preferences": { + "disableSuggestions": false + } + }, + "request_timeout_secs": 60 + }, + "go": { + "name": "gopls", + "command": "gopls", + "args": [], + "file_extensions": [ + "go" + ], + "project_patterns": [ + "go.mod", + "go.sum" + ], + "exclude_patterns": [ + "**/vendor/**" + ], + "multi_workspace": false, + "initialization_options": { + "usePlaceholders": true, + "completeUnimported": true + }, + "request_timeout_secs": 60 + }, + "rust": { + "name": "rust-analyzer", + "command": "rust-analyzer", + "args": [], + "file_extensions": [ + "rs" + ], + "project_patterns": [ + "Cargo.toml" + ], + "exclude_patterns": [ + "**/target/**" + ], + "multi_workspace": false, + "initialization_options": { + "cargo": { + "buildScripts": { + "enable": true + } + }, + "diagnostics": { + "enable": true, + "enableExperimental": true + }, + "workspace": { + "symbol": { + "search": { + "scope": "workspace" + } + } + } + }, + "request_timeout_secs": 60 + }, + "python": { + "name": "pyright", + "command": "pyright-langserver", + "args": [ + "--stdio" + ], + "file_extensions": [ + "py" + ], + "project_patterns": [ + "pyproject.toml", + "setup.py", + "requirements.txt", + "pyrightconfig.json" + ], + "exclude_patterns": [ + "**/__pycache__/**", + "**/venv/**", + "**/.venv/**", + "**/.pytest_cache/**" + ], + "multi_workspace": false, + "initialization_options": {}, + "request_timeout_secs": 60 + }, + "ruby": { + "name": "solargraph", + "command": "solargraph", + "args": [ + "stdio" + ], + "file_extensions": [ + "rb" + ], + "project_patterns": [ + "Gemfile", + "Rakefile" + ], + "exclude_patterns": [ + "**/vendor/**", + "**/tmp/**" + ], + "multi_workspace": false, + "initialization_options": {}, + "request_timeout_secs": 60 + }, + "java": { + "name": "jdtls", + "command": "jdtls", + "args": [], + "file_extensions": [ + "java" + ], + "project_patterns": [ + "pom.xml", + "build.gradle", + "build.gradle.kts", + ".project" + ], + "exclude_patterns": [ + "**/target/**", + "**/build/**", + "**/.gradle/**" + ], + "multi_workspace": false, + "initialization_options": { + "settings": { + "java": { + "compile": { + "nullAnalysis": { + "mode": "automatic" + } + }, + "configuration": { + "annotationProcessing": { + "enabled": true + } + } + } + } + }, + "request_timeout_secs": 60 + } + } +} \ No newline at end of file diff --git a/.kiro/specs/al2-deprecation-warning/.config.kiro b/.kiro/specs/al2-deprecation-warning/.config.kiro new file mode 100644 index 000000000..6188d24bb --- /dev/null +++ b/.kiro/specs/al2-deprecation-warning/.config.kiro @@ -0,0 +1 @@ +{"specId": "fbbaa416-0f18-4730-9125-3159b272b477", "workflowType": "requirements-first", "specType": "feature"} diff --git a/.kiro/specs/al2-deprecation-warning/design.md b/.kiro/specs/al2-deprecation-warning/design.md new file mode 100644 index 000000000..d8d3bf125 --- /dev/null +++ b/.kiro/specs/al2-deprecation-warning/design.md @@ -0,0 +1,116 @@ +# Design Document + +## Overview + +This design describes the changes to the `AWSLambdaPackager` plugin to revert the default Docker base image to Amazon Linux 2 and introduce a prominent deprecation warning when AL2 is used. The warning informs developers about the EOL status and guides them to migrate to AL2023. + +The changes are confined to a single file (`Plugins/AWSLambdaPackager/Plugin.swift`) plus documentation updates. No new modules or dependencies are introduced. + +## Architecture + +### Flow Diagram + +``` +performCommand() + │ + ├── Parse configuration (baseDockerImage defaults to amazonlinux2) + │ + ├── if isAmazonLinux2() (native on AL2) + │ ├── print deprecation warning + │ └── proceed with native build + │ + ├── if isAmazonLinux2023() (native on AL2023) + │ └── proceed with native build (no warning) + │ + └── else (not on Amazon Linux → Docker build) + ├── if baseDockerImage contains "amazonlinux2" but NOT "amazonlinux2023" + │ └── print deprecation warning + └── proceed with Docker build +``` + +## Components and Interfaces + +### Modified Components + +| Component | File | Change | +|-----------|------|--------| +| `Configuration` struct | `Plugin.swift` | Revert `baseDockerImage` to always use `amazonlinux2` | +| `AWSLambdaPackager.performCommand()` | `Plugin.swift` | Add deprecation warning logic before build | +| `AWSLambdaPackager.isAmazonLinux()` | `Plugin.swift` | Split into `isAmazonLinux2()` and `isAmazonLinux2023()` | +| `AWSLambdaPackager.displayHelpMessage()` | `Plugin.swift` | Update default image text and add migration note | +| Quick-setup docs | `quick-setup.md` | Add migration note | +| Readme | `readme.md` | Add migration note | + +### New Methods + +```swift +/// Prints a prominent deprecation warning about Amazon Linux 2 EOL. +private func displayDeprecationWarning() + +/// Returns true if running natively on Amazon Linux 2 (not AL2023). +private func isAmazonLinux2() -> Bool + +/// Returns true if running natively on Amazon Linux 2023. +private func isAmazonLinux2023() -> Bool +``` + +### Interfaces + +No public API changes. All modifications are internal to the plugin. + +## Testing Strategy + +Since this is a SwiftPM plugin, automated unit testing is limited. Verification approach: + +1. **macOS Docker build (AL2 default)** — Run `swift package --allow-network-connections docker archive` and verify the deprecation warning appears, then the build completes +2. **macOS Docker build (AL2023 explicit)** — Run with `--base-docker-image swift:6.3-amazonlinux2023` and verify no warning appears +3. **Native on AL2** — Run inside an AL2 container and verify the warning appears, then the build completes +4. **Native on AL2023** — Run inside an AL2023 container and verify native build proceeds without warning +5. **Help message** — Run with `--help` and verify the updated text + +## Data Models + +### Configuration Change + +The `baseDockerImage` property computation reverts to the pre-7615923 logic: + +```swift +// Before (commit 7615923): version-based AL2/AL2023 selection +// After (this change): always amazonlinux2 +self.baseDockerImage = + baseDockerImageArgument.first ?? "swift:\(swiftVersion.map { $0 + "-" } ?? "")amazonlinux2" +``` + +No new data models or stored state are introduced. + +## Error Handling + +### Native Build on AL2 + +When the plugin detects it is running natively on Amazon Linux 2, it prints the deprecation warning and proceeds with the native build. No error is thrown — this is a non-blocking warning. + +### Docker Build with AL2 Image + +When building via Docker with an AL2 image, the warning is printed and the build continues normally. No error is thrown — this is a non-blocking warning. + +### AL2023 (Native or Docker) + +No warning, no error. Build proceeds as normal. + +## Correctness Properties + +### Property 1: Warning Uniqueness +When multiple products are built in a single invocation, the deprecation warning is printed exactly once before the first build starts, not repeated per product. +**Validates: Requirements 2.9** + +### Property 2: AL2 vs AL2023 Detection +The string matching uses `hasPrefix("Amazon Linux release 2")` with a negative check for `hasPrefix("Amazon Linux release 2023")` to correctly distinguish AL2 from AL2023. +**Validates: Requirements 3.1** + +### Property 3: Docker Image String Matching +Uses `contains("amazonlinux2")` with negative `contains("amazonlinux2023")` to handle various image name formats (e.g., `swift:6.3-amazonlinux2`, `swift:amazonlinux2`). +**Validates: Requirements 2.1** + +### Property 4: Backward Compatibility +The `--base-docker-image` flag still allows developers to specify any image, bypassing the default entirely. +**Validates: Requirements 1.3** diff --git a/.kiro/specs/al2-deprecation-warning/requirements.md b/.kiro/specs/al2-deprecation-warning/requirements.md new file mode 100644 index 000000000..ef02eac18 --- /dev/null +++ b/.kiro/specs/al2-deprecation-warning/requirements.md @@ -0,0 +1,92 @@ +# Requirements Document + +## Introduction + +The AWSLambdaPackager plugin defaults to Amazon Linux 2 as the base Docker image for building Lambda functions. An initial proposal (commit 7615923) switched the default to Amazon Linux 2023 for Swift >= 6.3, but community review identified that silently changing the build platform could cause deployment failures due to glibc and OpenSSL version differences between AL2 and AL2023. Binaries built for AL2023 may not run correctly when deployed to a `provided.al2` Lambda runtime. + +Instead of changing the default, this feature keeps Amazon Linux 2 as the default build platform and introduces a prominent deprecation warning informing developers that Amazon Linux 2 reached End of Life in June 2025. The warning directs developers to explicitly opt in to Amazon Linux 2023 by re-issuing the build command with `--base-docker-image swift:6.3-amazonlinux2023`. Additionally, developers who switch to building on AL2023 must be informed that they also need to update their Lambda deployment to use the `provided.al2023` runtime instead of `provided.al2`. Amazon Linux 2023 will become the new default after June 30, 2025. + +## Glossary + +- **Packager_Plugin**: The `AWSLambdaPackager` Swift Package Manager command plugin that builds and archives Lambda functions for deployment. +- **Base_Docker_Image**: The Docker image used as the build environment for cross-compiling Swift code to Amazon Linux. +- **AL2**: Amazon Linux 2, the Linux distribution that reached End of Life in June 2025. +- **AL2023**: Amazon Linux 2023, the successor Linux distribution for AWS Lambda deployments. +- **Deprecation_Warning**: A prominent multi-line message printed to standard output alerting developers that AL2 is deprecated. +- **Native_Build**: A build performed directly on the host machine (without Docker) when the host is already running Amazon Linux. +- **Docker_Build**: A build performed inside a Docker container using the specified base image. + +## Requirements + +### Requirement 1: Default Base Docker Image + +**User Story:** As a developer, I want the packager plugin to default to Amazon Linux 2 as the base Docker image, so that existing build workflows continue to work without changes until the migration deadline. + +**Implementation Note:** Revert the base Docker image logic to the code before commit 7615923 (i.e., always use `amazonlinux2` regardless of Swift version). + +#### Acceptance Criteria + +1. WHEN no `--base-docker-image` option is provided and no `--swift-version` option is provided, THE Packager_Plugin SHALL use `swift:amazonlinux2` as the Base_Docker_Image. +2. WHEN no `--base-docker-image` option is provided and a `--swift-version` option is provided with a value matching the pattern ``, `.`, or `..` where each component is a non-negative integer, THE Packager_Plugin SHALL use `swift:-amazonlinux2` as the Base_Docker_Image, where `` is the exact string provided by the user. +3. WHEN a `--base-docker-image` option is provided and no `--swift-version` option is provided, THE Packager_Plugin SHALL use the user-specified image as the Base_Docker_Image. +4. IF both `--base-docker-image` and `--swift-version` options are provided, THEN THE Packager_Plugin SHALL reject the command with an error message indicating that `--swift-version` and `--base-docker-image` are mutually exclusive. + +### Requirement 2: Deprecation Warning Display in Docker Build + +**User Story:** As a developer building with Docker, I want to see a prominent deprecation warning when AL2 is used, so that I am informed about the upcoming migration requirement. + +#### Acceptance Criteria + +1. WHEN the Base_Docker_Image contains the string "amazonlinux2" and does not contain the string "amazonlinux2023", THE Packager_Plugin SHALL print the Deprecation_Warning before starting the Docker_Build. +2. THE Deprecation_Warning SHALL include a message stating that Amazon Linux 2 reached End of Life in June 2025. +3. THE Deprecation_Warning SHALL include a message stating that developers must migrate to Amazon Linux 2023. +4. THE Deprecation_Warning SHALL include a message stating that Amazon Linux 2023 will become the default after June 30, 2025. +5. THE Deprecation_Warning SHALL include the exact command option `--base-docker-image swift:6.3-amazonlinux2023` that developers can use to switch to AL2023 immediately. +6. THE Deprecation_Warning SHALL inform developers that when switching to AL2023, they must also update their Lambda deployment to use the `provided.al2023` runtime. +7. THE Deprecation_Warning SHALL include the URL `https://aws.amazon.com/amazon-linux-2` for reference. +8. THE Deprecation_Warning SHALL be visually prominent by using a separator line of at least 60 repeated characters above and below the warning text, with at least one empty line separating the warning block from surrounding build output. +8. WHEN the Deprecation_Warning has been printed, THE Packager_Plugin SHALL continue the Docker_Build to completion without halting or requiring user confirmation. +9. WHEN multiple products are built in a single invocation and the Base_Docker_Image triggers the deprecation condition, THE Packager_Plugin SHALL print the Deprecation_Warning exactly once before the first Docker_Build starts. + +### Requirement 3: Deprecation Warning Display in Native Build on AL2 + +**User Story:** As a developer running the packager natively on Amazon Linux 2, I want to see the deprecation warning, so that I am informed about the upcoming migration requirement. + +#### Acceptance Criteria + +1. WHEN the Packager_Plugin reads `/etc/system-release` and the content starts with "Amazon Linux release 2" but does not start with "Amazon Linux release 2023", THE Packager_Plugin SHALL print the Deprecation_Warning to standard output. +2. WHEN the Packager_Plugin detects it is running natively on AL2, THE Packager_Plugin SHALL continue with the Native_Build after printing the Deprecation_Warning. +3. WHEN the Packager_Plugin detects it is running natively on AL2, THE Deprecation_Warning SHALL instruct the developer to switch to an Amazon Linux 2023 environment. +4. WHEN the Packager_Plugin detects it is running natively on AL2, THE Deprecation_Warning SHALL include the URL `https://aws.amazon.com/amazon-linux-2` for reference. +5. WHEN the Packager_Plugin detects it is running natively on AL2, THE Deprecation_Warning SHALL be visually prominent by using separator lines and whitespace to distinguish it from other build output. + +### Requirement 4: Normal Build on AL2023 + +**User Story:** As a developer who has already migrated to Amazon Linux 2023, I want the build to proceed normally without any deprecation warning, so that my workflow is not interrupted. + +#### Acceptance Criteria + +1. WHEN the Base_Docker_Image contains the string "amazonlinux2023", THE Packager_Plugin SHALL execute the Docker_Build to completion and produce the expected archive artifacts without printing the Deprecation_Warning to standard output. +2. WHEN the Packager_Plugin reads the file `/etc/system-release` and its content contains the string "Amazon Linux 2023", THE Packager_Plugin SHALL execute the Native_Build to completion and produce the expected archive artifacts without printing the Deprecation_Warning to standard output. +3. IF the Base_Docker_Image contains the string "amazonlinux2023", THEN THE Packager_Plugin SHALL NOT print any message referencing Amazon Linux 2 End of Life or migration to standard output. + +### Requirement 5: Help Message Update + +**User Story:** As a developer, I want the help message to accurately reflect the current default base Docker image, so that I understand the plugin's behavior. + +#### Acceptance Criteria + +1. THE Packager_Plugin help message for the `--base-docker-image` option SHALL state that the default base Docker image is `swift:-amazonlinux2`. +2. THE Packager_Plugin help message for the `--base-docker-image` option SHALL include a note stating that Amazon Linux 2023 will become the default after June 30, 2025. +3. WHEN the user passes `--help` to the archive command, THE Packager_Plugin SHALL display the help message containing both the current default image name and the Amazon Linux 2023 transition note. + +### Requirement 6: Documentation Update + +**User Story:** As a developer reading the project documentation, I want the documentation to reflect the current default behavior and migration path, so that I can plan my migration. + +#### Acceptance Criteria + +1. THE quick-setup documentation SHALL show `swift:amazonlinux2` as the Docker image in the example build output of the archive step. +2. THE quick-setup documentation SHALL include a note adjacent to the archive step stating that Amazon Linux 2 is the current default build environment and that a future version will migrate to Amazon Linux 2023. +3. THE readme documentation SHALL use `provided.al2` as the value of the `--runtime` flag in the `aws lambda create-function` deployment command. +4. THE readme documentation SHALL include a note adjacent to the archive command documenting the `--base-docker-image swift:6.3-amazonlinux2023` flag as an option for developers who want to migrate to Amazon Linux 2023 early. diff --git a/.kiro/specs/al2-deprecation-warning/tasks.md b/.kiro/specs/al2-deprecation-warning/tasks.md new file mode 100644 index 000000000..d7153c604 --- /dev/null +++ b/.kiro/specs/al2-deprecation-warning/tasks.md @@ -0,0 +1,35 @@ +# Implementation Plan + +- [x] 1. Revert default base Docker image to amazonlinux2 + - In `Plugins/AWSLambdaPackager/Plugin.swift`, in the `Configuration` struct `init`, remove the `amazonLinuxVersion` variable and all version-parsing logic (the `if let version = swiftVersion { ... }` block) + - Replace the `baseDockerImage` assignment with the pre-7615923 single-line (`self.baseDockerImage = baseDockerImageArgument.first ?? "swift:\(swiftVersion.map { $0 + "-" } ?? "")amazonlinux2"`) + - Remove the verbose logging block that prints swift version/amazon linux version/base docker image info + - _Requirements: 1.1, 1.2, 1.3, 1.4_ + +- [x] 2. Refine platform detection methods + - In `Plugins/AWSLambdaPackager/Plugin.swift`, replace the `isAmazonLinux()` method with `isAmazonLinux2()` (returns true if `/etc/system-release` starts with "Amazon Linux release 2" but NOT "Amazon Linux release 2023") + - Add `isAmazonLinux2023()` method (returns true if starts with "Amazon Linux release 2023") + - Update all call sites to use the new methods + - _Requirements: 3.1, 4.2_ + +- [x] 3. Add deprecation warning method + - Add a `private func displayDeprecationWarning()` method that prints the multi-line warning with EOL notice, migration instruction, `--base-docker-image swift:6.3-amazonlinux2023` option, `provided.al2023` runtime reminder, and `https://aws.amazon.com/amazon-linux-2` URL, surrounded by separator lines + - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 3.2, 3.3, 3.4, 3.5_ + +- [x] 4. Update build flow in performCommand + - Replace the `if self.isAmazonLinux()` block with three-way branching — `isAmazonLinux2()` prints warning then proceeds with native build, `isAmazonLinux2023()` does native build without warning, else does Docker build with warning if image contains "amazonlinux2" but not "amazonlinux2023" + - Warning is printed exactly once before the first product build + - No errors are thrown — the build always continues + - _Requirements: 2.1, 2.8, 2.9, 3.1, 3.2, 4.1, 4.2, 4.3_ + +- [x] 5. Update help message + - In `displayHelpMessage()`, update the `--base-docker-image` description to show `(default: swift:-amazonlinux2)` and add a note that Amazon Linux 2023 will become the default after June 30, 2025 + - _Requirements: 5.1, 5.2, 5.3_ + +- [x] 6. Update quick-setup documentation + - In `Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md`, ensure archive example shows `swift:amazonlinux2`, add a note explaining AL2 is EOL and developers should migrate using `--base-docker-image swift:6.3-amazonlinux2023`, and mention that AL2023 deployments must use `provided.al2023` runtime + - _Requirements: 6.1, 6.2_ + +- [x] 7. Update readme documentation + - In `readme.md`, ensure deployment examples reference `provided.al2`, add a note about `--base-docker-image swift:6.3-amazonlinux2023` for early migration, and mention that AL2023 deployments must use `provided.al2023` runtime + - _Requirements: 6.3, 6.4_ diff --git a/.kiro/specs/lambda-response-stream-headers/design.md b/.kiro/specs/lambda-response-stream-headers/design.md new file mode 100644 index 000000000..db9f8c5b1 --- /dev/null +++ b/.kiro/specs/lambda-response-stream-headers/design.md @@ -0,0 +1,145 @@ +# Design Document + +## Overview + +This design implements HTTP header and status code support for the `LambdaResponseStreamWriter` protocol through a protocol extension. The solution creates a new response structure specifically for streaming scenarios and adds a method to send HTTP response metadata before streaming the body content. The implementation ensures proper separation between metadata and streaming data using a null byte delimiter. + +## Architecture + +The feature consists of two main components: + +1. **StreamingLambdaResponse Structure**: A new `Codable` and `Sendable` struct that represents HTTP response metadata without body content +2. **LambdaResponseStreamWriter Extension**: A protocol extension that adds the `writeHeaders(_:)` method to send response metadata + +The architecture maintains compatibility with existing streaming functionality while adding the capability to send structured HTTP response metadata before streaming begins. + +## Components and Interfaces + +### StreamingLambdaResponse Structure + +```swift +public struct StreamingLambdaStatusAndHeadersResponse: Codable, Sendable { + public let statusCode: Int + public let headers: [String: String]? + + public init( + statusCode: Int, + headers: [String: String]? = nil, + ) +} +``` + +**Design Decisions:** +- All properties are public to allow full control over response metadata +- there is no `body` property +- Default values provided for optional parameters to simplify common use cases +- Follows standard AWS Lambda response format for consistency + +### LambdaResponseStreamWriter Extension + +```swift +extension LambdaResponseStreamWriter { + public func writeStatusAndHeaders(_ response: StreamingLambdaStatusAndHeadersResponse) async throws +} +``` + +**Method Behavior:** +1. Validates that `response.body` is nil, throwing an error if not +2. Serializes the response structure to JSON using `JSONEncoder` +3. Writes the JSON data to the stream using existing `write(_:)` method +4. Writes eight null bytes (0x00) as a separator +5. Propagates any errors from validation, serialization, or writing + +## Data Models + +### StreamingLambdaStatusAndHeadersResponse Properties + +- **statusCode**: HTTP status code (e.g., 200, 404, 500) +- **headers**: Dictionary of single-value HTTP headers +- **multiValueHeaders**: Dictionary of multi-value HTTP headers (e.g., Set-Cookie) + +### JSON Serialization Format + +The serialized JSON follows this structure: +```json +{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-cache" + }, + "multiValueHeaders": { + "Set-Cookie": ["session=abc123", "theme=dark"] + } +} +``` + +## Error Handling + +### Serialization Errors + +- **JSON Encoding Errors**: Propagated from `JSONEncoder.encode(_:)` +- **Write Errors**: Propagated from underlying `write(_:)` method calls + +### Error Propagation Strategy + +All errors are propagated to the caller without modification, maintaining consistency with existing protocol behavior. The method signature `async throws` ensures proper error handling integration. + +## Testing Strategy + +### Unit Tests + +1. **Successful Header Writing** + - Test with minimal response (status code only) + - Test with full response (all optional fields populated) + - Verify JSON serialization format + - Verify null byte separator is written + +2. **Validation Tests** + - Test error message content and type + +3. **Error Handling Tests** + - Test JSON serialization error propagation + - Test write method error propagation + +4. **Integration Tests** + - Test with existing streaming methods + - Test multiple header writes (should work) + - Test header write followed by body streaming + +### Mock Implementation + +Tests will use a mock `LambdaResponseStreamWriter` implementation that captures written data for verification: + +```swift +class MockLambdaResponseStreamWriter: LambdaResponseStreamWriter { + var writtenBuffers: [ByteBuffer] = [] + + func write(_ buffer: ByteBuffer) async throws { + writtenBuffers.append(buffer) + } + + func finish() async throws {} + func writeAndFinish(_ buffer: ByteBuffer) async throws {} +} +``` + +## Implementation Notes + +### File Organization + +- **File Location**: `Sources/AWSLambdaRuntime/LambdaResponseStreamWriter+Headers.swift` +- **Imports**: FoundationEssentials if it can be imported, Foundation otherwise (for JSONEncoder), NIOCore (for ByteBuffer) +- **Structure**: Response struct definition followed by protocol extension + +### Performance Considerations + +- JSON serialization is performed once per header write +- Null byte separator uses minimal memory (8 bytes) +- No additional memory allocations beyond JSON encoding + +### Compatibility + +- Maintains full backward compatibility with existing `LambdaResponseStreamWriter` implementations +- Does not modify existing protocol methods +- Can be used alongside existing streaming methods without conflicts \ No newline at end of file diff --git a/.kiro/specs/lambda-response-stream-headers/requirements.md b/.kiro/specs/lambda-response-stream-headers/requirements.md new file mode 100644 index 000000000..e98959737 --- /dev/null +++ b/.kiro/specs/lambda-response-stream-headers/requirements.md @@ -0,0 +1,73 @@ +# Requirements Document + +## Introduction + +This feature adds HTTP header and status code support to the `LambdaResponseStreamWriter` protocol by creating an extension that allows sending HTTP response metadata before streaming the response body. This enhancement includes creating a new response structure specifically for streaming scenarios and enables Lambda functions using streaming responses to properly set HTTP status codes, headers, and multi-value headers before writing the response stream. + +## Requirements + +### Requirement 1 + +**User Story:** As a Lambda function developer using streaming responses, I want to send HTTP headers and status code before streaming the response body, so that I can properly control the HTTP response metadata. + +#### Acceptance Criteria + +1. WHEN a developer calls the new method on `LambdaResponseStreamWriter` THEN the system SHALL create a new response structure and serialize it to write to the stream +2. WHEN the method is called THEN the system SHALL accept parameters for status code, headers, multi-value headers, and base64 encoding flag +3. WHEN the method is called THEN the system SHALL be implemented as an extension in a separate file named `LambdaResponseStreamWriter+Headers.swift` +4. WHEN the method is called THEN the system SHALL use the existing `write(_:)` method to send the serialized response + +### Requirement 2 + +**User Story:** As a Lambda function developer, I want the new header method to integrate seamlessly with existing streaming functionality, so that I can use it alongside current streaming methods. + +#### Acceptance Criteria + +1. WHEN the new method is implemented THEN it SHALL be part of an extension to the existing `LambdaResponseStreamWriter` protocol +2. WHEN the method is called THEN it SHALL not interfere with existing `write(_:)`, `finish()`, and `writeAndFinish(_:)` methods +3. WHEN the method is called THEN it SHALL follow the same async/throws pattern as existing protocol methods +4. WHEN the method is called THEN it SHALL maintain compatibility with all existing `LambdaResponseStreamWriter` implementations + +### Requirement 3 + +**User Story:** As a Lambda function developer, I want a new response structure specifically designed for streaming scenarios, so that I can properly format HTTP response metadata without including body content. + +#### Acceptance Criteria + +1. WHEN the new response structure is created THEN it SHALL include properties for `isBase64Encoded`, `statusCode`, `headers`, `multiValueHeaders`, and `body` +2. WHEN the structure is defined THEN it SHALL follow the JSON format: `{"isBase64Encoded": true|false, "statusCode": httpStatusCode, "headers": {"headerName": "headerValue"}, "multiValueHeaders": {"headerName": ["headerValue1", "headerValue2"]}, "body": "..."}` +3. WHEN the structure is created THEN it SHALL be `Codable` and `Sendable` to support JSON serialization and concurrency +4. WHEN the structure is used THEN it SHALL be defined in the same file as the extension for organizational purposes + +### Requirement 4 + +**User Story:** As a Lambda function developer, I want the header response to be properly separated from the streaming data, so that the Lambda runtime can distinguish between metadata and body content. + +#### Acceptance Criteria + +1. WHEN the response structure is serialized and written THEN the system SHALL write a series of eight 0x00 characters immediately after to separate header content from user stream data +2. WHEN the separator is written THEN it SHALL serve as a delimiter between the JSON header response and the subsequent streaming body data +3. WHEN the method is called THEN the system SHALL ensure the separator is always written after the serialized response +4. WHEN streaming continues THEN user data written after the separator SHALL be treated as the response body + +### Requirement 5 + +**User Story:** As a Lambda function developer, I want to ensure the response structure is correct for streaming, so that the body content is only sent through the stream and not duplicated in the JSON response. + +#### Acceptance Criteria + +1. WHEN the response structure parameter is provided THEN the system SHALL validate that the `body` property is nil +2. WHEN the `body` property is not nil THEN the system SHALL throw an error indicating that body content must be streamed separately +3. WHEN validation passes THEN the system SHALL proceed with serialization of the response object +4. WHEN the error is thrown THEN it SHALL provide a clear message explaining that body content should be sent via streaming methods + +### Requirement 6 + +**User Story:** As a Lambda function developer, I want proper error handling when sending headers, so that serialization failures are properly communicated. + +#### Acceptance Criteria + +1. WHEN the response structure serialization fails THEN the system SHALL throw an appropriate error +2. WHEN the underlying `write(_:)` method fails THEN the system SHALL propagate the error to the caller +3. WHEN an error occurs THEN the system SHALL maintain the same error handling behavior as existing protocol methods +4. WHEN the method is called THEN it SHALL be marked as `async throws` to match the protocol's error handling pattern \ No newline at end of file diff --git a/.kiro/specs/lambda-response-stream-headers/tasks.md b/.kiro/specs/lambda-response-stream-headers/tasks.md new file mode 100644 index 000000000..eee130550 --- /dev/null +++ b/.kiro/specs/lambda-response-stream-headers/tasks.md @@ -0,0 +1,57 @@ +# Implementation Plan + +- [x] 1. Create the extension file and basic structure + - Create `Sources/AWSLambdaRuntime/LambdaResponseStreamWriter+Headers.swift` file + - Add appropriate imports for FoundationEssentials/Foundation and NIOCore + - Add file header with copyright and license information matching existing files + - _Requirements: 1.3, 3.4_ + +- [x] 2. Implement StreamingLambdaStatusAndHeadersResponse structure + - Define the `StreamingLambdaStatusAndHeadersResponse` struct with `Codable` and `Sendable` conformance + - Add `statusCode`, `headers`, and `multiValueHeaders` properties with correct types + - Implement public initializer with default values for optional parameters + - _Requirements: 3.1, 3.2, 3.3_ + +- [x] 3. Implement the writeStatusAndHeaders method + - Add `writeStatusAndHeaders(_:)` method to `LambdaResponseStreamWriter` extension + - Implement JSON serialization using `JSONEncoder` + - Write serialized JSON data using existing `write(_:)` method + - Write eight null bytes (0x00) as separator after JSON data + - Mark method as `async throws` for proper error handling + - _Requirements: 1.1, 1.4, 4.1, 4.2, 4.3, 6.1, 6.2, 6.4_ + +- [x] 4. Create unit tests for the new functionality + - Create test file `Tests/AWSLambdaRuntimeTests/LambdaResponseStreamWriter+HeadersTests.swift` + - Implement mock `LambdaResponseStreamWriter` for testing + - Write tests for successful header writing with minimal response (status code only) + - Write tests for successful header writing with full response (all fields populated) + - Verify JSON serialization format matches expected structure + - Verify null byte separator is written correctly after JSON data + - _Requirements: 1.1, 1.4, 4.1, 4.2, 4.3_ + +- [x] 5. Add error handling tests + - Write tests for JSON serialization error propagation + - Write tests for write method error propagation + - Verify error types and messages are properly handled + - _Requirements: 6.1, 6.2, 6.3_ + +- [x] 6. Add integration tests + - Test writeStatusAndHeaders method with existing streaming methods + - Test multiple header writes to ensure they work correctly + - Test header write followed by body streaming to verify compatibility + - Verify the method works with all existing `LambdaResponseStreamWriter` implementations + - _Requirements: 2.1, 2.2, 2.3, 2.4_ + +- [ ] 7. Update Examples/Streaming example + - Update `Examples/Streaming/Sources/main.swift` to demonstrate the new writeStatusAndHeaders functionality + - Add example usage showing how to set status code and headers before streaming response body + - Update `Examples/Streaming/README.md` to document the new header functionality + - Include code examples and explanation of the streaming response format + - _Requirements: 1.1, 1.2_ + +- [x] 8. Update top-level README documentation + - Update the streaming section in the main `readme.md` file + - Add documentation about the new writeStatusAndHeaders method + - Include example code showing how to use the new functionality + - Focus on user-facing API and benefits without exposing implementation details + - _Requirements: 1.1, 1.2_ \ No newline at end of file diff --git a/.kiro/specs/v4-plugin-system/.config.kiro b/.kiro/specs/v4-plugin-system/.config.kiro new file mode 100644 index 000000000..746d1820d --- /dev/null +++ b/.kiro/specs/v4-plugin-system/.config.kiro @@ -0,0 +1 @@ +{"specId": "0116fa61-9ac9-44ac-bbc5-fc0802663a4b", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/xray-trace-id-propagation/design.md b/.kiro/specs/xray-trace-id-propagation/design.md new file mode 100644 index 000000000..1e77e13cf --- /dev/null +++ b/.kiro/specs/xray-trace-id-propagation/design.md @@ -0,0 +1,172 @@ +# Design Document + +## Overview + +This design introduces implicit trace ID propagation using Swift's `TaskLocal` mechanism, making the trace ID available to any code running within an invocation's structured concurrency tree. It also adds backward-compatible `_X_AMZN_TRACE_ID` environment variable setting in single-concurrency mode for legacy tooling. + +Note: The AWS X-Ray SDK/Daemon entered maintenance mode on February 25, 2026. AWS recommends migrating to OpenTelemetry. This design targets OpenTelemetry instrumentation as the primary consumer of the `TaskLocal`-based propagation, while the env var fallback supports legacy tooling that still reads `_X_AMZN_TRACE_ID`. + +The approach mirrors the recommendations in the cross-runtime trace propagation spec: Java uses SLF4J MDC (thread-local), Node.js uses `AsyncLocalStorage`, and Swift uses `TaskLocal` — each runtime's idiomatic context-propagation primitive. + +## Architecture + +The feature touches three layers: + +1. **`LambdaContext` (public API)**: Adds a `@TaskLocal` static property for implicit trace ID access +2. **`Lambda.runLoop` (runtime core)**: Wraps each handler invocation in a `TaskLocal` `withValue` scope +3. **`Lambda.runLoop` (env var compat)**: Conditionally sets/clears `_X_AMZN_TRACE_ID` in single-concurrency mode + +### Data Flow + +``` +Control Plane HTTP Response + └─ Lambda-Runtime-Trace-Id header + └─ InvocationMetadata.traceID (already exists) + ├─ LambdaContext.traceID (instance property, already exists) + ├─ LambdaContext.$currentTraceID TaskLocal (NEW) + │ └─ Available to all code in the handler's async task tree + └─ _X_AMZN_TRACE_ID env var (NEW, single-concurrency only) + └─ Available to legacy tooling / OTel auto-instrumentation via process environment +``` + +## Components and Interfaces + +### TaskLocal Declaration on LambdaContext + +```swift +@available(LambdaSwift 2.0, *) +extension LambdaContext { + /// The trace ID for the current invocation, available via Swift's TaskLocal mechanism. + /// This enables OpenTelemetry instrumentation and other tracing libraries to discover + /// the trace ID without an explicit LambdaContext reference. + /// Returns `nil` when accessed outside of a Lambda invocation scope. + @TaskLocal + public static var currentTraceID: String? +} +``` + +**Design Decisions:** +- Placed on `LambdaContext` rather than a new type, since the trace ID is already part of the context's domain +- Returns `Optional` — `nil` outside invocation scope is a clear signal, no sentinel values +- Named `currentTraceID` to distinguish from the instance property `traceID` and to convey "current invocation" + +### Concurrency Mode Detection + +The run loop needs to know whether it's operating in single or multi-concurrency mode to decide whether to set the environment variable. Rather than re-reading `AWS_LAMBDA_MAX_CONCURRENCY` in the run loop, the concurrency mode is passed as a parameter from the caller. + +```swift +@available(LambdaSwift 2.0, *) +extension Lambda { + @inlinable + package static func runLoop( + runtimeClient: RuntimeClient, + handler: Handler, + loggingConfiguration: LoggingConfiguration, + logger: Logger, + isSingleConcurrencyMode: Bool = true + ) async throws where Handler: StreamingLambdaHandler +} +``` + +**Design Decisions:** +- Default value `true` preserves backward compatibility — `LambdaRuntime` (single-concurrency) doesn't need changes +- `LambdaManagedRuntime` passes `false` when `maxConcurrency > 1` +- Boolean is simpler than passing the integer concurrency value since we only need a binary decision + +### Modified Run Loop (Lambda.swift) + +The core change wraps the handler invocation in a `TaskLocal` scope: + +```swift +// Inside the while loop, after creating requestLogger: + +try await LambdaContext.$currentTraceID.withValue(invocation.metadata.traceID) { + + // Set env var only in single-concurrency mode + if isSingleConcurrencyMode { + setenv("_X_AMZN_TRACE_ID", invocation.metadata.traceID, 1) + } + defer { + if isSingleConcurrencyMode { + unsetenv("_X_AMZN_TRACE_ID") + } + } + + do { + try await handler.handle( + invocation.event, + responseWriter: writer, + context: LambdaContext( + requestID: invocation.metadata.requestID, + traceID: invocation.metadata.traceID, + // ... rest unchanged + ) + ) + } catch { + try await writer.reportError(error) + } +} +``` + +**Design Decisions:** +- `TaskLocal.withValue` is used (not `TaskLocal.withValue(operation:)` with a stored binding) to ensure the scope is tied to structured concurrency +- The env var is set inside the `withValue` scope so both mechanisms are active simultaneously +- `defer` ensures cleanup even if the handler throws +- The `setenv`/`unsetenv` calls use POSIX functions already imported in the file (`Darwin.C` / `Glibc` / `Musl`) + +### LambdaManagedRuntime Changes + +```swift +// In _run(), when maxConcurrency > 1: +try await LambdaRuntime.startRuntimeInterfaceClient( + endpoint: runtimeEndpoint, + handler: self.handler, + eventLoop: self.eventLoop, + loggingConfiguration: self.loggingConfiguration, + logger: logger, + isSingleConcurrencyMode: false // NEW +) +``` + +And the `startRuntimeInterfaceClient` method passes this through to `Lambda.runLoop`. + +## Error Handling + +No new error types are introduced. `TaskLocal.withValue` does not throw on its own. The `setenv`/`unsetenv` calls are best-effort (their return values are ignored, matching the convention in other Lambda runtimes). + +## Testing Strategy + +### Unit Tests + +1. **TaskLocal propagation in single-concurrency mode** + - Verify `LambdaContext.currentTraceID` returns the correct trace ID inside a handler + - Verify `LambdaContext.currentTraceID` returns `nil` outside a handler scope + +2. **TaskLocal isolation in multi-concurrency mode** + - Spawn multiple concurrent tasks, each with a different trace ID via `withValue` + - Verify each task sees only its own trace ID + +3. **Environment variable behavior** + - In single-concurrency mode: verify `_X_AMZN_TRACE_ID` is set during handler execution and cleared after + - In multi-concurrency mode: verify `_X_AMZN_TRACE_ID` is NOT set + +4. **Background task propagation** + - Verify the `TaskLocal` remains available during background work after the response is sent + +### Integration Tests + +- Use the existing local test server infrastructure to run a handler that reads `LambdaContext.currentTraceID` and returns it in the response, verifying it matches the trace ID sent in the invocation headers. + +## Performance Considerations + +- `TaskLocal` access is O(1) — it's a lookup in the task's local storage, comparable to reading a local variable +- `setenv`/`unsetenv` are syscalls but only execute in single-concurrency mode (one invocation at a time), so there's no contention +- No heap allocations beyond the `String` value already present in `InvocationMetadata.traceID` +- No new dependencies introduced + +## Compatibility + +- Fully backward compatible: existing `context.traceID` instance property is unchanged +- The `TaskLocal` is additive — code that doesn't use it is unaffected +- The `isSingleConcurrencyMode` parameter defaults to `true`, so `LambdaRuntime` callers don't need changes +- The env var behavior in single-concurrency mode matches what other Lambda runtimes (Python, Node.js, Java) do today for backward compatibility with legacy tooling diff --git a/.kiro/specs/xray-trace-id-propagation/github-issue-633.md b/.kiro/specs/xray-trace-id-propagation/github-issue-633.md new file mode 100644 index 000000000..227d7a5ee --- /dev/null +++ b/.kiro/specs/xray-trace-id-propagation/github-issue-633.md @@ -0,0 +1,104 @@ +## [core] Implement Trace ID Propagation for Multi-Concurrent Environments + +The Swift AWS Lambda Runtime currently receives the trace ID from the Lambda Runtime API via the `Lambda-Runtime-Trace-Id` header and stores it in `LambdaContext.traceID`. However, there's no implicit propagation mechanism — downstream code (OpenTelemetry instrumentation, HTTP client middleware) can't discover the current invocation's trace ID without an explicit `LambdaContext` reference. + +According to the [AWS Lambda Runtime API documentation](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html), the runtime should also set the `_X_AMZN_TRACE_ID` environment variable. This is missing from the Swift runtime. + +> **Note:** The AWS X-Ray SDK/Daemon entered maintenance mode on February 25, 2026. AWS recommends migrating to [OpenTelemetry](https://docs.aws.amazon.com/xray/latest/devguide/xray-otel-migration.html). The `TaskLocal`-based propagation proposed here is the forward-looking mechanism for OpenTelemetry integration. The `_X_AMZN_TRACE_ID` environment variable is maintained for backward compatibility with legacy tooling only. + +### Current Behavior + +- The runtime receives the `Lambda-Runtime-Trace-Id` header from the Lambda Runtime API +- The trace ID is stored in `LambdaContext.traceID` +- The `_X_AMZN_TRACE_ID` environment variable is **not** set +- No implicit propagation mechanism exists for downstream libraries + +### Expected Behavior + +- The runtime receives the `Lambda-Runtime-Trace-Id` header +- The trace ID is stored in `LambdaContext.traceID` (unchanged) +- A `@TaskLocal` makes the trace ID implicitly available to all code in the handler's async task tree +- In single-concurrency mode only, the `_X_AMZN_TRACE_ID` environment variable is set per invocation and cleared after + +### Implementation Considerations + +#### Standard Lambda Functions (single concurrency) + +Setting the environment variable with `setenv()` is straightforward since only one invocation runs at a time. The `TaskLocal` is also set for consistency. + +#### Lambda Managed Instances (multi-concurrency) + +Multiple concurrent invocations share the same process. Environment variables are process-global, so setting `_X_AMZN_TRACE_ID` would cause trace ID conflicts between concurrent invocations. In this mode, the runtime must **skip** the env var entirely and rely solely on the `TaskLocal`. + +The runtime detects the mode via `AWS_LAMBDA_MAX_CONCURRENCY` (already read by `LambdaManagedRuntime`). + +### How Other Runtimes Handle This + +- **Python:** Uses `os.environ['_X_AMZN_TRACE_ID']` — safe because Python's multi-concurrency model uses separate processes with isolated `os.environ` ([ref](https://github.com/aws/aws-lambda-python-runtime-interface-client)) +- **Java:** Uses SLF4J MDC (thread-local map) to avoid `SystemProperty` conflicts across threads ([ref](https://github.com/aws/aws-lambda-java-libs)) +- **Node.js:** Uses `AsyncLocalStorage` to bind trace ID to the current async call chain ([ref](https://github.com/aws/aws-lambda-nodejs-runtime-interface-client)) + +### Solution: TaskLocal + Conditional Environment Variable + +Swift's `TaskLocal` is the direct equivalent of Java's MDC and Node.js's `AsyncLocalStorage`. It isolates values to the current structured concurrency tree. + +#### 1. Define TaskLocal on LambdaContext + +```swift +@available(LambdaSwift 2.0, *) +extension LambdaContext { + @TaskLocal + public static var currentTraceID: String? +} +``` + +#### 2. Wrap Handler Invocation in TaskLocal Scope + +In `Sources/AWSLambdaRuntime/Lambda.swift`, inside `Lambda.runLoop`: + +```swift +try await LambdaContext.$currentTraceID.withValue(invocation.metadata.traceID) { + if isSingleConcurrencyMode { + setenv("_X_AMZN_TRACE_ID", invocation.metadata.traceID, 1) + } + defer { + if isSingleConcurrencyMode { + unsetenv("_X_AMZN_TRACE_ID") + } + } + + try await handler.handle(invocation.event, responseWriter: writer, context: context) +} +``` + +#### 3. Thread Concurrency Mode Through the Call Chain + +Add `isSingleConcurrencyMode: Bool = true` parameter to: +- `Lambda.runLoop` (default `true` for backward compat) +- `LambdaRuntime.startRuntimeInterfaceClient` + +`LambdaManagedRuntime` passes `false` when `maxConcurrency > 1`. + +### Files to Modify + +- **`Sources/AWSLambdaRuntime/LambdaContext.swift`** — Add `@TaskLocal public static var currentTraceID: String?` +- **`Sources/AWSLambdaRuntime/Lambda.swift`** — Wrap handler call in `withValue` scope, add `isSingleConcurrencyMode` parameter, conditionally set/clear env var +- **`Sources/AWSLambdaRuntime/Runtime/LambdaRuntime.swift`** — Add `isSingleConcurrencyMode` parameter to `startRuntimeInterfaceClient`, pass through to `Lambda.runLoop` +- **`Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift`** — Pass `isSingleConcurrencyMode: false` when `maxConcurrency > 1` + +### Testing Requirements + +- [ ] `LambdaContext.currentTraceID` returns `nil` outside invocation scope +- [ ] `LambdaContext.currentTraceID` returns correct value inside handler +- [ ] Concurrent tasks with different trace IDs see their own values (no cross-contamination) +- [ ] Single-concurrency mode: `_X_AMZN_TRACE_ID` is set during handler execution and cleared after +- [ ] Multi-concurrency mode: `_X_AMZN_TRACE_ID` is NOT set +- [ ] TaskLocal remains available during background work after response is sent + +### References + +- [AWS Lambda Runtime API Documentation](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html) +- [X-Ray Tracing Header Documentation](https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader) +- [Migrating from X-Ray to OpenTelemetry](https://docs.aws.amazon.com/xray/latest/devguide/xray-otel-migration.html) +- [Swift TaskLocal Documentation](https://developer.apple.com/documentation/swift/tasklocal) +- Design and implementation plan: `.kiro/specs/xray-trace-id-propagation/` diff --git a/.kiro/specs/xray-trace-id-propagation/requirements.md b/.kiro/specs/xray-trace-id-propagation/requirements.md new file mode 100644 index 000000000..87e6d7fb2 --- /dev/null +++ b/.kiro/specs/xray-trace-id-propagation/requirements.md @@ -0,0 +1,61 @@ +# Requirements Document + +## Introduction + +This feature adds proper X-Ray Trace ID propagation for multi-concurrent Lambda environments (Managed Instances / "Elevator"). Today, the Swift Lambda runtime extracts the `Lambda-Runtime-Trace-Id` header per invocation and passes it through `LambdaContext.traceID`, but it does not provide an implicit propagation mechanism. Downstream libraries (e.g., OpenTelemetry instrumentation or HTTP client middleware) have no way to discover the current invocation's trace ID without an explicit `LambdaContext` reference. In multi-concurrent mode (`AWS_LAMBDA_MAX_CONCURRENCY > 1`), multiple invocations run simultaneously in the same process, making process-level environment variables unsuitable for trace propagation. + +Note: The AWS X-Ray SDK/Daemon entered maintenance mode on February 25, 2026. AWS recommends migrating to OpenTelemetry for instrumentation. This design targets OpenTelemetry as the primary consumer of implicit trace ID propagation, while maintaining the `_X_AMZN_TRACE_ID` environment variable in single-concurrency mode for backward compatibility with legacy tooling. + +The solution uses Swift's `TaskLocal` to store the trace ID in the current task's scope, making it implicitly available to all code running within an invocation's async call tree. In single-concurrency mode, the runtime additionally sets the `_X_AMZN_TRACE_ID` environment variable for backward compatibility with legacy tooling that reads it. + +## Requirements + +### Requirement 1 + +**User Story:** As a library author building OpenTelemetry or tracing middleware for Swift Lambda functions, I want to implicitly access the current invocation's trace ID from anywhere in the async call tree, so that I can auto-inject trace context headers on outbound HTTP calls without requiring an explicit `LambdaContext` reference. + +#### Acceptance Criteria + +1. WHEN a Lambda invocation is being handled THEN the system SHALL make the X-Ray Trace ID available via a `TaskLocal` property accessible as `LambdaContext.currentTraceID` +2. WHEN code running inside the handler's async call tree accesses `LambdaContext.currentTraceID` THEN it SHALL return the trace ID for the current invocation +3. WHEN code running outside any invocation scope accesses `LambdaContext.currentTraceID` THEN it SHALL return `nil` +4. WHEN multiple invocations run concurrently THEN each invocation's `TaskLocal` SHALL contain its own trace ID without cross-contamination + +### Requirement 2 + +**User Story:** As a Lambda function developer using the traditional single-concurrency mode, I want the runtime to set the `_X_AMZN_TRACE_ID` environment variable per invocation, so that legacy tooling and OpenTelemetry auto-instrumentation that reads this variable continues to work without code changes. + +#### Acceptance Criteria + +1. WHEN `AWS_LAMBDA_MAX_CONCURRENCY` is 1 or unset THEN the runtime SHALL set the `_X_AMZN_TRACE_ID` process environment variable to the current invocation's trace ID before calling the handler +2. WHEN the invocation completes THEN the runtime SHALL clear the `_X_AMZN_TRACE_ID` environment variable +3. WHEN `AWS_LAMBDA_MAX_CONCURRENCY` is greater than 1 THEN the runtime SHALL NOT set the `_X_AMZN_TRACE_ID` environment variable (to avoid cross-invocation contamination) + +### Requirement 3 + +**User Story:** As a Lambda function developer, I want the `TaskLocal`-based trace ID propagation to work transparently with all handler protocols (`StreamingLambdaHandler`, `LambdaHandler`, `LambdaWithBackgroundProcessingHandler`), so that I don't need to change my handler code. + +#### Acceptance Criteria + +1. WHEN the runtime invokes any handler type THEN the trace ID `TaskLocal` SHALL be set before the handler's `handle` function is called +2. WHEN background work executes after the response is sent (via `LambdaWithBackgroundProcessingHandler`) THEN the `TaskLocal` trace ID SHALL remain available during background execution +3. WHEN the handler function returns or throws THEN the `TaskLocal` scope SHALL end naturally via structured concurrency + +### Requirement 4 + +**User Story:** As a Lambda function developer, I want a public API to read the current trace ID for manual propagation scenarios, so that I can pass it to libraries that don't automatically read the `TaskLocal`. + +#### Acceptance Criteria + +1. WHEN a developer accesses `LambdaContext.currentTraceID` THEN it SHALL return an `Optional` containing the trace ID or `nil` if not in an invocation scope +2. WHEN a developer accesses `context.traceID` on a `LambdaContext` instance THEN it SHALL continue to work as before (no breaking changes) + +### Requirement 5 + +**User Story:** As a maintainer of the Swift Lambda runtime, I want the trace propagation mechanism to have minimal performance overhead, so that it does not impact cold start times or invocation latency. + +#### Acceptance Criteria + +1. WHEN the `TaskLocal` is set per invocation THEN the overhead SHALL be negligible (sub-microsecond, comparable to a dictionary lookup) +2. WHEN running in single-concurrency mode THEN the `setenv`/`unsetenv` calls SHALL add no measurable latency to invocation processing +3. WHEN the feature is compiled THEN it SHALL NOT introduce any new external dependencies beyond what the runtime already uses diff --git a/.kiro/specs/xray-trace-id-propagation/tasks.md b/.kiro/specs/xray-trace-id-propagation/tasks.md new file mode 100644 index 000000000..99957b16f --- /dev/null +++ b/.kiro/specs/xray-trace-id-propagation/tasks.md @@ -0,0 +1,45 @@ +# Implementation Plan + +- [ ] 1. Add `@TaskLocal` property to `LambdaContext` + - In `Sources/AWSLambdaRuntime/LambdaContext.swift`, add a `@TaskLocal` static property `currentTraceID` of type `String?` to `LambdaContext` via an extension + - Add documentation comment explaining the property returns the trace ID for the current invocation, or `nil` outside an invocation scope. Note that this is intended for use by OpenTelemetry instrumentation and tracing middleware. + - _Requirements: 1.1, 1.3, 4.1, 4.2_ + +- [ ] 2. Add `isSingleConcurrencyMode` parameter to `Lambda.runLoop` + - In `Sources/AWSLambdaRuntime/Lambda.swift`, add a `isSingleConcurrencyMode: Bool = true` parameter to both `runLoop` overloads (the current one and the deprecated one) + - The deprecated overload should forward the parameter to the current overload + - _Requirements: 2.1, 2.3_ + +- [ ] 3. Wrap handler invocation in `TaskLocal.withValue` scope in `Lambda.runLoop` + - In `Sources/AWSLambdaRuntime/Lambda.swift`, inside the `while !Task.isCancelled` loop, wrap the handler invocation (from `handler.handle(...)` through the catch block) in `LambdaContext.$currentTraceID.withValue(invocation.metadata.traceID) { ... }` + - Inside the `withValue` scope, conditionally call `setenv("_X_AMZN_TRACE_ID", invocation.metadata.traceID, 1)` when `isSingleConcurrencyMode` is `true` + - Add a `defer` block that calls `unsetenv("_X_AMZN_TRACE_ID")` when `isSingleConcurrencyMode` is `true` + - _Requirements: 1.1, 1.2, 2.1, 2.2, 2.3, 3.1, 3.2, 3.3_ + +- [ ] 4. Add `isSingleConcurrencyMode` parameter to `LambdaRuntime.startRuntimeInterfaceClient` + - In `Sources/AWSLambdaRuntime/Runtime/LambdaRuntime.swift`, add `isSingleConcurrencyMode: Bool = true` parameter to the `startRuntimeInterfaceClient` static method + - Pass the parameter through to `Lambda.runLoop` + - _Requirements: 2.1, 2.3_ + +- [ ] 5. Pass `isSingleConcurrencyMode: false` from `LambdaManagedRuntime` when concurrency > 1 + - In `Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift`, in the `_run()` method, when `maxConcurrency > 1`, pass `isSingleConcurrencyMode: false` to `LambdaRuntime.startRuntimeInterfaceClient` + - The single-concurrency path (`maxConcurrency <= 1`) should pass `isSingleConcurrencyMode: true` (or rely on the default) + - _Requirements: 2.1, 2.3_ + +- [ ] 6. Write unit tests for `TaskLocal` trace ID propagation + - Create or extend test file in `Tests/AWSLambdaRuntimeTests/` to test `LambdaContext.currentTraceID` + - Test that `LambdaContext.currentTraceID` returns `nil` outside an invocation scope + - Test that `LambdaContext.$currentTraceID.withValue("test-trace-id") { ... }` makes the value accessible inside the closure + - Test that concurrent tasks with different trace IDs via `withValue` each see their own value (no cross-contamination) + - _Requirements: 1.2, 1.4_ + +- [ ] 7. Write unit tests for environment variable behavior + - Test that in single-concurrency mode, `_X_AMZN_TRACE_ID` is set during handler execution (use the local test server or mock runtime client) + - Test that in single-concurrency mode, `_X_AMZN_TRACE_ID` is cleared after handler execution + - Test that in multi-concurrency mode, `_X_AMZN_TRACE_ID` is NOT set during handler execution + - _Requirements: 2.1, 2.2, 2.3_ + +- [ ] 8. Update documentation + - Update `Sources/AWSLambdaRuntime/Docs.docc/` if there is existing documentation about tracing or context to mention `LambdaContext.currentTraceID` + - Add a brief note in the managed instances documentation (`Sources/AWSLambdaRuntime/Docs.docc/managed-instances.md`) about trace ID propagation in multi-concurrency mode + - _Requirements: 1.1, 4.1_ diff --git a/.kiro/steering/product.md b/.kiro/steering/product.md new file mode 100644 index 000000000..ead87679e --- /dev/null +++ b/.kiro/steering/product.md @@ -0,0 +1,22 @@ +# Swift AWS Lambda Runtime + +The Swift AWS Lambda Runtime is a library that enables developers to build AWS Lambda functions using Swift. It provides a multi-tier API for creating serverless functions ranging from simple closures to complex, performance-sensitive event handlers. + +## Key Features + +- **Performance-optimized**: Low memory footprint, deterministic performance, and quick start times +- **Developer-friendly**: Emphasizes safety, expressiveness, and ease of use +- **Multi-tier API**: Supports both simple closures and complex event handlers +- **AWS Integration**: Built-in support for AWS services through events and responses +- **Response Streaming**: Support for streaming large responses back to clients +- **Background Tasks**: Ability to run code after returning the main response +- **Local Testing**: Built-in local HTTP server for testing functions before deployment + +## Target Use Cases + +- Event-driven serverless functions +- API Gateway backends +- AWS service integrations (S3, SNS, SQS) +- Cost-effective compute workloads +- Mission-critical microservices +- Data-intensive workloads requiring efficient resource utilization \ No newline at end of file diff --git a/.kiro/steering/structure.md b/.kiro/steering/structure.md new file mode 100644 index 000000000..84b075636 --- /dev/null +++ b/.kiro/steering/structure.md @@ -0,0 +1,105 @@ +# Project Structure + +## Root Directory + +- `Package.swift` - Swift Package Manager manifest with dependencies and targets +- `Package@swift-6.0.swift` - Swift 6.0 specific package manifest +- `Makefile` - Build automation and convenience commands +- `readme.md` - Main project documentation and getting started guide + +## Core Source Code + +### `Sources/AWSLambdaRuntime/` +Main runtime library implementation: + +- `Lambda.swift` - Core Lambda runtime loop and execution logic +- `LambdaRuntime.swift` - Main runtime class and public API +- `LambdaHandlers.swift` - Handler protocol definitions and implementations +- `LambdaContext.swift` - Request context and metadata +- `LambdaRuntimeClient.swift` - HTTP client for AWS Lambda Runtime API +- `Lambda+Codable.swift` - JSON encoding/decoding support +- `Lambda+LocalServer.swift` - Local testing server implementation +- `LambdaRuntimeError.swift` - Error types and handling +- `ControlPlaneRequest.swift` - AWS control plane communication +- `Utils.swift` - Utility functions and helpers + +### `Sources/AWSLambdaRuntime/FoundationSupport/` +Foundation integration and JSON support + +### `Sources/AWSLambdaRuntime/Docs.docc/` +Documentation and tutorials using DocC + +### `Sources/MockServer/` +Mock server for performance testing + +## Examples Directory + +### `Examples/` +Comprehensive example implementations: + +- `_MyFirstFunction/` - Quick start tutorial with deployment script +- `HelloWorld/` - Basic Lambda function example +- `HelloJSON/` - JSON input/output handling +- `APIGateway/` - REST API with API Gateway integration +- `APIGateway+LambdaAuthorizer/` - API with Lambda authorizer +- `Streaming/` - Response streaming example +- `StreamingFromEvent/` - Event-driven streaming +- `BackgroundTasks/` - Background processing after response +- `S3EventNotifier/` - S3 event handling +- `S3_AWSSDK/` - AWS SDK integration +- `S3_Soto/` - Soto library integration +- `CDK/` - AWS CDK deployment example +- `Testing/` - Unit testing patterns +- `Tutorial/` - Step-by-step learning materials + +## Testing + +### `Tests/AWSLambdaRuntimeTests/` +Comprehensive test suite covering: +- Runtime functionality +- Handler implementations +- Error scenarios +- Integration tests + +## Build and Deployment + +### `Plugins/` +Swift Package Manager plugins: +- `AWSLambdaPackager` - Creates deployment-ready ZIP archives + +### `.build/` +Build artifacts and intermediate files (generated) + +## Configuration Files + +- `.swift-version` - Swift toolchain version specification +- `.swift-format` - Code formatting configuration +- `.gitignore` - Git ignore patterns +- `.licenseignore` - License checking exclusions + +## Development Tools + +### `.devcontainer/` +VS Code development container configuration + +### `.vscode/` +VS Code workspace settings and configurations + +### `scripts/` +Build and deployment automation scripts + +## Documentation + +- `CODE_OF_CONDUCT.md` - Community guidelines +- `CONTRIBUTING.md` - Contribution guidelines +- `CONTRIBUTORS.txt` - Project contributors +- `SECURITY.md` - Security policy and reporting +- `LICENSE.txt` - Apache 2.0 license +- `NOTICE.txt` - Third-party notices + +## Naming Conventions + +- **Files**: PascalCase for Swift files (e.g., `LambdaRuntime.swift`) +- **Directories**: PascalCase for major components, lowercase for utilities +- **Examples**: Descriptive names indicating functionality +- **Tests**: Mirror source structure with `Tests` suffix \ No newline at end of file diff --git a/.kiro/steering/swift-api-design-guidelines.md b/.kiro/steering/swift-api-design-guidelines.md new file mode 100644 index 000000000..0f349e77b --- /dev/null +++ b/.kiro/steering/swift-api-design-guidelines.md @@ -0,0 +1,222 @@ +# Swift API Design Guidelines + +*Content adapted from [Swift.org API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/) for compliance with licensing restrictions* + +## Table of Contents + +- [Introduction](#introduction) +- [Fundamentals](#fundamentals) +- [Naming](#naming) + - [Promote Clear Usage](#promote-clear-usage) + - [Strive for Fluent Usage](#strive-for-fluent-usage) + - [Use Terminology Well](#use-terminology-well) +- [Conventions](#conventions) + - [General Conventions](#general-conventions) + - [Parameters](#parameters) + - [Argument Labels](#argument-labels) +- [Special Instructions](#special-instructions) + +## Introduction + +Delivering a clear, consistent developer experience when writing Swift code is largely defined by the names and idioms that appear in APIs. These design guidelines explain how to make sure that your code feels like a part of the larger Swift ecosystem. + +## Fundamentals + +- **Clarity at the point of use** is your most important goal. Entities such as methods and properties are declared only once but used repeatedly. Design APIs to make those uses clear and concise. + +- **Clarity is more important than brevity.** Although Swift code can be compact, it is a non-goal to enable the smallest possible code with the fewest characters. + +- **Write a documentation comment** for every declaration. Insights gained by writing documentation can have a profound impact on your design. + +### Documentation Guidelines + +- Use Swift's dialect of Markdown +- Begin with a summary that describes the entity being declared +- Focus on the summary; it's the most important part +- Use a single sentence fragment if possible, ending with a period +- Describe what a function or method does and what it returns +- Describe what a subscript accesses +- Describe what an initializer creates +- For all other declarations, describe what the declared entity is + +Example: +```swift +/// Returns a "view" of `self` containing the same elements in reverse order. +func reversed() -> ReverseCollection +``` + +## Naming + +### Promote Clear Usage + +- **Include all the words needed to avoid ambiguity** for a person reading code where the name is used. + +```swift +// Good +employees.remove(at: x) + +// Bad - unclear intent +employees.remove(x) // are we removing x? +``` + +- **Omit needless words.** Every word in a name should convey salient information at the use site. + +```swift +// Bad - redundant type information +public mutating func removeElement(_ member: Element) -> Element? +allViews.removeElement(cancelButton) + +// Good - clearer without redundancy +public mutating func remove(_ member: Element) -> Element? +allViews.remove(cancelButton) +``` + +- **Name variables, parameters, and associated types according to their roles,** rather than their type constraints. + +```swift +// Bad - names based on types +var string = "Hello" +func restock(from widgetFactory: WidgetFactory) + +// Good - names based on roles +var greeting = "Hello" +func restock(from supplier: WidgetFactory) +``` + +- **Compensate for weak type information** to clarify a parameter's role. + +```swift +// Bad - vague usage +func add(_ observer: NSObject, for keyPath: String) +grid.add(self, for: graphics) // vague + +// Good - clear roles +func addObserver(_ observer: NSObject, forKeyPath path: String) +grid.addObserver(self, forKeyPath: graphics) // clear +``` + +### Strive for Fluent Usage + +- **Prefer method and function names that make use sites form grammatical English phrases.** + +```swift +x.insert(y, at: z) // "x, insert y at z" +x.subviews(havingColor: y) // "x's subviews having color y" +x.capitalizingNouns() // "x, capitalizing nouns" +``` + +- **Begin names of factory methods with "make"** + +```swift +x.makeIterator() +``` + +- **Name functions and methods according to their side-effects** + - Those without side-effects should read as noun phrases: `x.distance(to: y)` + - Those with side-effects should read as imperative verb phrases: `x.sort()` + +- **Name Mutating/nonmutating method pairs consistently** + - When naturally described by a verb, use imperative for mutating and past participle for nonmutating: + +```swift +// Mutating +x.sort() +x.append(y) + +// Nonmutating +z = x.sorted() +z = x.appending(y) +``` + +- **Uses of Boolean methods and properties should read as assertions about the receiver** + +```swift +x.isEmpty +line1.intersects(line2) +``` + +- **Protocols that describe what something is should read as nouns** + +```swift +Collection +``` + +- **Protocols that describe a capability should use suffixes `able`, `ible`, or `ing`** + +```swift +Equatable +ProgressReporting +``` + +### Use Terminology Well + +- **Avoid obscure terms** if a more common word conveys meaning just as well +- **Stick to the established meaning** if you do use a term of art +- **Avoid abbreviations** - they are effectively terms-of-art +- **Embrace precedent** - don't optimize for total beginners at the expense of existing culture + +## Conventions + +### General Conventions + +- **Document the complexity of any computed property that is not O(1)** +- **Prefer methods and properties to free functions** (exceptions: no obvious self, unconstrained generics, established domain notation) +- **Follow case conventions**: Types and protocols are `UpperCamelCase`, everything else is `lowerCamelCase` +- **Methods can share a base name** when they share the same basic meaning or operate in distinct domains + +### Parameters + +- **Choose parameter names to serve documentation** +- **Take advantage of defaulted parameters** when it simplifies common uses +- **Prefer to locate parameters with defaults toward the end** of the parameter list +- **If your API will run in production, prefer `#fileID`** over alternatives + +### Argument Labels + +- **Omit all labels when arguments can't be usefully distinguished** + +```swift +min(number1, number2) +zip(sequence1, sequence2) +``` + +- **In initializers that perform value preserving type conversions, omit the first argument label** + +```swift +Int64(someUInt32) +String(veryLargeNumber) +``` + +- **When the first argument forms part of a prepositional phrase, give it an argument label** + +```swift +x.removeBoxes(havingLength: 12) +``` + +- **Otherwise, if the first argument forms part of a grammatical phrase, omit its label** + +```swift +x.addSubview(y) +``` + +- **Label all other arguments** + +## Special Instructions + +- **Label tuple members and name closure parameters** where they appear in your API +- **Take extra care with unconstrained polymorphism** to avoid ambiguities in overload sets + +Example of resolving ambiguity: +```swift +// Ambiguous +public mutating func append(_ newElement: Element) +public mutating func append(_ newElements: S) + +// Clear +public mutating func append(_ newElement: Element) +public mutating func append(contentsOf newElements: S) +``` + +--- + +*Content was rephrased for compliance with licensing restrictions* diff --git a/.kiro/steering/swift-general.md b/.kiro/steering/swift-general.md new file mode 100644 index 000000000..1421493eb --- /dev/null +++ b/.kiro/steering/swift-general.md @@ -0,0 +1,104 @@ +You are a coding assistant--with access to tools--specializing +in analyzing codebases. Below is the content of the file the +user is working on. Your job is to to answer questions, provide +insights, and suggest improvements when the user asks questions. + +Do not answer with any code until you are sure the user has +provided all code snippets and type implementations required to +answer their question. + +Briefly--in as little text as possible--walk through the solution +in prose to identify types you need that are missing from the files +that have been sent to you. + +Whenever possible, favor Apple programming languages and +frameworks or APIs that are already available on Apple devices. +Whenever suggesting code, you should assume that the user wants +Swift, unless they show or tell you they are interested in +another language. + +Always prefer Swift, Objective-C, C, and C++ over alternatives. + +Pay close attention to the platform that this code is for. +For example, if you see clues that the user is writing a Mac +app, avoid suggesting iOS-only APIs. + +Refer to Apple platforms with their official names, like iOS, +iPadOS, macOS, watchOS and visionOS. Avoid mentioning specific +products and instead use these platform names. + +In most projects, you can also provide code examples using the new +Swift Testing framework that uses Swift Macros. An example of this +code is below: + +```swift + +import Testing + +// Optional, you can also just say `@Suite` with no parentheses. +@Suite("You can put a test suite name here, formatted as normal text.") +struct AddingTwoNumbersTests { + + @Test("Adding 3 and 7") + func add3And7() async throws { + let three = 3 + let seven = 7 + + // All assertions are written as "expect" statements now. + #expect(three + seven == 10, "The sums should work out.") + } + + @Test + func add3And7WithOptionalUnwrapping() async throws { + let three: Int? = 3 + let seven = 7 + + // Similar to `XCTUnwrap` + let unwrappedThree = try #require(three) + + let sum = three + seven + + #expect(sum == 10) + } + +} +``` +When asked to write unit tests, always prefer the new Swift testing framework over XCTest. + +In general, prefer the use of Swift Concurrency (async/await, +actors, etc.) over tools like Dispatch or Combine, but if the +user's code or words show you they may prefer something else, +you should be flexible to this preference. + +Sometimes, the user may provide specific code snippets for your +use. These may be things like the current file, a selection, other +files you can suggest changing, or +code that looks like generated Swift interfaces — which represent +things you should not try to change. + +However, this query will start without any additional context. + +When it makes sense, you should propose changes to existing code. +Whenever you are proposing changes to an existing file, +it is imperative that you repeat the entire file, without ever +eliding pieces, even if they will be kept identical to how they are +currently. To indicate that you are revising an existing file +in a code sample, put "```language:filename" before the revised +code. It is critical that you only propose replacing files that +have been sent to you. For example, if you are revising +FooBar.swift, you would say: + +```swift:FooBar.swift +// the entire code of the file with your changes goes here. +// Do not skip over anything. +``` + +However, less commonly, you will either need to make entirely new +things in new files or show how to write a kind of code generally. +When you are in this rarer circumstance, you can just show the +user a code snippet, with normal markdown: +```swift +// Swift code here +``` + + diff --git a/.kiro/steering/tech.md b/.kiro/steering/tech.md new file mode 100644 index 000000000..35229f735 --- /dev/null +++ b/.kiro/steering/tech.md @@ -0,0 +1,87 @@ +# Technology Stack + +## Core Technologies + +- **Swift 6.x**: Primary programming language (minimum Swift 6.0) +- **SwiftNIO**: Asynchronous networking framework for HTTP client implementation +- **Swift Package Manager**: Dependency management and build system +- **Docker**: Required for cross-compilation to Amazon Linux 2 + +## Key Dependencies + +- `swift-nio`: Asynchronous event-driven network application framework +- `swift-log`: Logging API for Swift +- `swift-collections`: Additional collection types (DequeModule) +- `swift-service-lifecycle`: Service lifecycle management (optional trait) + +## Platform Requirements + +- **macOS**: Version 15 (Sequoia) or later for development +- **Target Runtime**: Amazon Linux 2 (provided.al2 runtime) +- **Architecture**: Supports both x86_64 and ARM64 (Apple Silicon) + +## Build System + +### Swift Package Manager Commands + +```bash +# Build the project +swift build + +# Run tests +swift test + +# Format code +swift format format --parallel --recursive --in-place ./Package.swift Examples/ Sources/ Tests/ + +# Generate documentation +swift package generate-documentation --target AWSLambdaRuntime + +# Preview documentation +swift package --disable-sandbox preview-documentation --target AWSLambdaRuntime +``` + +### Lambda-Specific Commands + +```bash +# Initialize a new Lambda function +swift package lambda-init --allow-writing-to-package-directory + +# Build and archive for AWS deployment +swift package archive --allow-network-connections docker + +# Add 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 +``` + +### Local Testing + +```bash +# Run locally (starts HTTP server on port 7000) +swift run + +# Test with custom endpoint +LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT=/2015-03-31/functions/function/invocations swift run + +# Invoke local function +curl -v --header "Content-Type: application/json" --data @events/test.json http://127.0.0.1:7000/invoke +``` + +### Cross-Platform Build + +```bash +# Build on Linux using Docker +docker run --rm -v $(pwd):/work swift:6.1 /bin/bash -c "cd /work && swift build && swift test" + +# Using container command (alternative) +CONTAINER=podman make build-linux +``` + +## Deployment Tools + +- **AWS CLI**: Command-line deployment +- **AWS SAM**: Serverless Application Model for infrastructure as code +- **AWS CDK**: Cloud Development Kit for programmatic infrastructure +- **Terraform**: Third-party infrastructure as code +- **Serverless Framework**: Third-party deployment framework \ No newline at end of file From 3e86ec4f237572bc6e47426c448efe77f07060e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Thu, 4 Jun 2026 12:53:44 +0200 Subject: [PATCH 22/27] Initial Commit for plugins v2 --- .../.config.kiro | 2 +- .kiro/specs/lambda-v2-plugins/design.md | 609 +++++++++ .kiro/specs/lambda-v2-plugins/requirements.md | 229 ++++ .kiro/specs/lambda-v2-plugins/tasks.md | 280 ++++ Examples/APIGatewayV1/README.md | 2 +- .../APIGatewayV2+LambdaAuthorizer/README.md | 2 +- Examples/APIGatewayV2/README.md | 2 +- Examples/BackgroundTasks/README.md | 35 +- Examples/CDK/README.md | 2 +- Examples/HelloJSON/README.md | 21 +- Examples/HelloWorld/README.md | 19 +- Examples/HelloWorldNoTraits/README.md | 18 +- Examples/HummingbirdLambda/README.md | 2 +- Examples/JSONLogging/README.md | 26 +- Examples/ManagedInstances/README.md | 2 +- Examples/MultiSourceAPI/README.md | 2 +- Examples/MultiTenant/README.md | 2 +- Examples/README.md | 4 +- Examples/ResourcesPackaging/README.md | 18 +- Examples/S3EventNotifier/README.md | 18 +- Examples/S3_AWSSDK/README.md | 2 +- Examples/S3_Soto/README.md | 2 +- Examples/ServiceLifecycle+Postgres/README.md | 2 +- Examples/Streaming+APIGateway/README.md | 2 +- Examples/Streaming+Codable/README.md | 62 +- Examples/Streaming+FunctionUrl/README.md | 69 +- Examples/_MyFirstFunction/clean.sh | 19 +- .../create_and_deploy_function.sh | 196 --- Examples/_MyFirstFunction/create_function.sh | 40 +- Package.swift | 47 +- Plugins/AWSLambdaBuilder/Plugin.swift | 10 +- Plugins/AWSLambdaDeployer/Plugin.swift | 25 +- Plugins/AWSLambdaInitializer/Plugin.swift | 9 +- Plugins/AWSLambdaPackager/Plugin.swift | 139 ++ .../AWSLambdaPluginHelper.swift | 51 +- .../spm => }/ArgumentExtractor.swift | 0 .../AWSLambdaPluginHelper/Extensions.swift | 19 +- .../GeneratedClients/IAM/IAMClient.swift | 158 +++ .../GeneratedClients/IAM/IAMErrors.swift | 86 ++ .../GeneratedClients/IAM/IAMShapes.swift | 216 +++ .../Lambda/LambdaClient.swift | 187 +++ .../Lambda/LambdaErrors.swift | 74 + .../Lambda/LambdaShapes.swift | 574 ++++++++ .../GeneratedClients/S3/S3Client.swift | 129 ++ .../GeneratedClients/S3/S3Errors.swift | 72 + .../GeneratedClients/S3/S3Shapes.swift | 182 +++ .../GeneratedClients/STS/STSClient.swift | 83 ++ .../GeneratedClients/STS/STSErrors.swift | 75 + .../GeneratedClients/STS/STSShapes.swift | 49 + Sources/AWSLambdaPluginHelper/Process.swift | 2 +- .../Vendored/crypto/Array+Extensions.swift | 171 --- .../Vendored/crypto/Authenticator.swift | 35 - .../Vendored/crypto/BatchedCollections.swift | 102 -- .../Vendored/crypto/Bit.swift | 41 - .../crypto/Collections+Extensions.swift | 75 - .../Vendored/crypto/Digest.swift | 93 -- .../Vendored/crypto/DigestType.swift | 33 - .../Vendored/crypto/Generics.swift | 58 - .../Vendored/crypto/HMAC.swift | 140 -- .../Vendored/crypto/Int+Extension.swift | 48 - .../Vendored/crypto/NoPadding.swift | 42 - .../Vendored/crypto/Padding.swift | 55 - .../Vendored/crypto/SHA1.swift | 179 --- .../Vendored/crypto/SHA2.swift | 417 ------ .../Vendored/crypto/SHA3.swift | 317 ----- .../Vendored/crypto/UInt16+Extension.swift | 52 - .../Vendored/crypto/UInt32+Extension.swift | 66 - .../Vendored/crypto/UInt64+Extension.swift | 59 - .../Vendored/crypto/UInt8+Extension.swift | 89 -- .../Vendored/crypto/Updatable.swift | 130 -- .../Vendored/crypto/Utils.swift | 146 -- .../Vendored/crypto/ZeroPadding.swift | 55 - .../Vendored/signer/AWSCredentials.swift | 61 - .../Vendored/signer/AWSSigner.swift | 362 ----- .../lambda-build/Builder.swift | 206 ++- .../lambda-deploy/Deployer.swift | 1209 ++++++++++++++++- .../lambda-init/Initializer.swift | 110 +- .../AWSLambdaRuntime/Docs.docc/Deployment.md | 219 ++- .../Resources/code/04-01-02-plugin-archive.sh | 2 +- .../Resources/code/04-01-03-plugin-archive.sh | 4 +- .../Resources/code/04-01-04-plugin-archive.sh | 4 +- .../AWSLambdaRuntime/Docs.docc/quick-setup.md | 39 +- .../tutorials/03-write-function.tutorial | 2 +- .../tutorials/04-deploy-function.tutorial | 8 +- .../AWSSignerTests.swift | 90 -- .../BuilderConfigurationTests.swift | 210 +++ .../CryptoTests.swift | 79 -- .../DeployerConfigurationTests.swift | 229 ++++ .../DeployerS3Tests.swift | 143 ++ .../Placeholder.swift | 3 + .../PropertyTests.swift | 614 +++++++++ readme.md | 78 +- scripts/generate-aws-clients.sh | 349 +++++ scripts/integration-test.sh | 320 +++++ 94 files changed, 6832 insertions(+), 3788 deletions(-) rename .kiro/specs/{v4-plugin-system => lambda-v2-plugins}/.config.kiro (57%) create mode 100644 .kiro/specs/lambda-v2-plugins/design.md create mode 100644 .kiro/specs/lambda-v2-plugins/requirements.md create mode 100644 .kiro/specs/lambda-v2-plugins/tasks.md delete mode 100755 Examples/_MyFirstFunction/create_and_deploy_function.sh create mode 100644 Plugins/AWSLambdaPackager/Plugin.swift rename Sources/AWSLambdaPluginHelper/{Vendored/spm => }/ArgumentExtractor.swift (100%) create mode 100644 Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMClient.swift create mode 100644 Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMErrors.swift create mode 100644 Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMShapes.swift create mode 100644 Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaClient.swift create mode 100644 Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaErrors.swift create mode 100644 Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaShapes.swift create mode 100644 Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Client.swift create mode 100644 Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Errors.swift create mode 100644 Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Shapes.swift create mode 100644 Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSClient.swift create mode 100644 Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSErrors.swift create mode 100644 Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSShapes.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Array+Extensions.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Authenticator.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/BatchedCollections.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Bit.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Collections+Extensions.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Digest.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/DigestType.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Generics.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Int+Extension.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/NoPadding.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA1.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA3.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt16+Extension.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt32+Extension.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt64+Extension.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt8+Extension.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Updatable.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/Utils.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/crypto/ZeroPadding.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/signer/AWSCredentials.swift delete mode 100644 Sources/AWSLambdaPluginHelper/Vendored/signer/AWSSigner.swift delete mode 100644 Tests/AWSLambdaPluginHelperTests/AWSSignerTests.swift create mode 100644 Tests/AWSLambdaPluginHelperTests/BuilderConfigurationTests.swift delete mode 100644 Tests/AWSLambdaPluginHelperTests/CryptoTests.swift create mode 100644 Tests/AWSLambdaPluginHelperTests/DeployerConfigurationTests.swift create mode 100644 Tests/AWSLambdaPluginHelperTests/DeployerS3Tests.swift create mode 100644 Tests/AWSLambdaPluginHelperTests/Placeholder.swift create mode 100644 Tests/AWSLambdaPluginHelperTests/PropertyTests.swift create mode 100755 scripts/generate-aws-clients.sh create mode 100755 scripts/integration-test.sh diff --git a/.kiro/specs/v4-plugin-system/.config.kiro b/.kiro/specs/lambda-v2-plugins/.config.kiro similarity index 57% rename from .kiro/specs/v4-plugin-system/.config.kiro rename to .kiro/specs/lambda-v2-plugins/.config.kiro index 746d1820d..e36f7cf1c 100644 --- a/.kiro/specs/v4-plugin-system/.config.kiro +++ b/.kiro/specs/lambda-v2-plugins/.config.kiro @@ -1 +1 @@ -{"specId": "0116fa61-9ac9-44ac-bbc5-fc0802663a4b", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file +{"specId": "0116fa61-9ac9-44ac-bbc5-fc0802663a4b", "workflowType": "requirements-first", "specType": "feature"} diff --git a/.kiro/specs/lambda-v2-plugins/design.md b/.kiro/specs/lambda-v2-plugins/design.md new file mode 100644 index 000000000..129c9396e --- /dev/null +++ b/.kiro/specs/lambda-v2-plugins/design.md @@ -0,0 +1,609 @@ +# Design Document: Lambda V2 Plugins + +## Overview + +This design covers the v4 plugin system for `swift-aws-lambda-runtime`, delivering three SwiftPM command plugins (`lambda-init`, `lambda-build`, `lambda-deploy`) and a legacy `archive` passthrough. All functional logic lives in a single shared executable target (`AWSLambdaPluginHelper`) that dispatches to `Initializer`, `Builder`, or `Deployer` based on the first argument. The deploy command is the primary new implementation, leveraging Soto Core for AWS credential management, SigV4 signing, and HTTP transport, with generated service clients (Lambda, IAM, S3, STS) committed to the repository. + +Key design changes from the current implementation: +- Default base image moves from `amazonlinux2` to `amazonlinux2023` +- The blanket AL2 deprecation warning is removed; an informational warning is emitted only when AL2 is explicitly chosen +- `--container-cli` is replaced by a unified `--cross-compile` option accepting `docker`, `container`, `swift-static-sdk`, `custom-sdk` +- Default binary stripping with `-Xlinker -s` and `--no-strip` opt-out +- `--output-directory` is accepted as a deprecated alias for `--output-path` +- Vendored crypto/signer/HTTP code under `Vendored/` is removed in favor of `soto-core` +- The `archive` plugin is uncommented and shares sources with `AWSLambdaBuilder` + +## Architecture + +```mermaid +graph TD + subgraph "SwiftPM Plugins (thin wrappers)" + INIT["AWSLambdaInitializer
verb: lambda-init"] + BUILD["AWSLambdaBuilder
verb: lambda-build"] + DEPLOY["AWSLambdaDeployer
verb: lambda-deploy"] + ARCHIVE["AWSLambdaPackager
verb: archive (deprecated)"] + end + + subgraph "Shared Executable Target" + HELPER["AWSLambdaPluginHelper
argv[1] dispatch"] + INITIALIZER["Initializer"] + BUILDER["Builder"] + DEPLOYER["Deployer"] + end + + subgraph "Dependencies" + SOTO["SotoCore
(credentials, signing, HTTP)"] + NIO["SwiftNIO
(NIOCore, NIOHTTP1)"] + end + + subgraph "Generated Clients (committed)" + LAMBDA_CLIENT["LambdaClient"] + IAM_CLIENT["IAMClient"] + S3_CLIENT["S3Client"] + STS_CLIENT["STSClient"] + end + + subgraph "AWS" + LAMBDA_API["AWS Lambda"] + IAM_API["AWS IAM"] + S3_API["AWS S3"] + STS_API["AWS STS"] + end + + INIT -->|spawn| HELPER + BUILD -->|spawn| HELPER + DEPLOY -->|spawn| HELPER + ARCHIVE -->|spawn + deprecation warning| HELPER + + HELPER --> INITIALIZER + HELPER --> BUILDER + HELPER --> DEPLOYER + + DEPLOYER --> SOTO + DEPLOYER --> LAMBDA_CLIENT + DEPLOYER --> IAM_CLIENT + DEPLOYER --> S3_CLIENT + DEPLOYER --> STS_CLIENT + + BUILDER --> NIO + + LAMBDA_CLIENT --> LAMBDA_API + IAM_CLIENT --> IAM_API + S3_CLIENT --> S3_API + STS_CLIENT --> STS_API +``` + +### Plugin → Helper Communication + +Each plugin wrapper: +1. Resolves `PluginContext`-only values (tool paths, package ID/name/directory, products, configuration) +2. Spawns `AWSLambdaPluginHelper` as a subprocess with the command verb as `argv[1]` +3. Checks exit status: non-zero → `Diagnostics.error(...)` + immediate halt + +### Helper Dispatch Bug Fix + +The current `command(from:)` method has a guard `args.count > 2` which is too strict — the minimum valid invocation is `[binary_path, command]` (count == 2). The fix changes the guard to `args.count > 1` and reads `args[1]` for the command name. The remaining arguments (`Array(args.dropFirst(2))`) are passed to the subcommand handler. + +## Components and Interfaces + +### 1. Plugin Wrappers + +#### AWSLambdaInitializer (`Plugins/AWSLambdaInitializer/Plugin.swift`) + +Thin wrapper. Resolves `--dest-dir` from `context.package.directoryURL`. Passes `["init", "--dest-dir", path] + arguments` to the helper. + +#### AWSLambdaBuilder (`Plugins/AWSLambdaBuilder/Plugin.swift`) + +Resolves output path, products, configuration, tool paths (docker/zip). Passes `["build", ...resolved, ...user_args]` to the helper. + +#### AWSLambdaDeployer (`Plugins/AWSLambdaDeployer/Plugin.swift`) + +Resolves products list (to determine executable target name). Passes `["deploy", "--products", targetNames, ...user_args]` to the helper. + +#### AWSLambdaPackager (legacy `archive` alias) + +Implemented as a **separate plugin directory** (`Plugins/AWSLambdaPackager/Plugin.swift`) because SwiftPM does not allow two plugin targets to share the same source path (it rejects them with an "overlapping sources" error). The plugin has its own thin `Plugin.swift` that: +1. Emits a deprecation warning via `Diagnostics.warning("'archive' is deprecated. Please use 'swift package lambda-build' instead.")` +2. Spawns the `AWSLambdaPluginHelper` executable with `["build"] + arguments` (same delegation as `AWSLambdaBuilder`) +3. Checks exit status and reports a diagnostic error on failure + +This was validated at compile time — `swift package describe` confirms both `archive` (verb) and `lambda-build` (verb) resolve to valid, independent plugins without source overlap. + +### 2. Initializer (`Sources/AWSLambdaPluginHelper/lambda-init/`) + +**Existing implementation preserved.** Files: `Initializer.swift`, `Template.swift`. + +Behavior: Writes `Default_Template` or `URL_Template` (when `--with-url`) to `Sources/main.swift` in the destination directory. + +No changes required — current implementation satisfies all Requirement 1 criteria. + +### 3. Builder (`Sources/AWSLambdaPluginHelper/lambda-build/`) + +**Existing implementation preserved with minimal modifications.** + +#### Changes Required + +| Change | Requirement | Description | +|--------|------------|-------------| +| Default image | 6.1 | `baseDockerImage` default: `"swift:\(version)-amazonlinux2023"` (was `amazonlinux2`) | +| Remove blanket warning | 6.2 | Remove the unconditional `displayDeprecationWarning()` call for all AL2 scenarios | +| AL2 informational warning | 6.3 | Emit a shorter informational warning ONLY when `--base-docker-image` explicitly contains `amazonlinux2` (not `amazonlinux2023`) | +| Strip by default | 2.5 | Add `-Xlinker -s` to both native and Docker build commands | +| `--no-strip` opt-out | 2.6 | When flag present, omit the strip linker flags | +| Unified `--cross-compile` | 2.7 | Replace `--container-cli` with `--cross-compile` accepting `docker\|container\|swift-static-sdk\|custom-sdk` | +| Unsupported methods | 2.14 | `swift-static-sdk`/`custom-sdk` → error with SDK_Installation_Guide link | +| `--output-directory` alias | 7.5/7.6 | Accept as deprecated alias for `--output-path` | +| Container CLI validation | 2.11/2.12 | Check CLI exists; if not → error with download page URL | + +#### BuilderConfiguration Changes + +```swift +// New cross-compile enum replaces ContainerCLI +enum CrossCompileMethod: String { + case docker + case container + case swiftStaticSdk = "swift-static-sdk" + case customSdk = "custom-sdk" +} + +// In BuilderConfiguration.init: +// 1. Extract --cross-compile (default: "docker") +// 2. Extract --no-strip flag +// 3. Extract --output-directory as alias for --output-path +// 4. Default baseDockerImage → "swift:-amazonlinux2023" +``` + +### 4. Deployer (`Sources/AWSLambdaPluginHelper/lambda-deploy/`) + +**New implementation.** The existing stub is replaced. + +#### DeployerConfiguration + +```swift +struct DeployerConfiguration { + let help: Bool + let verboseLogging: Bool + let withURL: Bool + let delete: Bool + let region: String? // nil → resolved by Soto + let iamRole: String? // nil → create new role + let inputDirectory: URL? // nil → default build output path + let architecture: Architecture // .host by default + let products: [String] // from plugin wrapper + + enum Architecture: String { + case x64, arm64 + static var host: Architecture { + #if arch(x86_64) + return .x64 + #else + return .arm64 + #endif + } + } +} +``` + +#### Deploy Orchestration + +```swift +struct Deployer { + func deploy(arguments: [String]) async throws { + let config = try DeployerConfiguration(arguments: arguments) + if config.help { displayHelpMessage(); return } + + // 1. Initialize Soto AWSClient (credential provider chain) + // 2. Warn if ~/.aws/config missing (non-blocking) + // 3. Resolve account ID via STS GetCallerIdentity + // 4. Determine function name from product/target + // 5. Check if function exists (GetFunction) + // 6. If --delete: teardown and return + // 7. Ensure IAM role exists + // 8. Upload code (direct or via S3 staging) + // 9. Create or update function + // 10. If --with-url: configure Function URL + // 11. Report deployment success: + // - Function ARN and region + // - If --with-url: Function URL + ready-to-use curl --aws-sigv4 command + // - If no URL: ready-to-use aws lambda invoke command + // 12. Shutdown AWSClient + } +} +``` + +### 5. Generated AWS Service Clients + +Located at: `Sources/AWSLambdaPluginHelper/GeneratedClients/` + +Structure: +``` +GeneratedClients/ +├── Lambda/ +│ ├── LambdaClient.swift +│ ├── LambdaShapes.swift +│ └── LambdaErrors.swift +├── IAM/ +│ ├── IAMClient.swift +│ ├── IAMShapes.swift +│ └── IAMErrors.swift +├── S3/ +│ ├── S3Client.swift +│ ├── S3Shapes.swift +│ └── S3Errors.swift +└── STS/ + ├── STSClient.swift + ├── STSShapes.swift + └── STSErrors.swift +``` + +Each client is a lightweight struct wrapping `AWSClient` from SotoCore: + +```swift +struct LambdaClient { + let client: AWSClient + let region: Region + + func getFunction(_ input: GetFunctionRequest) async throws -> GetFunctionResponse + func createFunction(_ input: CreateFunctionRequest) async throws -> CreateFunctionResponse + func updateFunctionCode(_ input: UpdateFunctionCodeRequest) async throws -> UpdateFunctionCodeResponse + func deleteFunction(_ input: DeleteFunctionRequest) async throws + func createFunctionUrlConfig(_ input: CreateFunctionUrlConfigRequest) async throws -> CreateFunctionUrlConfigResponse + func deleteFunctionUrlConfig(_ input: DeleteFunctionUrlConfigRequest) async throws + func addPermission(_ input: AddPermissionRequest) async throws -> AddPermissionResponse + func removePermission(_ input: RemovePermissionRequest) async throws +} +``` + +### 6. Generation Script + +Located at: `scripts/generate-aws-clients.sh` + +One-time, maintainer-run script that: +1. Clones Soto Code Generator +2. Downloads AWS service model JSON files for Lambda, IAM, S3, STS +3. Runs the generator with a config specifying only needed operations +4. Copies output to `Sources/AWSLambdaPluginHelper/GeneratedClients/` + +The script is NOT part of the build. If generated files are missing, the build fails with a compile error. + +## Data Models + +### Deploy State Machine + +```swift +enum DeploymentAction { + case create(CreateFunctionRequest) + case update(UpdateFunctionCodeRequest) + case delete(functionName: String) +} + +struct DeploymentContext { + let accountId: String + let region: Region + let functionName: String + let architecture: DeployerConfiguration.Architecture + let zipArchiveURL: URL + let zipArchiveSize: Int64 + let iamRoleARN: String + let action: DeploymentAction +} +``` + +### Bucket Name Construction + +```swift +/// Constructs the deployment bucket name per the naming convention. +/// Format: `swift-aws-lambda-runtime--` +static func deploymentBucketName(region: String, accountId: String) -> String { + "swift-aws-lambda-runtime-\(region)-\(accountId)" +} +``` + +### Archive Size Threshold + +```swift +/// AWS Lambda direct upload limit (50 MB compressed). +static let directUploadLimit: Int64 = 50 * 1024 * 1024 +``` + +### Cross-Compile Configuration + +```swift +enum CrossCompileMethod: String, CustomStringConvertible { + case docker + case container + case swiftStaticSdk = "swift-static-sdk" + case customSdk = "custom-sdk" + + var isSupported: Bool { + switch self { + case .docker, .container: return true + case .swiftStaticSdk, .customSdk: return false + } + } + + var description: String { rawValue } +} +``` + +## Deploy Orchestration Sequence + +```mermaid +sequenceDiagram + participant Dev as Developer + participant Plugin as AWSLambdaDeployer + participant Helper as Plugin Helper (deploy) + participant Soto as SotoCore / AWSClient + participant STS as AWS STS + participant IAM as AWS IAM + participant S3 as AWS S3 + participant Lambda as AWS Lambda + + Dev->>Plugin: swift package lambda-deploy --with-url + Plugin->>Helper: spawn ["deploy", "--products", "MyFunc", "--with-url"] + + Helper->>Soto: Initialize AWSClient (credential chain) + Note over Helper: Warn if ~/.aws/config absent (non-blocking) + + Helper->>STS: GetCallerIdentity + STS-->>Helper: account-id + + Helper->>Lambda: GetFunction("MyFunc") + alt Function does not exist + Helper->>IAM: CreateRole("swift-lambda-MyFunc-role") + IAM-->>Helper: role ARN + Helper->>IAM: AttachRolePolicy(AWSLambdaBasicExecutionRole) + Note over Helper: Wait for role propagation + + alt ZIP ≤ 50 MB + Helper->>Lambda: CreateFunction(ZipFile: base64) + else ZIP > 50 MB + Helper->>S3: HeadBucket("swift-aws-lambda-runtime--") + alt Bucket missing + Helper->>S3: CreateBucket + end + Helper->>S3: PutObject(zip) + Helper->>Lambda: CreateFunction(S3Bucket, S3Key) + Helper->>S3: DeleteObject(zip) + end + + Lambda-->>Helper: function ARN + + Helper->>Lambda: CreateFunctionUrlConfig(AuthType: AWS_IAM) + Lambda-->>Helper: Function URL + else Function exists + alt ZIP ≤ 50 MB + Helper->>Lambda: UpdateFunctionCode(ZipFile: base64) + else ZIP > 50 MB + Helper->>S3: stage via S3 (same as create path) + Helper->>Lambda: UpdateFunctionCode(S3Bucket, S3Key) + Helper->>S3: DeleteObject(zip) + end + end + + Note over Helper: Report: function ARN, region + alt --with-url used + Note over Helper: Report: Function URL + curl --aws-sigv4 command + else no URL + Note over Helper: Report: aws lambda invoke command + end + + Helper->>Soto: shutdown AWSClient + Helper-->>Plugin: exit 0 + Plugin-->>Dev: Function URL / invoke command +``` + +## Package.swift Changes + +```swift +// Add soto-core dependency +dependencies: [ + .package(url: "https://github.com/apple/swift-nio.git", from: "2.99.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.12.0"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.5.0"), + .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.11.0"), + .package(url: "https://github.com/soto-project/soto-core.git", from: "7.0.0"), // NEW +], + +// Update AWSLambdaPluginHelper dependencies +.executableTarget( + name: "AWSLambdaPluginHelper", + dependencies: [ + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "SotoCore", package: "soto-core"), // NEW + ], + swiftSettings: defaultSwiftSettings +), + +// Uncomment archive alias plugin (separate directory, NOT shared path) +// SwiftPM rejects shared-path with "overlapping sources" error. +// Instead: Plugins/AWSLambdaPackager/Plugin.swift with its own thin wrapper. +.plugin( + name: "AWSLambdaPackager", + capability: .command( + intent: .custom( + verb: "archive", + description: + "Archive the Lambda binary and prepare it for uploading to AWS. (Deprecated: use lambda-build instead)" + ), + permissions: [ + .allowNetworkConnections( + scope: .docker, + reason: "This plugin uses Docker to create the AWS Lambda ZIP package." + ) + ] + ), + dependencies: [ + .target(name: "AWSLambdaPluginHelper") + ] + // No `path:` — uses default Plugins/AWSLambdaPackager/ directory +), +``` + +## Correctness Properties + +*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* + +### Property 1: ZIP archive always contains bootstrap binary + +*For any* compiled product with any valid name, when the packaging step produces a ZIP archive, the archive SHALL contain a file named `bootstrap` (the renamed binary) regardless of the original product name. + +**Validates: Requirements 2.3** + +### Property 2: Deprecated option alias equivalence + +*For any* path value supplied via `--output-directory`, the resulting `BuilderConfiguration.outputDirectory` SHALL be identical to the value produced when the same path is supplied via `--output-path`. + +**Validates: Requirements 7.5, 7.6** + +### Property 3: Cross-compile method parsing round-trip + +*For any* valid `CrossCompileMethod` enum case, converting the case to its `rawValue` string and parsing it back SHALL produce the original enum case. + +**Validates: Requirements 2.7** + +### Property 4: Mutual exclusion of --swift-version and --base-docker-image + +*For any* non-empty swift-version string and any non-empty base-docker-image string supplied together, argument parsing SHALL throw a mutually-exclusive-argument error. + +**Validates: Requirements 2.17** + +### Property 5: Deployment bucket name construction + +*For any* valid AWS region string and any valid 12-digit account ID, `deploymentBucketName(region:accountId:)` SHALL produce the string `"swift-aws-lambda-runtime--"` and the result SHALL always be a valid S3 bucket name (lowercase, no uppercase, 3-63 chars). + +**Validates: Requirements 3.17, 3.18** + +### Property 6: Archive size determines upload strategy + +*For any* ZIP archive size, if the size is ≤ `directUploadLimit` (50 MB) then the deploy logic SHALL choose direct upload; if the size is > `directUploadLimit` then it SHALL choose S3 staging. + +**Validates: Requirements 3.15, 3.19** + +### Property 7: Non-zero helper exit causes plugin wrapper halt + +*For any* plugin wrapper (Initializer, Builder, Deployer) and any non-zero exit code from the helper subprocess, the wrapper SHALL emit a `Diagnostics.error` and SHALL NOT continue with further work. + +**Validates: Requirements 5.6** + +### Property 8: AL2 warning emitted only for explicit AL2 image selection + +*For any* base-docker-image value containing "amazonlinux2" but NOT "amazonlinux2023", the builder SHALL emit the AL2 informational deprecation warning. *For any* base-docker-image value containing "amazonlinux2023" or when using the default image, the builder SHALL NOT emit the AL2 warning. + +**Validates: Requirements 6.2, 6.3** + +### Property 9: Unsupported cross-compile methods report error with link + +*For any* cross-compile value in {`swift-static-sdk`, `custom-sdk`}, the builder SHALL report a "not yet supported" error that contains a URL to the SDK_Installation_Guide. + +**Validates: Requirements 2.14** + +### Property 10: Host architecture default + +*For any* host machine architecture (x64 or arm64), when `--architecture` is omitted, the deployer SHALL set the Lambda function architecture to match the host. + +**Validates: Requirements 3.13** + +## Error Handling + +### Error Categories and Responses + +| Category | Example | Response | +|----------|---------|----------| +| Argument validation | Mutually exclusive options | Print error, exit non-zero immediately | +| Missing tool | Docker/container CLI not found | Print error with download URL, exit non-zero | +| Unsupported method | `--cross-compile swift-static-sdk` | Print "not yet supported" with SDK guide link, exit non-zero | +| Build failure | Compilation error in container | Stream compiler output, exit non-zero | +| Product not found | Binary missing after build | Print expected path, exit non-zero | +| Credential failure | No AWS credentials resolved | Print descriptive error, exit non-zero | +| AWS API error | CreateFunction returns 4xx/5xx | Print AWS error message and code, exit non-zero | +| File I/O error | Cannot write template | Print OS error description, exit non-zero | +| Non-blocking warning | ~/.aws/config absent | Print informational warning, CONTINUE | +| Non-blocking warning | AL2 image explicitly chosen | Print deprecation notice, CONTINUE with build | + +### Error Propagation Strategy + +1. **Helper → Plugin**: Non-zero exit code. Plugin reads exit status and emits `Diagnostics.error(...)`. +2. **Within Helper**: Swift `throw` with typed errors (`BuilderErrors`, `DeployerErrors`). Top-level `main()` catches and prints, then calls `exit(1)`. +3. **AWS errors**: Soto throws `AWSClientError` or `AWSResponseError`; the deployer catches, formats the message (including request ID when available), and re-throws as `DeployerErrors.awsError(...)`. + +### Deployer-Specific Errors + +```swift +enum DeployerErrors: Error, CustomStringConvertible { + case credentialResolutionFailed(String) + case awsAPIError(service: String, operation: String, message: String) + case archiveNotFound(URL) + case invalidArchitecture(String) + case functionURLCreationFailed(String) + case iamRoleCreationFailed(String) + case missingProduct +} +``` + +## Testing Strategy + +### Unit Tests (Swift Testing framework) + +Located at: `Tests/AWSLambdaPluginHelperTests/` + +Test categories: +- **Argument parsing**: Verify `BuilderConfiguration` and `DeployerConfiguration` parse all options correctly, handle defaults, detect mutual exclusions, and map deprecated aliases +- **Cross-compile method parsing**: Round-trip enum ↔ string, invalid values +- **Bucket name construction**: Various region/account combinations, validate S3 naming rules +- **Archive size threshold**: Boundary values (exactly 50MB, 50MB+1, 0) +- **Host architecture detection**: Verify correct enum value on current platform +- **AL2 image detection**: Various image strings, boundary between AL2 and AL2023 + +### Property-Based Tests (SwiftCheck or swift-testing parameterized) + +Configuration: Minimum 100 iterations per property test. + +Each property test references its design document property via tag comment: +```swift +// Feature: lambda-v2-plugins, Property 2: Deprecated option alias equivalence +@Test(arguments: samplePaths) +func deprecatedAliasEquivalence(path: String) { ... } +``` + +Property-based testing library: Use Swift Testing's `@Test(arguments:)` with generated input collections for parameterized testing, since the input domains are finite and well-bounded (path strings, enum cases, size values). + +### End-to-End Test (Shell Script) + +Located at: `scripts/integration-test.sh` + +Flow: +```bash +#!/bin/bash +set -euo pipefail + +FUNCTION_NAME="swift-lambda-e2e-test-$(date +%s)" +CLEANUP_NEEDED=false + +cleanup() { + if [ "$CLEANUP_NEEDED" = true ]; then + swift package lambda-deploy --delete --products "$FUNCTION_NAME" || true + fi +} +trap cleanup EXIT + +# 1. Create temp directory, init swift package +# 2. swift package lambda-init --with-url +# 3. swift package --allow-network-connections docker lambda-build +# 4. swift package --allow-network-connections all lambda-deploy --with-url +CLEANUP_NEEDED=true +# 5. Extract Function URL from output +# 6. curl --aws-sigv4 "aws:amz::lambda" \ +# --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \ +# -H "x-amz-security-token: $AWS_SESSION_TOKEN" \ +# "$FUNCTION_URL?name=World" +# 7. Verify response contains expected message +# 8. swift package lambda-deploy --delete +CLEANUP_NEEDED=false +``` + +Key aspects: +- Uses `trap cleanup EXIT` to guarantee resource cleanup on failure (Req 9.10) +- Function URL uses `AWS_IAM` auth, validated via `curl --aws-sigv4` (Req 9.7, 9.8) +- Verifies response body matches expected output (Req 9.6, 9.11) diff --git a/.kiro/specs/lambda-v2-plugins/requirements.md b/.kiro/specs/lambda-v2-plugins/requirements.md new file mode 100644 index 000000000..a1d6900e4 --- /dev/null +++ b/.kiro/specs/lambda-v2-plugins/requirements.md @@ -0,0 +1,229 @@ +# Requirements Document + +## Introduction + +This feature delivers the v4 plugin system for `swift-aws-lambda-runtime`, replacing the legacy single-purpose `archive` plugin with three focused SwiftPM command plugins that cover the end-to-end developer experience: scaffolding (`lambda-init`), building and packaging (`lambda-build`), and deployment (`lambda-deploy`). The authoritative design source is the v4 proposal (`Plugins/Documentation.docc/Proposals/0001-v4-plugins.md`). + +The three plugins are thin `CommandPlugin` wrappers (`AWSLambdaInitializer`, `AWSLambdaBuilder`, `AWSLambdaDeployer`) that spawn a shared executable target (`AWSLambdaPluginHelper`) which implements the actual `init`, `build`, and `deploy` logic. The v4 proposal changes the deployment strategy to depend on Soto Core for AWS credential management, configuration parsing, and request signing, with AWS service clients generated one-time by the Soto Code Generator and checked into the repository. Vendored crypto, signer, and HTTP client code is removed. + +This release makes Amazon Linux 2023 the default Amazon Linux target and the default base container image (`swift:-amazonlinux2023`), with the associated default Lambda runtime `provided.al2023`. Amazon Linux 2 (AL2) is no longer the default but remains usable when the developer explicitly supplies an AL2 base image through the `--base-docker-image` option. The Amazon Linux 2 deprecation warning is removed. + +The project targets Swift 6 with Swift Package Manager, is developed on macOS 15 or later, and targets the Amazon Linux 2023 (`provided.al2023`) Lambda runtime. + +## Glossary + +- **Plugin_System**: The collective set of three SwiftPM command plugins (`AWSLambdaInitializer`, `AWSLambdaBuilder`, `AWSLambdaDeployer`) and the shared `AWSLambdaPluginHelper` executable target. +- **Plugin_Helper**: The `AWSLambdaPluginHelper` executable target that implements the `init`, `build`, and `deploy` commands and is invoked as a subprocess by each plugin wrapper. +- **Init_Plugin**: The `lambda-init` command (wrapper `AWSLambdaInitializer`) that scaffolds a new Lambda function source file. +- **Build_Plugin**: The `lambda-build` command (wrapper `AWSLambdaBuilder`) that compiles and packages a Lambda function into a deployable ZIP archive. +- **Deploy_Plugin**: The `lambda-deploy` command (wrapper `AWSLambdaDeployer`) that deploys a packaged Lambda function to AWS. +- **Template**: A hardcoded Swift source file scaffolded by the Init_Plugin into `Sources/main.swift`. +- **Default_Template**: The Template that defines a Lambda function receiving a JSON request and returning a JSON response. +- **URL_Template**: The Template that defines a Lambda function invoked through a Lambda Function URL using `FunctionURLRequest` and `FunctionURLResponse`. +- **ZIP_Archive**: The deployment package produced by the Build_Plugin containing the compiled binary renamed to `bootstrap` plus any `.resources` bundles. +- **Cross_Compilation_Method**: The mechanism used to compile a Lambda binary for Amazon Linux 2023 when the build does not run natively on Amazon_Linux_2023. Accepted values are `docker` (cross-compile using Docker), `container` (cross-compile using Apple's `container` CLI, a native OCI image runtime for macOS that does not require Docker Desktop), `swift-static-sdk` (the Swift Static Linux SDK built with musl), and `custom-sdk` (a custom Swift SDK for Amazon Linux). +- **SDK_Installation_Guide**: The published documentation website that explains how to install the Swift Static Linux SDK and the custom Swift SDK for Amazon Linux used by the `swift-static-sdk` and `custom-sdk` Cross_Compilation_Method values. +- **Amazon_Linux_2023**: The Amazon Linux 2023 operating system, used as the default cross-compilation target environment and corresponding to the `provided.al2023` Lambda runtime. +- **Base_Image**: The container image used for Docker-based cross-compilation, defaulting to `swift:-amazonlinux2023` and overridable by the developer through the `--base-docker-image` option. +- **Lambda_Runtime_Identifier**: The default AWS Lambda managed runtime identifier `provided.al2023` used when deploying functions. +- **Soto_Core**: The `soto-core` Swift package providing AWS credential management, configuration file parsing, SigV4 request signing, and HTTP client functionality. +- **Credential_Provider_Chain**: Soto Core's ordered credential resolution: environment variables (including the `AWS_PROFILE` environment variable), AWS configuration files (`~/.aws/credentials` and `~/.aws/config`), ECS container credentials, and EC2 instance metadata service (IMDSv2). Soto Core resolves the active region from the same environment variables and AWS profile configuration. +- **AWS_Configuration_Resolution**: Soto_Core's standard resolution of the AWS region, credentials, and active profile from the environment variables (`AWS_REGION`, `AWS_DEFAULT_REGION`, and `AWS_PROFILE`) and the AWS configuration files (`~/.aws/config` and `~/.aws/credentials`), used instead of any plugin-specific configuration parsing. +- **AWS_Configuration_Files**: The `~/.aws/config` and `~/.aws/credentials` files created by running the AWS CLI `aws configure` command, which store the developer's AWS credentials and default region and are consumed by AWS_Configuration_Resolution and the Credential_Provider_Chain. +- **Generated_Service_Client**: AWS service API client code (Lambda, IAM, S3, and STS) produced one time by the maintainer-run Generation_Script using the Soto Code Generator and committed to the repository, not generated at build time. +- **Generation_Script**: A maintainer-invoked script that runs the Soto Code Generator one time to produce the Generated_Service_Client source code, which is then committed to the repository. The Generation_Script is a manual operation and is not part of the package build. +- **Deployment_Bucket**: The dedicated, reusable Amazon S3 bucket named `swift-aws-lambda-runtime--` used to stage large deployment ZIP_Archive uploads, where `` is the deployment region and `` is the AWS_Account_Identifier. The Deployment_Bucket is created when absent and reused when present. +- **AWS_Account_Identifier**: The AWS account number, resolved through the AWS STS `GetCallerIdentity` API, used as part of the Deployment_Bucket name. +- **Function_Name**: The Lambda function name, derived from the `executableTarget` name defined in `Package.swift`. +- **Function_URL**: An HTTPS endpoint that invokes a Lambda function directly, configured through the AWS Lambda `CreateFunctionUrlConfig` API. The Function_URL uses IAM (`AWS_IAM`) authentication, so callers must sign their requests with AWS Signature Version 4 rather than calling the endpoint anonymously. +- **IAM_Role**: The AWS Identity and Access Management role assumed by the deployed Lambda function during execution. +- **Host_Architecture**: The CPU architecture of the machine on which the Deploy_Plugin runs, expressed as `x64` or `arm64`. +- **Legacy_Archive_Plugin**: The existing `archive` SwiftPM command plugin (verb `archive`) provided before the v4 plugin system, which this release retains as a passthrough to the Build_Plugin. +- **Deprecated_Option_Alias**: A Legacy_Archive_Plugin option name (such as `--output-directory`) that the Build_Plugin still accepts and maps to its current replacement option name (such as `--output-path`), supported for backward compatibility but not the primary documented interface. +- **End_To_End_Test_Suite**: The automated test, implemented as a shell script, that scaffolds, builds, deploys, validates, and deletes a Lambda function to validate the full plugin lifecycle end to end against AWS. The suite uses the URL function variant (the URL_Template) and validates the deployed function by sending an AWS Signature Version 4 signed `curl` request to its IAM-protected Function_URL. + +## Requirements + +### Requirement 1: Scaffold a new Lambda function (lambda-init) + +**User Story:** As a Swift developer new to AWS Lambda, I want to scaffold a ready-to-build Lambda function from a template, so that I can start a new project without writing boilerplate by hand. + +#### Acceptance Criteria + +1. WHEN the Init_Plugin is invoked without a template option, THE Plugin_Helper SHALL write the Default_Template to `Sources/main.swift`. +2. WHEN the Init_Plugin is invoked with the `--with-url` option, THE Plugin_Helper SHALL write the URL_Template to `Sources/main.swift`. +3. THE Default_Template SHALL define a Lambda function that decodes a JSON request into a `Decodable` type and returns an `Encodable` JSON response. +4. THE URL_Template SHALL define a Lambda function that accepts a `FunctionURLRequest` and returns a `FunctionURLResponse`. +5. WHERE the `--allow-writing-to-package-directory` option is provided, THE Init_Plugin SHALL write to the package directory without requesting interactive permission. +6. WHEN the Init_Plugin is invoked with the `--verbose` option, THE Plugin_Helper SHALL emit the destination path of the created file. +7. WHEN the Init_Plugin is invoked with the `--help` option, THE Plugin_Helper SHALL display usage information and SHALL NOT write any file. +8. IF writing the Template to `Sources/main.swift` fails, THEN THE Plugin_Helper SHALL report a descriptive error identifying the failure. +9. WHEN the Init_Plugin completes successfully, THE Plugin_Helper SHALL report the path of the created file and the next command to package the function. + +### Requirement 2: Build and package the Lambda function (lambda-build) + +**User Story:** As a Lambda developer, I want to compile my function for Amazon Linux 2023 and package it as a deployment ZIP, so that I can upload an artifact that runs on AWS Lambda. + +#### Acceptance Criteria + +1. WHEN the Build_Plugin is invoked on Amazon_Linux_2023, THE Plugin_Helper SHALL compile the configured products natively using `swift build` with the `--static-swift-stdlib` flag. +2. WHILE running on a platform other than Amazon_Linux_2023, THE Plugin_Helper SHALL compile the configured products using the selected Cross_Compilation_Method. +3. WHEN packaging a compiled product, THE Plugin_Helper SHALL produce a ZIP_Archive containing the product binary renamed to `bootstrap`. +4. WHEN packaging a compiled product that has associated `.resources` bundles, THE Plugin_Helper SHALL include each `.resources` bundle in the ZIP_Archive. +5. WHEN compiling a product, THE Plugin_Helper SHALL pass the linker flags `-Xlinker -s` to strip debug symbols from the binary. +6. WHERE the `--no-strip` option is provided, THE Plugin_Helper SHALL compile the product without the debug-symbol-stripping linker flags. +7. THE Build_Plugin SHALL accept a `--cross-compile` option that selects the Cross_Compilation_Method from the values `docker`, `container`, `swift-static-sdk`, and `custom-sdk`, defaulting to `docker`. +8. WHEN the `--cross-compile` option is omitted, THE Plugin_Helper SHALL use the `docker` Cross_Compilation_Method. +9. WHERE the Cross_Compilation_Method is `docker`, THE Plugin_Helper SHALL execute the compilation in a container started with Docker. +10. WHERE the Cross_Compilation_Method is `container`, THE Plugin_Helper SHALL execute the compilation in a container started with Apple's `container` CLI. +11. IF the Cross_Compilation_Method is `docker` and the Docker CLI cannot be located, THEN THE Plugin_Helper SHALL report that Docker is not installed and SHALL direct the user to the Docker download and installation page. +12. IF the Cross_Compilation_Method is `container` and Apple's `container` CLI cannot be located, THEN THE Plugin_Helper SHALL report that the `container` CLI is not installed and SHALL direct the user to the Apple `container` CLI download and installation page. +13. THE Plugin_Helper SHALL support the `docker` and `container` Cross_Compilation_Method values. +14. WHEN the `--cross-compile` option selects `swift-static-sdk` or `custom-sdk`, THE Plugin_Helper SHALL report that the selected Cross_Compilation_Method is not yet supported and SHALL direct the user to the SDK_Installation_Guide describing how to install the corresponding SDK. +15. WHEN the Cross_Compilation_Method is `container`, THE Plugin_Helper SHALL pull the Base_Image and run the build using the image-pull and run command forms appropriate to the Apple `container` CLI. +16. WHEN the `--base-docker-image` option is omitted, THE Plugin_Helper SHALL use `swift:-amazonlinux2023` as the Base_Image. +17. IF both the `--swift-version` option and the `--base-docker-image` option are provided, THEN THE Plugin_Helper SHALL report a mutually-exclusive-argument error. +18. WHEN the `--disable-docker-image-update` option is omitted and the Cross_Compilation_Method is `docker` or `container`, THE Plugin_Helper SHALL pull the Base_Image before compiling. +19. WHERE the `--disable-docker-image-update` option is provided, THE Plugin_Helper SHALL compile without pulling the Base_Image. +20. THE Build_Plugin SHALL accept the options `--output-path`, `--products`, `--configuration`, `--swift-version`, `--verbose`, and `--help` with the same meaning as the existing `archive` plugin. +21. WHEN the `--configuration` option is omitted, THE Plugin_Helper SHALL build using the `release` configuration. +22. IF a configured product binary is absent after compilation, THEN THE Plugin_Helper SHALL report that the product executable was not found. +23. WHEN the Build_Plugin completes successfully, THE Plugin_Helper SHALL report the count of created archives and the path of each ZIP_Archive. + +### Requirement 3: Deploy the Lambda function to AWS (lambda-deploy) + +**User Story:** As a Lambda developer, I want to deploy my packaged function to AWS from the command line, so that I can run my function in the cloud without configuring separate deployment tooling. + +#### Acceptance Criteria + +1. WHEN the Deploy_Plugin is invoked and no Lambda function with the Function_Name exists, THE Plugin_Helper SHALL create the function using the AWS Lambda `CreateFunction` API with the `provided.al2023` Lambda_Runtime_Identifier. +2. WHEN the Deploy_Plugin is invoked and a Lambda function with the Function_Name already exists, THE Plugin_Helper SHALL update the function code using the AWS Lambda `UpdateFunctionCode` API. +3. THE Plugin_Helper SHALL determine the Function_Name from the `executableTarget` name defined in `Package.swift`. +4. WHEN the Deploy_Plugin is invoked with the `--delete` option, THE Plugin_Helper SHALL delete the Lambda function using the `DeleteFunction` API and delete its associated IAM_Role. +5. WHEN the Deploy_Plugin creates a Lambda function and no `--iam-role` option is provided, THE Plugin_Helper SHALL create a new IAM_Role with the permissions required for Lambda execution. +6. WHERE the `--iam-role` option is provided, THE Plugin_Helper SHALL configure the Lambda function to use the specified IAM_Role. +7. WHERE the `--with-url` option is provided, THE Plugin_Helper SHALL configure a Function_URL using the `CreateFunctionUrlConfig` API with the `AWS_IAM` auth type, so that the Function_URL is protected by IAM authentication and callers must sign requests with AWS Signature Version 4 rather than being granted public, unauthenticated access. +8. WHEN the `--region` option is omitted, THE Plugin_Helper SHALL deploy to the AWS region resolved through AWS_Configuration_Resolution provided by Soto_Core. +9. WHERE the `--region` option is provided, THE Plugin_Helper SHALL deploy to the specified region, overriding the region resolved through AWS_Configuration_Resolution. +10. THE Plugin_Helper SHALL respect the current AWS region, credentials, and profile selection through Soto_Core's AWS_Configuration_Resolution rather than implementing its own configuration parsing. +11. WHEN the `--input-directory` option is omitted, THE Plugin_Helper SHALL read the deployment ZIP_Archive from the default Build_Plugin output path. +12. WHERE the `--input-directory` option is provided, THE Plugin_Helper SHALL read the deployment ZIP_Archive from the specified path. +13. WHEN the `--architecture` option is omitted, THE Plugin_Helper SHALL set the function architecture to the Host_Architecture. +14. WHERE the `--architecture` option is provided with `x64` or `arm64`, THE Plugin_Helper SHALL set the function architecture to the specified value. +15. WHEN the deployment ZIP_Archive size is within the AWS Lambda direct-upload limit, THE Plugin_Helper SHALL deploy the function code as a Base64 payload through the AWS Lambda REST API. +16. THE Plugin_Helper SHALL determine the AWS_Account_Identifier through the AWS STS `GetCallerIdentity` API. +17. WHEN the deployment ZIP_Archive size exceeds the AWS Lambda direct-upload limit AND the Deployment_Bucket does not exist, THE Plugin_Helper SHALL create the Deployment_Bucket named `swift-aws-lambda-runtime--` using the `CreateBucket` API. +18. WHEN the deployment ZIP_Archive size exceeds the AWS Lambda direct-upload limit AND the Deployment_Bucket already exists, THE Plugin_Helper SHALL reuse the Deployment_Bucket without recreating it. +19. WHEN the deployment ZIP_Archive size exceeds the AWS Lambda direct-upload limit, THE Plugin_Helper SHALL upload the ZIP_Archive as an S3 object into the Deployment_Bucket using the `PutObject` API and reference that object during deployment. +20. WHEN the deployment completes, THE Plugin_Helper SHALL delete the uploaded S3 object from the Deployment_Bucket using the `DeleteObject` API while retaining the Deployment_Bucket for reuse. +21. THE Plugin_Helper SHALL resolve AWS credentials using the Credential_Provider_Chain. +22. IF AWS credentials cannot be resolved through the Credential_Provider_Chain, THEN THE Plugin_Helper SHALL report a descriptive credential-resolution error and SHALL fail the deployment. +23. WHEN the Deploy_Plugin runs and the local AWS_Configuration_Files (`~/.aws/config` and `~/.aws/credentials`) are absent, THE Plugin_Helper SHALL emit a non-blocking, informational warning that suggests installing the AWS CLI and running `aws configure`, and SHALL continue the deployment, because credentials may still be resolved from environment variables, ECS or EKS container credentials, or the EC2 instance metadata service (IMDSv2) through the Credential_Provider_Chain; the absence of the local AWS_Configuration_Files alone SHALL NOT block deployment, and the deployment fails only when credentials cannot be resolved through the Credential_Provider_Chain (criterion 3.22) or when an AWS API request returns an error response (criterion 3.24). +24. IF an AWS API request returns an error response, THEN THE Plugin_Helper SHALL report the AWS error to the developer. +25. WHEN the Deploy_Plugin is invoked with the `--help` option, THE Plugin_Helper SHALL display usage information and SHALL NOT call any AWS API. +26. WHEN the Deploy_Plugin is invoked with the `--verbose` option, THE Plugin_Helper SHALL emit detailed progress output for each AWS API interaction. +27. WHEN the Deploy_Plugin completes a successful deployment, THE Plugin_Helper SHALL report the Lambda function ARN and the deployment region to the developer. +28. WHERE the `--with-url` option is provided and the deployment completes successfully, THE Plugin_Helper SHALL report the Function_URL to the developer and SHALL display a ready-to-use `curl` command that includes AWS Signature Version 4 authentication (using curl's `--aws-sigv4` option) so the developer can immediately invoke the function. +29. WHERE the `--with-url` option is not provided and the deployment completes successfully, THE Plugin_Helper SHALL display a ready-to-use `aws lambda invoke` command that the developer can use to invoke the deployed function. + +### Requirement 4: AWS service client generation and access + +**User Story:** As a project maintainer, I want type-safe AWS service clients for Lambda, IAM, S3, and STS generated once and checked into the repository, so that the deploy plugin can call AWS APIs without depending on the entire Soto SDK at build time. + +#### Acceptance Criteria + +1. THE Plugin_System SHALL include Generated_Service_Client code for the AWS Lambda, IAM, S3, and STS operations required by the Deploy_Plugin, committed to the repository. +2. THE Generated_Service_Client for AWS Lambda SHALL provide the operations `CreateFunction`, `UpdateFunctionCode`, `DeleteFunction`, `GetFunction`, `CreateFunctionUrlConfig`, `DeleteFunctionUrlConfig`, `AddPermission`, and `RemovePermission`. +3. THE Generated_Service_Client for AWS IAM SHALL provide the operations `CreateRole`, `DeleteRole`, `AttachRolePolicy`, `DetachRolePolicy`, `GetRole`, `PutRolePolicy`, and `DeleteRolePolicy`. +4. THE Generated_Service_Client for AWS S3 SHALL provide the operations `CreateBucket`, `HeadBucket`, `PutObject`, and `DeleteObject`. +5. THE Generated_Service_Client for AWS STS SHALL provide the operation `GetCallerIdentity`. +6. THE Plugin_System SHALL provide a Generation_Script that invokes the Soto Code Generator to produce the Generated_Service_Client code for the required AWS Lambda, IAM, S3, and STS operations. +7. THE Generation_Script SHALL be a one-time, maintainer-run operation that is separate from the package build. +8. THE Plugin_System SHALL commit the Generated_Service_Client source code produced by the Generation_Script to the git repository. +9. WHEN a developer builds the package, THE Plugin_System SHALL compile the Generated_Service_Client code present on the file system, SHALL NOT invoke the Soto Code Generator during the build, and SHALL fail the build if the Generated_Service_Client code is not found on the file system. + +### Requirement 5: Dependencies and package integration + +**User Story:** As a project maintainer, I want the package to depend on Soto Core and remove vendored AWS infrastructure code, so that credential handling and request signing are production-tested and the maintenance burden is reduced. + +#### Acceptance Criteria + +1. THE Plugin_System SHALL declare a package dependency on `soto-core`. +2. THE Plugin_Helper target SHALL depend on the `SotoCore` product. +3. THE Plugin_System SHALL NOT include vendored crypto, signer, or HTTP client code under `Sources/AWSLambdaPluginHelper/Vendored/`. +4. THE Plugin_System SHALL implement the `init`, `build`, and `deploy` commands within the single `AWSLambdaPluginHelper` executable target. +5. WHEN a plugin wrapper is invoked, THE plugin wrapper SHALL run the Plugin_Helper as a subprocess and pass the corresponding command and arguments. +6. IF the Plugin_Helper subprocess exits with a non-zero status, THEN THE invoking plugin wrapper SHALL report a diagnostic error and SHALL halt immediately without continuing further work. + +### Requirement 6: Amazon Linux 2023 as the default target + +**User Story:** As a Lambda developer, I want Amazon Linux 2023 to be the default target, so that my functions build and deploy against the current, supported AWS Lambda runtime without legacy AL2 prompts, while I retain the ability to target Amazon Linux 2 when I explicitly need it. + +#### Acceptance Criteria + +1. THE Build_Plugin SHALL use `swift:-amazonlinux2023` as the default Base_Image. +2. THE Plugin_System SHALL NOT emit an Amazon Linux 2 deprecation warning when the developer does not explicitly select an Amazon Linux 2 base image. +3. WHERE the developer explicitly provides an Amazon Linux 2 image through the `--base-docker-image` option, THE Plugin_Helper SHALL emit an informational warning noting that Amazon Linux 2 is deprecated and recommending migration to Amazon Linux 2023. +4. WHERE the developer provides an Amazon Linux 2 image through the `--base-docker-image` option, THE Plugin_Helper SHALL compile the products using the supplied Base_Image. +5. WHEN the Deploy_Plugin creates or updates a Lambda function, THE Plugin_Helper SHALL set the Lambda_Runtime_Identifier to `provided.al2023` as the default runtime. +6. WHEN the Build_Plugin runs natively on Amazon_Linux_2023, THE Plugin_Helper SHALL compile the products without requiring a container runtime. + +### Requirement 7: Backward compatibility with the legacy `archive` plugin + +**User Story:** As a developer with existing CI pipelines that call `swift package archive`, I want the legacy `archive` command to keep working as a passthrough to the new build plugin with an unchanged CLI, so that upgrading to the v4 plugin system does not break my existing automation. + +#### Acceptance Criteria + +1. THE Plugin_System SHALL retain an `archive` command that acts as a passthrough to the Build_Plugin, resolving to the same sources and implementation as the `lambda-build` command. +2. WHEN a developer invokes `swift package archive`, THE Plugin_System SHALL perform the same build-and-package behavior as `swift package lambda-build`. +3. WHEN a developer invokes the `archive` command, THE Plugin_System SHALL display a deprecation warning that encourages the developer to use the `lambda-build` plugin (the `swift package lambda-build` command) instead, while still performing the build-and-package work. +4. THE Build_Plugin SHALL accept the command-line interface of the Legacy_Archive_Plugin, including the options `--output-directory`, `--products`, `--configuration`, `--swift-version`, `--base-docker-image`, `--disable-docker-image-update`, `--verbose`, and `--help`, so that existing invocations continue to work unchanged. +5. THE Build_Plugin SHALL accept the legacy `--output-directory` option as a Deprecated_Option_Alias for `--output-path`. +6. WHEN a developer supplies a Deprecated_Option_Alias such as `--output-directory`, THE Plugin_Helper SHALL treat the Deprecated_Option_Alias as equivalent to its current replacement option such as `--output-path`. +7. WHERE an option name has been renamed from the Legacy_Archive_Plugin, THE Build_Plugin SHALL accept the legacy option name as a Deprecated_Option_Alias that maps to the current option name. +8. WHEN a developer supplies a Deprecated_Option_Alias, THE Plugin_Helper MAY emit a deprecation notice indicating the current option name and SHALL honor the Deprecated_Option_Alias. +9. THE documentation for the Build_Plugin SHALL document the current option names as the primary documented interface and SHALL present the Deprecated_Option_Alias names only as supported compatibility aliases. +10. THE Plugin_System SHALL accept a set of Build_Plugin options that is a superset of the Legacy_Archive_Plugin options so that existing CI invocations of `swift package archive` continue to work. + +### Requirement 8: Documentation, tutorials, and examples migration + +**User Story:** As a developer learning from the project's documentation and examples, I want all DocC documentation, tutorials, and example READMEs updated to show the new plugin usage, so that I follow the current recommended workflow instead of deprecated commands. + +#### Acceptance Criteria + +1. THE Plugin_System documentation SHALL update the DocC articles and tutorials under `Sources/AWSLambdaRuntime/Docs.docc/`, the top-level `readme.md`, and the example READMEs under `Examples/` to show the new plugin commands `lambda-init`, `lambda-build`, and `lambda-deploy`. +2. WHERE documentation or an example README currently shows `swift package archive`, THE documentation SHALL show `swift package lambda-build` using the current option names. +3. WHERE an example currently deploys using the raw AWS CLI commands such as `aws lambda create-function`, `aws lambda update-function-code`, or `aws lambda create-function-url-config`, THE example documentation SHALL show the `lambda-deploy` plugin instead. +4. WHERE an example currently deploys using AWS SAM with a `template.yaml` and `sam deploy` or using AWS CDK with `cdk deploy`, THE example SHALL continue to use AWS SAM or AWS CDK respectively and SHALL retain its existing deployment tooling rather than the `lambda-deploy` plugin. +5. THE documentation SHALL remove the references to the Amazon Linux 2 deprecation guidance that are made obsolete by Requirement 6, consistent with Amazon_Linux_2023 being the default target. +6. WHERE example documentation shows invoking the function, THE example documentation MAY retain the raw `aws lambda invoke` command for invocation, because this release provides no plugin invoke command. +7. THE documentation SHALL describe, as a prerequisite for local developer-machine deployments with the Deploy_Plugin, that the developer install the AWS CLI and run `aws configure` to create the AWS configuration in `~/.aws`, and SHALL note that on EC2, ECS, or EKS the credentials are typically provided automatically by the instance or task role through the Credential_Provider_Chain, so running `aws configure` is not required in those environments. + +### Requirement 9: End-to-end lifecycle test suite + +**User Story:** As a project maintainer, I want an automated end-to-end test that exercises the full plugin lifecycle (scaffold → build → deploy → invoke → delete), so that I can verify the three plugins work together against real AWS before release. + +#### Acceptance Criteria + +1. THE Plugin_System SHALL provide an End_To_End_Test_Suite that exercises the full lifecycle: scaffold using the Init_Plugin with the URL_Template, build and package using the Build_Plugin, deploy using the Deploy_Plugin with the `--with-url` option, validate the deployed function through its Function_URL, and delete using the Deploy_Plugin with the `--delete` option. +2. THE End_To_End_Test_Suite SHALL be implemented as a shell script and SHALL NOT be implemented as Swift code. +3. WHEN the End_To_End_Test_Suite runs, THE End_To_End_Test_Suite SHALL scaffold a new Lambda function using the Init_Plugin with the URL_Template through the `--with-url` option. +4. WHEN the function has been scaffolded, THE End_To_End_Test_Suite SHALL build and package the function using the Build_Plugin. +5. WHEN the function has been packaged, THE End_To_End_Test_Suite SHALL deploy the function to AWS using the Deploy_Plugin with the `--with-url` option so that a Function_URL is configured. +6. WHEN the function has been deployed, THE End_To_End_Test_Suite SHALL validate the deployment by sending an HTTP request to the Function_URL using a `curl` command and SHALL verify the returned response. +7. THE Function_URL created by the End_To_End_Test_Suite SHALL use IAM authentication (the `AWS_IAM` auth type) rather than public, unauthenticated access. +8. WHEN the End_To_End_Test_Suite invokes the Function_URL with `curl`, THE End_To_End_Test_Suite SHALL sign the request using AWS Signature Version 4 via curl's built-in `--aws-sigv4` option. +9. WHEN the validation completes, THE End_To_End_Test_Suite SHALL delete the deployed Lambda function and its associated IAM_Role using the Deploy_Plugin `--delete` option. +10. IF any step of the End_To_End_Test_Suite fails after the function has been deployed, THEN THE End_To_End_Test_Suite SHALL delete the deployed Lambda function and its associated resources so that no AWS resources are leaked. +11. IF the Function_URL response does not match the expected result, THEN THE End_To_End_Test_Suite SHALL report a test failure. + +### Requirement 10: Preserve existing working implementation + +**User Story:** As a project maintainer, I want the existing partially-implemented init and build code to be preferred over newly generated code, so that proven working behavior is not regressed by a rewrite. + +#### Acceptance Criteria + +1. WHERE an existing implementation of the Init_Plugin or Build_Plugin functionality is present in the repository, THE Plugin_System SHALL reuse the existing implementation rather than replacing it with newly generated code. +2. THE Plugin_System SHALL preserve the existing behavior of the Init_Plugin and Build_Plugin except where a change is required to satisfy an acceptance criterion in this specification. +3. IF existing Init_Plugin or Build_Plugin code is proven incorrect by failing an acceptance criterion or a test, THEN THE Plugin_System SHALL modify or replace only the incorrect code. +4. WHEN modifying the existing Init_Plugin or Build_Plugin code to satisfy the new requirements, including default-symbol stripping, the Amazon Linux 2023 default Base_Image, or the unified `--cross-compile` option, THE Plugin_System SHALL make the minimal changes necessary and SHALL retain the remaining working behavior. diff --git a/.kiro/specs/lambda-v2-plugins/tasks.md b/.kiro/specs/lambda-v2-plugins/tasks.md new file mode 100644 index 000000000..84d088c38 --- /dev/null +++ b/.kiro/specs/lambda-v2-plugins/tasks.md @@ -0,0 +1,280 @@ +# Implementation Plan: Lambda V2 Plugins + +## Overview + +This plan implements the v4 plugin system for `swift-aws-lambda-runtime` following a dependency-aware ordering: foundational Package.swift changes first, then the helper dispatch fix, Builder modifications, vendored code removal, generated AWS clients, the Deployer (the main new piece), documentation migration, and finally tests. Each task builds on prior tasks and ends with integration checkpoints. + +## Tasks + +- [x] 1. Package.swift changes and foundational setup + - [x] 1.1 Add soto-core dependency and update AWSLambdaPluginHelper target + - Add `.package(url: "https://github.com/soto-project/soto-core.git", from: "7.0.0")` to the package dependencies array + - Add `.product(name: "SotoCore", package: "soto-core")` to the `AWSLambdaPluginHelper` executable target dependencies + - Verify the existing `AWSLambdaPackager` plugin target (verb: `archive`) is already declared and functional + - Run `swift package resolve` to confirm dependency resolution succeeds + - _Requirements: 5.1, 5.2, 7.1_ + +- [x] 2. Fix helper dispatch bug + - [x] 2.1 Fix `args.count > 2` guard to `args.count > 1` in AWSLambdaPluginHelper.swift + - In `command(from:)`, change `guard args.count > 2` to `guard args.count > 1` + - This unblocks the minimum valid invocation `[binary_path, command]` (count == 2) + - Verify dispatch still works for `init`, `build`, and `deploy` subcommands + - _Requirements: 5.4, 5.5 (Design: Helper Dispatch Bug Fix)_ + +- [x] 3. Builder modifications (minimal changes per Req 10) + - [x] 3.1 Change default base image to Amazon Linux 2023 + - In `BuilderConfiguration.init`, change the default `baseDockerImage` from `"swift:\(version)-amazonlinux2"` to `"swift:\(version)-amazonlinux2023"` + - Update the help message default description accordingly + - _Requirements: 6.1, 2.16_ + + - [x] 3.2 Remove blanket AL2 deprecation warning and add targeted AL2 warning + - Remove the unconditional `displayDeprecationWarning()` call that fires when running on AL2 or when any AL2 image is detected + - Add a new, shorter informational warning that fires ONLY when `--base-docker-image` explicitly contains `amazonlinux2` but NOT `amazonlinux2023` + - _Requirements: 6.2, 6.3, 6.4_ + + - [x] 3.3 Add default binary stripping with `-Xlinker -s` and `--no-strip` opt-out + - Add `-Xlinker -s` flags to both native (`buildNative`) and Docker/container (`buildInDocker`) swift build commands + - Add `--no-strip` flag extraction in `BuilderConfiguration.init`; when present, omit the strip flags + - _Requirements: 2.5, 2.6_ + + - [x] 3.4 Replace `--container-cli` with `--cross-compile` option + - Replace the `ContainerCLI` enum with a `CrossCompileMethod` enum: `docker`, `container`, `swift-static-sdk`, `custom-sdk` + - Extract `--cross-compile` option (default: `docker`) instead of `--container-cli` + - For `swift-static-sdk` and `custom-sdk`, report "not yet supported" error with SDK_Installation_Guide link + - Retain Docker/container runtime behavior for `docker` and `container` values (reuse existing `ContainerCLI` pull/run logic internally) + - _Requirements: 2.7, 2.8, 2.9, 2.10, 2.13, 2.14_ + + - [x] 3.5 Add `--output-directory` deprecated alias for `--output-path` + - In `BuilderConfiguration.init`, extract `--output-directory` if `--output-path` is not provided + - Map it to the same `outputDirectory` property + - Optionally emit a deprecation notice + - _Requirements: 7.5, 7.6, 7.7, 7.8_ + + - [x] 3.6 Add container CLI existence check with helpful error messages + - Before executing Docker or container commands, verify the CLI binary exists at the resolved tool path + - If Docker CLI is missing: report error with Docker download/installation URL + - If Apple `container` CLI is missing: report error with Apple container CLI download URL + - _Requirements: 2.11, 2.12_ + +- [x] 4. Checkpoint - Verify builder compiles and existing tests pass + - Ensure all tests pass, ask the user if questions arise. + +- [x] 5. Remove vendored code + - [x] 5.1 Delete `Sources/AWSLambdaPluginHelper/Vendored/` directory + - Remove the entire `Vendored/` directory containing crypto, signer, and HTTP client code + - Verify the project still compiles (it should, since soto-core replaces this functionality) + - _Requirements: 5.3_ + +- [x] 6. Generated AWS service clients + - [x] 6.1 Create the generation script at `scripts/generate-aws-clients.sh` + - Write a shell script that clones/uses Soto Code Generator + - Configure it to produce clients for Lambda, IAM, S3, and STS with only the required operations + - Script copies output to `Sources/AWSLambdaPluginHelper/GeneratedClients/` + - Script is maintainer-run, NOT part of the build + - _Requirements: 4.6, 4.7_ + + - [x] 6.2 Create generated Lambda client (`GeneratedClients/Lambda/`) + - Create `LambdaClient.swift`, `LambdaShapes.swift`, `LambdaErrors.swift` + - Operations: `CreateFunction`, `UpdateFunctionCode`, `DeleteFunction`, `GetFunction`, `CreateFunctionUrlConfig`, `DeleteFunctionUrlConfig`, `AddPermission`, `RemovePermission` + - Each operation wraps SotoCore's `AWSClient` with proper request signing + - _Requirements: 4.1, 4.2_ + + - [x] 6.3 Create generated IAM client (`GeneratedClients/IAM/`) + - Create `IAMClient.swift`, `IAMShapes.swift`, `IAMErrors.swift` + - Operations: `CreateRole`, `DeleteRole`, `AttachRolePolicy`, `DetachRolePolicy`, `GetRole`, `PutRolePolicy`, `DeleteRolePolicy` + - _Requirements: 4.1, 4.3_ + + - [x] 6.4 Create generated S3 client (`GeneratedClients/S3/`) + - Create `S3Client.swift`, `S3Shapes.swift`, `S3Errors.swift` + - Operations: `CreateBucket`, `HeadBucket`, `PutObject`, `DeleteObject` + - _Requirements: 4.1, 4.4_ + + - [x] 6.5 Create generated STS client (`GeneratedClients/STS/`) + - Create `STSClient.swift`, `STSShapes.swift`, `STSErrors.swift` + - Operations: `GetCallerIdentity` + - _Requirements: 4.1, 4.5_ + +- [x] 7. Checkpoint - Verify generated clients compile with soto-core + - Ensure all tests pass, ask the user if questions arise. + +- [x] 8. Deployer implementation + - [x] 8.1 Implement `DeployerConfiguration` with full argument parsing + - Parse: `--help`, `--verbose`, `--with-url`, `--delete`, `--region`, `--iam-role`, `--input-directory`, `--architecture`, `--products` + - Default architecture to host architecture (`x64` or `arm64`) + - Default input directory to the standard build output path + - Update `displayHelpMessage()` with all options + - _Requirements: 3.3, 3.6, 3.7, 3.9, 3.11, 3.12, 3.13, 3.14, 3.25, 3.26_ + + - [x] 8.2 Implement AWS client initialization and credential/config verification + - Initialize SotoCore `AWSClient` with the credential provider chain + - Check for `~/.aws/config` and `~/.aws/credentials`; if absent, emit non-blocking informational warning suggesting `aws configure` + - If credentials cannot be resolved, report descriptive error and fail + - Handle `--region` override or fall through to Soto's region resolution + - _Requirements: 3.8, 3.10, 3.21, 3.22, 3.23_ + + - [x] 8.3 Implement account ID resolution and function existence check + - Call STS `GetCallerIdentity` to resolve AWS account ID + - Call Lambda `GetFunction` to determine if function already exists + - Determine deployment action: create, update, or delete + - _Requirements: 3.1, 3.2, 3.16_ + + - [x] 8.4 Implement IAM role management + - When creating a function without `--iam-role`: create IAM role `swift-lambda--role` with Lambda assume-role trust policy + - Attach `AWSLambdaBasicExecutionRole` managed policy + - Wait for role propagation before proceeding + - When `--iam-role` is provided: use the specified role ARN directly + - When `--delete` is used: detach policies and delete the role + - _Requirements: 3.4, 3.5, 3.6_ + + - [x] 8.5 Implement S3 staging for large archives + - Define `directUploadLimit = 50 * 1024 * 1024` (50 MB) + - Implement `deploymentBucketName(region:accountId:)` → `"swift-aws-lambda-runtime--"` + - If ZIP > 50 MB: check if bucket exists (`HeadBucket`), create if absent (`CreateBucket`) + - Upload ZIP to bucket (`PutObject`) + - After deployment, delete the uploaded object (`DeleteObject`) while retaining the bucket + - _Requirements: 3.15, 3.17, 3.18, 3.19, 3.20_ + + - [x] 8.6 Implement create/update/delete function orchestration + - **Create**: `CreateFunction` with `provided.al2023` runtime, architecture, IAM role, ZIP payload (direct or S3 reference) + - **Update**: `UpdateFunctionCode` with ZIP payload (direct or S3 reference) + - **Delete**: `DeleteFunction`, then delete IAM role and its policies + - Report AWS errors with service, operation, and message + - _Requirements: 3.1, 3.2, 3.4, 3.24, 6.5_ + + - [x] 8.7 Implement Function URL setup + - When `--with-url` is provided: call `CreateFunctionUrlConfig` with `AWS_IAM` auth type + - Add resource-based permission for Function URL invocation + - _Requirements: 3.7_ + + - [x] 8.8 Implement post-deploy output (success reporting) + - Report Lambda function ARN and deployment region + - If `--with-url`: display Function URL and a ready-to-use `curl --aws-sigv4` command + - If no URL: display a ready-to-use `aws lambda invoke` command + - Shutdown AWSClient cleanly + - _Requirements: 3.27, 3.28, 3.29_ + +- [x] 9. Checkpoint - Verify deployer compiles and integrates with generated clients + - Ensure all tests pass, ask the user if questions arise. + +- [x] 10. Documentation migration + - [x] 10.1 Update DocC articles and tutorials + - Update articles under `Sources/AWSLambdaRuntime/Docs.docc/` to show `lambda-init`, `lambda-build`, `lambda-deploy` commands + - Replace `swift package archive` references with `swift package lambda-build` + - Replace raw AWS CLI deployment commands with `lambda-deploy` plugin usage (except SAM/CDK examples) + - Remove obsolete Amazon Linux 2 deprecation guidance + - Document `aws configure` prerequisite for local deployments + - _Requirements: 8.1, 8.2, 8.3, 8.5, 8.7_ + + - [x] 10.2 Update top-level readme.md and example READMEs + - Update `readme.md` with new plugin commands and workflow + - Update example READMEs under `Examples/` to show new commands + - Keep SAM/CDK examples using their respective deployment tools + - May retain `aws lambda invoke` for invocation examples + - _Requirements: 8.1, 8.2, 8.3, 8.4, 8.6_ + +- [x] 11. Unit tests + - [x] 11.1 Write unit tests for BuilderConfiguration argument parsing + - Test `--cross-compile` parsing with all valid values and invalid values + - Test `--no-strip` flag detection + - Test `--output-directory` deprecated alias maps to `outputDirectory` + - Test mutual exclusion of `--swift-version` and `--base-docker-image` + - Test default base image is `amazonlinux2023` + - _Requirements: 2.5, 2.6, 2.7, 2.17, 6.1, 7.5_ + + - [x] 11.2 Write unit tests for DeployerConfiguration argument parsing + - Test `--architecture` parsing with valid and invalid values + - Test default architecture matches host + - Test `--region`, `--iam-role`, `--input-directory`, `--with-url`, `--delete` parsing + - Test `--help` flag produces help output without AWS calls + - _Requirements: 3.13, 3.14, 3.25_ + + - [x] 11.3 Write unit tests for deployment bucket name construction and archive threshold + - Test `deploymentBucketName(region:accountId:)` produces correct format + - Test bucket name is valid S3 name (lowercase, 3-63 chars) + - Test `directUploadLimit` boundary: exactly 50 MB → direct, 50 MB + 1 → S3 + - _Requirements: 3.15, 3.17, 3.19_ + + - [x] 11.4 Write property test for deprecated option alias equivalence (Property 2) + - **Property 2: Deprecated option alias equivalence** + - For any path value, `--output-directory ` produces the same `outputDirectory` as `--output-path ` + - **Validates: Requirements 7.5, 7.6** + + - [x] 11.5 Write property test for cross-compile method parsing round-trip (Property 3) + - **Property 3: Cross-compile method parsing round-trip** + - For any valid `CrossCompileMethod` case, `rawValue` → parse → original case + - **Validates: Requirements 2.7** + + - [x] 11.6 Write property test for mutual exclusion of --swift-version and --base-docker-image (Property 4) + - **Property 4: Mutual exclusion of --swift-version and --base-docker-image** + - For any non-empty swift-version and any non-empty base-docker-image, parsing throws an error + - **Validates: Requirements 2.17** + + - [x] 11.7 Write property test for deployment bucket name construction (Property 5) + - **Property 5: Deployment bucket name construction** + - For any valid region and 12-digit account ID, result matches `"swift-aws-lambda-runtime--"` and is a valid S3 bucket name + - **Validates: Requirements 3.17, 3.18** + + - [x] 11.8 Write property test for archive size determines upload strategy (Property 6) + - **Property 6: Archive size determines upload strategy** + - For any size ≤ 50 MB → direct upload chosen; for any size > 50 MB → S3 staging chosen + - **Validates: Requirements 3.15, 3.19** + + - [x] 11.9 Write property test for AL2 warning logic (Property 8) + - **Property 8: AL2 warning emitted only for explicit AL2 image selection** + - For any image string containing "amazonlinux2" but NOT "amazonlinux2023" → warning emitted; for "amazonlinux2023" or default → no warning + - **Validates: Requirements 6.2, 6.3** + + - [x] 11.10 Write property test for unsupported cross-compile methods (Property 9) + - **Property 9: Unsupported cross-compile methods report error with link** + - For `swift-static-sdk` and `custom-sdk`, builder reports "not yet supported" error containing SDK guide URL + - **Validates: Requirements 2.14** + +- [x] 12. Checkpoint - Ensure all unit and property tests pass + - Ensure all tests pass, ask the user if questions arise. + +- [x] 13. End-to-end test script + - [x] 13.1 Create `scripts/integration-test.sh` shell script + - Implement the full lifecycle: scaffold (lambda-init --with-url) → build (lambda-build) → deploy (lambda-deploy --with-url) → validate (curl --aws-sigv4 to Function URL) → delete (lambda-deploy --delete) + - Use `trap cleanup EXIT` to guarantee resource cleanup on failure + - Verify response body matches expected output + - Use unique function name with timestamp to avoid conflicts + - Script must be `bash`, NOT Swift + - _Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7, 9.8, 9.9, 9.10, 9.11_ + +- [x] 14. Final checkpoint - Full build verification + - Ensure all tests pass, ask the user if questions arise. + +## Notes + +- Tasks marked with `*` are optional and can be skipped for faster MVP +- Each task references specific requirements for traceability +- Checkpoints ensure incremental validation +- Property tests validate universal correctness properties from the design document +- Unit tests validate specific examples and edge cases +- The Deployer (task 8) is the largest piece of new work and is broken into focused sub-tasks +- Builder modifications (task 3) are minimal per Requirement 10 (preserve existing working code) +- Generated clients (task 6) must be in place before the Deployer can compile +- Documentation and tests can proceed in parallel once core implementation is complete +- The e2e test (task 13) depends on all prior implementation being complete + +## Task Dependency Graph + +```json +{ + "waves": [ + { "id": 0, "tasks": ["1.1"] }, + { "id": 1, "tasks": ["2.1", "5.1"] }, + { "id": 2, "tasks": ["3.1", "3.2", "3.3", "3.4", "3.5", "3.6"] }, + { "id": 3, "tasks": ["6.1"] }, + { "id": 4, "tasks": ["6.2", "6.3", "6.4", "6.5"] }, + { "id": 5, "tasks": ["8.1"] }, + { "id": 6, "tasks": ["8.2", "8.3"] }, + { "id": 7, "tasks": ["8.4", "8.5"] }, + { "id": 8, "tasks": ["8.6"] }, + { "id": 9, "tasks": ["8.7", "8.8"] }, + { "id": 10, "tasks": ["10.1", "10.2", "11.1", "11.2", "11.3"] }, + { "id": 11, "tasks": ["11.4", "11.5", "11.6", "11.7", "11.8", "11.9", "11.10"] }, + { "id": 12, "tasks": ["13.1"] } + ] +} +``` diff --git a/Examples/APIGatewayV1/README.md b/Examples/APIGatewayV1/README.md index 5f579a497..921037fd3 100644 --- a/Examples/APIGatewayV1/README.md +++ b/Examples/APIGatewayV1/README.md @@ -25,7 +25,7 @@ To build the package, type the following commands. ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. diff --git a/Examples/APIGatewayV2+LambdaAuthorizer/README.md b/Examples/APIGatewayV2+LambdaAuthorizer/README.md index 597d00a54..502833a64 100644 --- a/Examples/APIGatewayV2+LambdaAuthorizer/README.md +++ b/Examples/APIGatewayV2+LambdaAuthorizer/README.md @@ -19,7 +19,7 @@ To build the package, type the following commands. ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there are two ZIP files ready to deploy, one for the authorizer function and one for the business function. diff --git a/Examples/APIGatewayV2/README.md b/Examples/APIGatewayV2/README.md index ad33f3cd2..e7ffccee8 100644 --- a/Examples/APIGatewayV2/README.md +++ b/Examples/APIGatewayV2/README.md @@ -25,7 +25,7 @@ To build the package, type the following commands. ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. diff --git a/Examples/BackgroundTasks/README.md b/Examples/BackgroundTasks/README.md index 11d7b7766..f3cebceea 100644 --- a/Examples/BackgroundTasks/README.md +++ b/Examples/BackgroundTasks/README.md @@ -25,39 +25,34 @@ Once the struct is created and the `handle(...)` method is defined, the sample c To build & archive the package, type the following commands. ```bash -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` 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 +## Deploy with the lambda-deploy plugin -Here is how to deploy using the `aws` command line. +Here is how to deploy using the `lambda-deploy` plugin. ### 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.al2023 \ ---handler provided \ ---architectures arm64 \ ---role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution \ ---environment "Variables={LOG_LEVEL=debug}" \ ---timeout 15 +swift package --allow-network-connections all:443 lambda-deploy ``` -> [!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. +This creates the Lambda function, provisions the necessary IAM role, and uploads the deployment package. -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`. +> [!IMPORTANT] +> After deploying, update the function timeout to 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 so the timeout should be at least 15 seconds: +> ```bash +> aws lambda update-function-configuration \ +> --function-name BackgroundTasks \ +> --timeout 15 \ +> --environment "Variables={LOG_LEVEL=debug}" +> ``` -Be sure to set `AWS_ACCOUNT_ID` with your actual AWS account ID (for example: 012345678901). +The `--environment` argument 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. ### Invoke your Lambda function @@ -115,7 +110,7 @@ Type CTRL-C to stop tailing the logs. When done testing, you can delete the Lambda function with this command. ```bash -aws lambda delete-function --function-name BackgroundTasks +swift package --allow-network-connections all:443 lambda-deploy --delete ``` ## ⚠️ Security and Reliability Notice diff --git a/Examples/CDK/README.md b/Examples/CDK/README.md index 37383df08..c0abfea61 100644 --- a/Examples/CDK/README.md +++ b/Examples/CDK/README.md @@ -12,7 +12,7 @@ To build the package, type the following commands. ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. diff --git a/Examples/HelloJSON/README.md b/Examples/HelloJSON/README.md index d923c03aa..3b35f023d 100644 --- a/Examples/HelloJSON/README.md +++ b/Examples/HelloJSON/README.md @@ -21,7 +21,7 @@ The function return value will be encoded to a `HelloResponse` as your Lambda fu To build & archive the package, type the following commands. ```bash -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. @@ -29,24 +29,13 @@ The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPa ## Deploy -Here is how to deploy using the `aws` command line. +Here is how to deploy using the `lambda-deploy` plugin. ```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.al2023 \ ---handler provided \ ---architectures arm64 \ ---role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution +swift package --allow-network-connections all:443 lambda-deploy ``` -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). +This creates the Lambda function, provisions the necessary IAM role, and uploads the deployment package. ## Invoke your Lambda function @@ -76,7 +65,7 @@ This should output the following result. When done testing, you can delete the Lambda function with this command. ```bash -aws lambda delete-function --function-name HelloJSON +swift package --allow-network-connections all:443 lambda-deploy --delete ``` ## ⚠️ Security and Reliability Notice diff --git a/Examples/HelloWorld/README.md b/Examples/HelloWorld/README.md index f3d48cd3b..8471157d8 100644 --- a/Examples/HelloWorld/README.md +++ b/Examples/HelloWorld/README.md @@ -46,8 +46,7 @@ curl -d '"seb"' http://127.0.0.1:7000/invoke To build & archive the package, type the following commands. ```bash -swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. @@ -55,21 +54,13 @@ The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPa ## Deploy -Here is how to deploy using the `aws` command line. +Here is how to deploy using the `lambda-deploy` plugin. ```bash -aws lambda create-function \ ---function-name MyLambda \ ---zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip \ ---runtime provided.al2023 \ ---handler provided \ ---architectures arm64 \ ---role arn:aws:iam:::role/lambda_basic_execution +swift package --allow-network-connections all:443 lambda-deploy ``` -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). +This creates the Lambda function, provisions the necessary IAM role, and uploads the deployment package. ## Invoke your Lambda function @@ -99,7 +90,7 @@ This should output the following result. When done testing, you can delete the Lambda function with this command. ```bash -aws lambda delete-function --function-name MyLambda +swift package --allow-network-connections all:443 lambda-deploy --delete ``` ## ⚠️ Security and Reliability Notice diff --git a/Examples/HelloWorldNoTraits/README.md b/Examples/HelloWorldNoTraits/README.md index 59c493732..7706a39e2 100644 --- a/Examples/HelloWorldNoTraits/README.md +++ b/Examples/HelloWorldNoTraits/README.md @@ -53,7 +53,7 @@ To build & archive the package, type the following commands. ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. @@ -61,21 +61,13 @@ The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPa ## Deploy -Here is how to deploy using the `aws` command line. +Here is how to deploy using the `lambda-deploy` plugin. ```bash -aws lambda create-function \ ---function-name MyLambda \ ---zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip \ ---runtime provided.al2023 \ ---handler provided \ ---architectures arm64 \ ---role arn:aws:iam:::role/lambda_basic_execution +swift package --allow-network-connections all:443 lambda-deploy ``` -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). +This creates the Lambda function, provisions the necessary IAM role, and uploads the deployment package. ## Invoke your Lambda function @@ -103,7 +95,7 @@ This should output the following result. When done testing, you can delete the Lambda function with this command. ```bash -aws lambda delete-function --function-name MyLambda +swift package --allow-network-connections all:443 lambda-deploy --delete ``` ## ⚠️ Security and Reliability Notice diff --git a/Examples/HummingbirdLambda/README.md b/Examples/HummingbirdLambda/README.md index 8291b6e43..6c2d8b683 100644 --- a/Examples/HummingbirdLambda/README.md +++ b/Examples/HummingbirdLambda/README.md @@ -18,7 +18,7 @@ To build the package, type the following commands. ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. diff --git a/Examples/JSONLogging/README.md b/Examples/JSONLogging/README.md index 12c83ad62..4fbc6dc0d 100644 --- a/Examples/JSONLogging/README.md +++ b/Examples/JSONLogging/README.md @@ -84,7 +84,7 @@ AWS_LAMBDA_LOG_FORMAT=JSON swift run ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` The deployment package will be at: @@ -128,17 +128,19 @@ sam deploy --guided ## Deploy with AWS CLI -As an alternative to SAM, you can use the AWS CLI: +As an alternative to SAM, you can use the `lambda-deploy` plugin: ```bash -ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text) -aws lambda create-function \ - --function-name JSONLoggingExample \ - --zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/JSONLogging/JSONLogging.zip \ - --runtime provided.al2023 \ - --handler swift.bootstrap \ - --architectures arm64 \ - --role arn:aws:iam::${ACCOUNT_ID}:role/lambda_basic_execution \ +swift package --allow-network-connections all:443 lambda-deploy +``` + +This creates the Lambda function, provisions the necessary IAM role, and uploads the deployment package. + +After deploying, configure logging format: + +```bash +aws lambda update-function-configuration \ + --function-name JSONLogging \ --logging-config LogFormat=JSON,ApplicationLogLevel=DEBUG,SystemLogLevel=INFO ``` @@ -256,8 +258,8 @@ The runtime maps Swift's `Logger.Level` to AWS Lambda log levels: # SAM deployment sam delete -# AWS CLI deployment -aws lambda delete-function --function-name JSONLoggingExample +# Plugin deployment +swift package --allow-network-connections all:443 lambda-deploy --delete ``` ## ⚠️ Important Notes diff --git a/Examples/ManagedInstances/README.md b/Examples/ManagedInstances/README.md index 630f93d65..1c9015b4c 100644 --- a/Examples/ManagedInstances/README.md +++ b/Examples/ManagedInstances/README.md @@ -28,7 +28,7 @@ arn:aws:lambda:us-west-2:486652066693:capacity-provider:TestEC2 ```bash # Build and package the Swift Lambda function -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build # Change the values below to match your setup REGION=us-west-2 diff --git a/Examples/MultiSourceAPI/README.md b/Examples/MultiSourceAPI/README.md index f5ae855e8..5e7015cec 100644 --- a/Examples/MultiSourceAPI/README.md +++ b/Examples/MultiSourceAPI/README.md @@ -13,7 +13,7 @@ Based on the successfully decoded type, it returns an appropriate response. ## Building ```bash -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` ## Deploying diff --git a/Examples/MultiTenant/README.md b/Examples/MultiTenant/README.md index 3bf99a978..7fdc6870a 100644 --- a/Examples/MultiTenant/README.md +++ b/Examples/MultiTenant/README.md @@ -166,7 +166,7 @@ requestParameters: 1. **Build the Lambda function**: ```bash - swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 + swift package --allow-network-connections docker lambda-build ``` 2. **Deploy using SAM**: diff --git a/Examples/README.md b/Examples/README.md index 725f6c88c..4f27b365c 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -6,9 +6,9 @@ This directory contains example code for Lambda functions. - 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 build and archive your Lambda functions, you need to [install docker](https://docs.docker.com/desktop/install/mac-install/) or Apple [container](https://github.com/apple/container). -- 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). +- 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) (run `aws configure` to set up your credentials). - 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. diff --git a/Examples/ResourcesPackaging/README.md b/Examples/ResourcesPackaging/README.md index 53960fece..23383ce41 100644 --- a/Examples/ResourcesPackaging/README.md +++ b/Examples/ResourcesPackaging/README.md @@ -49,7 +49,7 @@ curl -d '"hello"' http://127.0.0.1:7000/invoke To build & archive the package, type the following commands. ```bash -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. @@ -57,21 +57,13 @@ The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPa ## Deploy -Here is how to deploy using the `aws` command line. +Here is how to deploy using the `lambda-deploy` plugin. ```bash -aws lambda create-function \ ---function-name MyLambda \ ---zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip \ ---runtime provided.al2023 \ ---handler provided \ ---architectures arm64 \ ---role arn:aws:iam:::role/lambda_basic_execution +swift package --allow-network-connections all:443 lambda-deploy ``` -The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x86_64`. - -Be sure to replace with your actual AWS account ID (for example: 012345678901). +This creates the Lambda function, provisions the necessary IAM role, and uploads the deployment package. ## Invoke your Lambda function @@ -99,7 +91,7 @@ This should output the following result. When done testing, you can delete the Lambda function with this command. ```bash -aws lambda delete-function --function-name MyLambda +swift package --allow-network-connections all:443 lambda-deploy --delete ``` ## ⚠️ Security and Reliability Notice diff --git a/Examples/S3EventNotifier/README.md b/Examples/S3EventNotifier/README.md index 08cdce561..b2c2ae023 100644 --- a/Examples/S3EventNotifier/README.md +++ b/Examples/S3EventNotifier/README.md @@ -23,7 +23,7 @@ To build & archive the package you can use the following commands: ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there are no errors, a ZIP file should be ready to deploy, located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/S3EventNotifier/S3EventNotifier.zip`. @@ -33,23 +33,13 @@ If there are no errors, a ZIP file should be ready to deploy, located at `.build > [!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: +To deploy the Lambda function, you can use the `lambda-deploy` plugin: ```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.al2023 \ - --handler provided \ - --architectures arm64 \ - --role arn:aws:iam:::role/lambda_basic_execution +swift package --allow-network-connections all:443 lambda-deploy --region eu-west-1 ``` -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). +This creates the Lambda function, provisions the necessary IAM role, and uploads the deployment package. 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: diff --git a/Examples/S3_AWSSDK/README.md b/Examples/S3_AWSSDK/README.md index 45c585b46..5d546cc7f 100644 --- a/Examples/S3_AWSSDK/README.md +++ b/Examples/S3_AWSSDK/README.md @@ -25,7 +25,7 @@ To build the package, type the following commands. ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. diff --git a/Examples/S3_Soto/README.md b/Examples/S3_Soto/README.md index 92af31801..e378ae595 100644 --- a/Examples/S3_Soto/README.md +++ b/Examples/S3_Soto/README.md @@ -25,7 +25,7 @@ To build the package, type the following command. ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. diff --git a/Examples/ServiceLifecycle+Postgres/README.md b/Examples/ServiceLifecycle+Postgres/README.md index f03e48717..5cd6520b7 100644 --- a/Examples/ServiceLifecycle+Postgres/README.md +++ b/Examples/ServiceLifecycle+Postgres/README.md @@ -80,7 +80,7 @@ The Lambda function uses the following environment variables for database connec 1. **Build the Lambda function:** ```bash - swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 + swift package --allow-network-connections docker lambda-build ``` 2. **Deploy with SAM:** diff --git a/Examples/Streaming+APIGateway/README.md b/Examples/Streaming+APIGateway/README.md index badecccef..423a2bda8 100644 --- a/Examples/Streaming+APIGateway/README.md +++ b/Examples/Streaming+APIGateway/README.md @@ -66,7 +66,7 @@ Once the struct is created and the `handle(...)` method is defined, the sample c To build & archive the package, type the following commands. ```bash -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. diff --git a/Examples/Streaming+Codable/README.md b/Examples/Streaming+Codable/README.md index db562d955..119dd9fcd 100644 --- a/Examples/Streaming+Codable/README.md +++ b/Examples/Streaming+Codable/README.md @@ -78,7 +78,7 @@ Key features demonstrated: To build & archive the package, type the following commands. ```bash -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. @@ -107,65 +107,17 @@ curl -v \ http://127.0.0.1:7000/invoke ``` -## Deploy with the AWS CLI +## Deploy with the lambda-deploy plugin -Here is how to deploy using the `aws` command line. +Here is how to deploy using the `lambda-deploy` plugin with a Function URL. -### Step 1: Create the function +### Step 1: Deploy the function with a URL ```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.al2023 \ ---handler provided \ ---architectures arm64 \ ---role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution +swift package --allow-network-connections all:443 lambda-deploy --with-url ``` -> [!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" -} -``` +This creates the Lambda function, provisions the necessary IAM role, configures a Function URL with IAM authentication, and uploads the deployment package. The output will include the Function URL and a ready-to-use `curl` command. ### Invoke your Lambda function @@ -205,7 +157,7 @@ This should output the following result, with configurable delays between each m When done testing, you can delete the Lambda function with this command. ```bash -aws lambda delete-function --function-name StreamingFromEvent +swift package --allow-network-connections all:443 lambda-deploy --delete ``` ## Deploy with AWS SAM diff --git a/Examples/Streaming+FunctionUrl/README.md b/Examples/Streaming+FunctionUrl/README.md index be09483ec..2238bfdc0 100644 --- a/Examples/Streaming+FunctionUrl/README.md +++ b/Examples/Streaming+FunctionUrl/README.md @@ -68,7 +68,7 @@ Once the struct is created and the `handle(...)` method is defined, the sample c To build & archive the package, type the following commands. ```bash -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. @@ -88,66 +88,25 @@ curl -v --output response.txt \ http://127.0.0.1:7000/invoke ``` -## Deploy with the AWS CLI +## Deploy with the lambda-deploy plugin -Here is how to deploy using the `aws` command line. +Here is how to deploy using the `lambda-deploy` plugin with a Function URL. -### Step 1: Create the function +### Step 1: Deploy the function with a URL ```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.al2023 \ ---handler provided \ ---architectures arm64 \ ---role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution \ ---timeout 15 +swift package --allow-network-connections all:443 lambda-deploy --with-url ``` -> [!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. +This creates the Lambda function, provisions the necessary IAM role, configures a Function URL with IAM authentication, and uploads the deployment package. The output will include the Function URL and a ready-to-use `curl` command. -```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" -} -``` +> [!IMPORTANT] +> After deploying, update the function timeout to 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 streams responses during 3 seconds so set the timeout for at least 5 seconds: +> ```bash +> aws lambda update-function-configuration \ +> --function-name StreamingNumbers \ +> --timeout 15 +> ``` ### Invoke your Lambda function @@ -187,7 +146,7 @@ Streaming complete! When done testing, you can delete the Lambda function with this command. ```bash -aws lambda delete-function --function-name StreamingNumbers +swift package --allow-network-connections all:443 lambda-deploy --delete ``` ## Deploy with AWS SAM diff --git a/Examples/_MyFirstFunction/clean.sh b/Examples/_MyFirstFunction/clean.sh index 7be3ac400..5108b7b83 100755 --- a/Examples/_MyFirstFunction/clean.sh +++ b/Examples/_MyFirstFunction/clean.sh @@ -14,24 +14,19 @@ ## ##===----------------------------------------------------------------------===## -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 +echo "This script deletes the Lambda function and IAM role, then removes local project files." +read -r -p "Are you sure you want to delete everything? [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 Lambda function and IAM role" +swift package --allow-network-connections all:443 lambda-deploy --delete || true -echo "🚀 Deleting the project files" +echo "🧹 Deleting local project files" rm -rf .build rm -rf ./Sources -rm trust-policy.json -rm Package.swift Package.resolved +rm -f Package.swift Package.resolved -echo "🎉 Done! Your project is cleaned up and ready for a fresh start." \ No newline at end of file +echo "🎉 Done! Your project is cleaned up and ready for a fresh start." diff --git a/Examples/_MyFirstFunction/create_and_deploy_function.sh b/Examples/_MyFirstFunction/create_and_deploy_function.sh deleted file mode 100755 index bfb9939d5..000000000 --- a/Examples/_MyFirstFunction/create_and_deploy_function.sh +++ /dev/null @@ -1,196 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright SwiftAWSLambdaRuntime project authors -## Copyright (c) Amazon.com, Inc. or its affiliates. -## 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'. - " - - printf "Are you ready to create your first Lambda function in Swift? [y/n] " - read -r continue - case $continue in - [Yy]*) ;; - *) echo "OK, try again later when you feel ready"; exit 1 ;; - esac -} - -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/awslabs/swift-aws-lambda-runtime.git --from 2.0.0 - # swift package add-dependency https://github.com/awslabs/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.3 - -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/awslabs/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 --base-docker-image swift:amazonlinux2023 > /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.al2023 \ - --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/Examples/_MyFirstFunction/create_function.sh b/Examples/_MyFirstFunction/create_function.sh index f0b9315cb..7165f8e8c 100755 --- a/Examples/_MyFirstFunction/create_function.sh +++ b/Examples/_MyFirstFunction/create_function.sh @@ -14,16 +14,18 @@ ## ##===----------------------------------------------------------------------===## +# Stop the script execution if an error occurs +set -e -o pipefail + # check if docker is installed -if ! which docker > /dev/null; then - echo "Docker is not installed. Please install Docker and try again." - exit 1 -fi +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. +echo "This script creates, builds, deploys, and invokes 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. +You must have an AWS account and have run 'aws configure' to set up your credentials in ~/.aws/. " printf "Are you ready to create your first Lambda function in Swift? [y/n] " @@ -42,12 +44,28 @@ swift package add-dependency https://github.com/swift-server/swift-aws-lambda-ev 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 "📝 Scaffold the Lambda function 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 "📦 Compile and package the function for deployment (this might take a while)" +swift package --allow-network-connections docker lambda-build echo "🚀 Deploy to AWS Lambda" +swift package --allow-network-connections all:443 lambda-deploy + +echo "" +echo "⏰ Waiting 5 secs for the Lambda function to be ready..." +sleep 5 +echo "🔗 Invoke the Lambda function" +aws lambda invoke \ + --function-name MyLambda \ + --payload $(echo '{"name":"World","age":30}' | base64) \ + /tmp/out.json > /dev/null && cat /tmp/out.json +echo "" +echo "" +echo "🎉 Done! Your first Lambda function in Swift is deployed on AWS Lambda." +echo "" +echo "To delete the function and clean up:" +echo " swift package --allow-network-connections all:443 lambda-deploy --delete" diff --git a/Package.swift b/Package.swift index 208e74a22..899b33ac2 100644 --- a/Package.swift +++ b/Package.swift @@ -11,6 +11,10 @@ let defaultSwiftSettings: [SwiftSetting] = let package = Package( name: "swift-aws-lambda-runtime", + // Required because soto-core declares platforms: [.macOS(.v10_15)] in its Package.swift. + // SwiftPM rejects executable targets whose platform minimum is below their dependencies'. + // Without this declaration the package defaults to macOS 10.13, causing a planning error. + platforms: [.macOS(.v15)], products: [ .library(name: "AWSLambdaRuntime", targets: ["AWSLambdaRuntime"]), @@ -29,6 +33,9 @@ let package = Package( // plugin to package the lambda, creating an archive that can be uploaded to AWS .plugin(name: "AWSLambdaBuilder", targets: ["AWSLambdaBuilder"]), + // legacy 'archive' command — deprecated passthrough to lambda-build + .plugin(name: "AWSLambdaPackager", targets: ["AWSLambdaPackager"]), + // plugin to deploy a Lambda function .plugin(name: "AWSLambdaDeployer", targets: ["AWSLambdaDeployer"]), ], @@ -51,6 +58,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-log.git", from: "1.12.0"), .package(url: "https://github.com/apple/swift-collections.git", from: "1.5.0"), .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.11.0"), + .package(url: "https://github.com/soto-project/soto-core.git", from: "7.13.0"), ], targets: [ .target( @@ -86,24 +94,26 @@ let package = Package( ] ), // 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 - // ), + // Uses its own Plugin.swift that emits a deprecation warning then delegates to the helper + .plugin( + name: "AWSLambdaPackager", + capability: .command( + intent: .custom( + verb: "archive", + description: + "Archive the Lambda binary and prepare it for uploading to AWS. (Deprecated: use lambda-build instead)" + ), + permissions: [ + .allowNetworkConnections( + scope: .docker, + reason: "This plugin uses Docker to create the AWS Lambda ZIP package." + ) + ] + ), + dependencies: [ + .target(name: "AWSLambdaPluginHelper") + ] + ), .plugin( name: "AWSLambdaBuilder", capability: .command( @@ -147,6 +157,7 @@ let package = Package( dependencies: [ .product(name: "NIOHTTP1", package: "swift-nio"), .product(name: "NIOCore", package: "swift-nio"), + .product(name: "SotoCore", package: "soto-core"), ], swiftSettings: defaultSwiftSettings ), diff --git a/Plugins/AWSLambdaBuilder/Plugin.swift b/Plugins/AWSLambdaBuilder/Plugin.swift index c2da2f06e..ab4d64416 100644 --- a/Plugins/AWSLambdaBuilder/Plugin.swift +++ b/Plugins/AWSLambdaBuilder/Plugin.swift @@ -94,9 +94,13 @@ struct AWSLambdaPackager: CommandPlugin { "--zip-tool-path", zipToolPath.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) + // Invoke the plugin helper, passing the current environment so that + // AWS credentials and HOME are available to the subprocess. + let process = Process() + process.executableURL = tool.url + process.arguments = args + process.environment = ProcessInfo.processInfo.environment + try process.run() process.waitUntilExit() // Check whether the subprocess invocation was successful. diff --git a/Plugins/AWSLambdaDeployer/Plugin.swift b/Plugins/AWSLambdaDeployer/Plugin.swift index 60b9c163e..d0237b369 100644 --- a/Plugins/AWSLambdaDeployer/Plugin.swift +++ b/Plugins/AWSLambdaDeployer/Plugin.swift @@ -23,11 +23,28 @@ struct AWSLambdaDeployer: CommandPlugin { let tool = try context.tool(named: "AWSLambdaPluginHelper") - let args = ["deploy"] + arguments + // Resolve products: use --products if provided, otherwise default to all executable targets + var argumentExtractor = ArgumentExtractor(arguments) + let productsArgument = argumentExtractor.extractOption(named: "products") + + let products: [Product] + if !productsArgument.isEmpty { + products = try context.package.products(named: productsArgument) + } else { + products = context.package.products.filter { $0 is ExecutableProduct } + } + + let productNames = products.map { $0.name }.joined(separator: ",") + + let args = ["deploy", "--products", productNames] + 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) + // Invoke the plugin helper, passing the current environment so that + // AWS credentials (env vars, HOME for ~/.aws/credentials) are available. + let process = Process() + process.executableURL = tool.url + process.arguments = args + process.environment = ProcessInfo.processInfo.environment + try process.run() process.waitUntilExit() // Check whether the subprocess invocation was successful. diff --git a/Plugins/AWSLambdaInitializer/Plugin.swift b/Plugins/AWSLambdaInitializer/Plugin.swift index 88cfc0eea..1f0d04505 100644 --- a/Plugins/AWSLambdaInitializer/Plugin.swift +++ b/Plugins/AWSLambdaInitializer/Plugin.swift @@ -24,9 +24,12 @@ struct AWSLambdaPackager: CommandPlugin { 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) + // Invoke the plugin helper, passing the current environment. + let process = Process() + process.executableURL = tool.url + process.arguments = args + process.environment = ProcessInfo.processInfo.environment + try process.run() process.waitUntilExit() // Check whether the subprocess invocation was successful. diff --git a/Plugins/AWSLambdaPackager/Plugin.swift b/Plugins/AWSLambdaPackager/Plugin.swift new file mode 100644 index 000000000..ed862b61f --- /dev/null +++ b/Plugins/AWSLambdaPackager/Plugin.swift @@ -0,0 +1,139 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// 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 +struct AWSLambdaPackager: CommandPlugin { + + func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws { + + Diagnostics.warning( + "'archive' is deprecated. Please use 'swift package lambda-build' instead." + ) + + // Resolve context-dependent values (same as AWSLambdaBuilder) + let outputDirectory: URL + let products: [Product] + let buildConfiguration: PackageManager.BuildConfiguration + let packageID: String = context.package.id + let packageDisplayName = context.package.displayName + let packageDirectory = context.package.directoryURL + let dockerToolPath = try context.tool(named: "docker").url + let zipToolPath = try context.tool(named: "zip").url + + var argumentExtractor = ArgumentExtractor(arguments) + + let outputPathArgument = argumentExtractor.extractOption(named: "output-path") + let outputDirectoryArgument = argumentExtractor.extractOption(named: "output-directory") + let productsArgument = argumentExtractor.extractOption(named: "products") + let configurationArgument = argumentExtractor.extractOption(named: "configuration") + + // output directory + if let outputPath = outputPathArgument.first ?? outputDirectoryArgument.first { + #if os(Linux) + var isDirectory: Bool = false + #else + var isDirectory: ObjCBool = false + #endif + guard FileManager.default.fileExists(atPath: outputPath, isDirectory: &isDirectory) + else { + throw PackagerErrors.invalidArgument("invalid output directory '\(outputPath)'") + } + outputDirectory = URL(string: outputPath)! + } else { + outputDirectory = context.pluginWorkDirectoryURL.appending(path: "\(AWSLambdaPackager.self)") + } + + // products + let explicitProducts = !productsArgument.isEmpty + if explicitProducts { + let _products = try context.package.products(named: productsArgument) + for product in _products { + guard product is ExecutableProduct else { + throw PackagerErrors.invalidArgument("product named '\(product.name)' is not an executable product") + } + } + products = _products + } else { + products = context.package.products.filter { $0 is ExecutableProduct } + } + + // build configuration + if let buildConfigurationName = configurationArgument.first { + guard let _buildConfiguration = PackageManager.BuildConfiguration(rawValue: buildConfigurationName) else { + throw PackagerErrors.invalidArgument("invalid build configuration named '\(buildConfigurationName)'") + } + buildConfiguration = _buildConfiguration + } else { + buildConfiguration = .release + } + + // Build the resolved arguments for the helper + let tool = try context.tool(named: "AWSLambdaPluginHelper") + let args = + [ + "build", + "--output-path", outputDirectory.path(), + "--products", products.map { $0.name }.joined(separator: ","), + "--configuration", buildConfiguration.rawValue, + "--package-id", packageID, + "--package-display-name", packageDisplayName, + "--package-directory", packageDirectory.path(), + "--docker-tool-path", dockerToolPath.path, + "--zip-tool-path", zipToolPath.path, + ] + arguments + + // Invoke the plugin helper, passing the current environment + let process = Process() + process.executableURL = tool.url + process.arguments = args + process.environment = ProcessInfo.processInfo.environment + try process.run() + process.waitUntilExit() + + if !(process.terminationReason == .exit && process.terminationStatus == 0) { + let problem = "\(process.terminationReason):\(process.terminationStatus)" + Diagnostics.error("AWSLambdaPluginHelper invocation failed: \(problem)") + } + } +} + +private enum PackagerErrors: Error, CustomStringConvertible { + case invalidArgument(String) + + var description: String { + switch self { + case .invalidArgument(let description): + return description + } + } +} + +extension PackageManager.BuildResult { + func executableArtifact(for product: Product) -> PackageManager.BuildResult.BuiltArtifact? { + let executables = self.builtArtifacts.filter { + $0.kind == .executable && $0.url.lastPathComponent == product.name + } + guard !executables.isEmpty else { + return nil + } + guard executables.count == 1, let executable = executables.first else { + return nil + } + return executable + } +} diff --git a/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift b/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift index 18de7f8de..389b47712 100644 --- a/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift +++ b/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift @@ -26,7 +26,12 @@ struct AWSLambdaPluginHelper { public static func main() async throws { let args = CommandLine.arguments let helper = AWSLambdaPluginHelper() - let command = try helper.command(from: args) + + guard let command = helper.command(from: args) else { + helper.displayHelpMessage() + return + } + switch command { case .`init`: try await Initializer().initialize(arguments: args) @@ -37,22 +42,46 @@ struct AWSLambdaPluginHelper { } } - private func command(from arguments: [String]) throws -> Command { + /// Returns nil when help should be displayed (no args, "help", "--help", or invalid command). + private func command(from arguments: [String]) -> Command? { let args = CommandLine.arguments - guard args.count > 2 else { - throw AWSLambdaPluginHelperError.noCommand + guard args.count > 1 else { + return nil } + let commandName = args[1] - guard let command = Command(rawValue: commandName) else { - throw AWSLambdaPluginHelperError.invalidCommand(commandName) + + if commandName == "help" || commandName == "--help" || commandName == "-h" { + return nil } - return command + return Command(rawValue: commandName) } -} -private enum AWSLambdaPluginHelperError: Error { - case noCommand - case invalidCommand(String) + private func displayHelpMessage() { + print( + """ + OVERVIEW: AWS Lambda Plugin Helper + + A shared helper executable for the Swift AWS Lambda Runtime plugins. + This tool is normally invoked by SwiftPM plugins (lambda-init, lambda-build, + lambda-deploy) and not called directly. + + USAGE: AWSLambdaPluginHelper [options] + + COMMANDS: + init Scaffold a new Lambda function from a template. + build Compile and package the Lambda function for deployment. + deploy Deploy the packaged Lambda function to AWS. + + Use 'AWSLambdaPluginHelper --help' for more information about a command. + + SWIFTPM PLUGIN USAGE: + swift package lambda-init --allow-writing-to-package-directory [--with-url] + swift package --allow-network-connections docker lambda-build [options] + swift package --allow-network-connections all:443 lambda-deploy [options] + """ + ) + } } diff --git a/Sources/AWSLambdaPluginHelper/Vendored/spm/ArgumentExtractor.swift b/Sources/AWSLambdaPluginHelper/ArgumentExtractor.swift similarity index 100% rename from Sources/AWSLambdaPluginHelper/Vendored/spm/ArgumentExtractor.swift rename to Sources/AWSLambdaPluginHelper/ArgumentExtractor.swift diff --git a/Sources/AWSLambdaPluginHelper/Extensions.swift b/Sources/AWSLambdaPluginHelper/Extensions.swift index c3e4da5b1..877d9752d 100644 --- a/Sources/AWSLambdaPluginHelper/Extensions.swift +++ b/Sources/AWSLambdaPluginHelper/Extensions.swift @@ -37,21 +37,4 @@ extension String { } } -extension HMAC { - public static func authenticate( - for data: [UInt8], - using key: [UInt8], - variant: HMAC.Variant = .sha2(.sha256) - ) throws -> [UInt8] { - let authenticator = HMAC(key: key, variant: variant) - return try authenticator.authenticate(data) - } - public static func authenticate( - for data: Data, - using key: [UInt8], - variant: HMAC.Variant = .sha2(.sha256) - ) throws -> [UInt8] { - let authenticator = HMAC(key: key, variant: variant) - return try authenticator.authenticate(data.bytes) - } -} + diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMClient.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMClient.swift new file mode 100644 index 000000000..3712c3997 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMClient.swift @@ -0,0 +1,158 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift AWS Lambda Runtime open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift AWS Lambda Runtime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift AWS Lambda Runtime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT + +import Logging +import SotoCore + +/// AWS Identity and Access Management (IAM) service client. +/// +/// IAM uses the AWS Query protocol. The endpoint is global: +/// `https://iam.amazonaws.com` +/// +/// Operations use POST with `Action=&Version=2010-05-08` +/// URL-encoded form body. Responses are XML. +public struct IAMClient: AWSService { + /// The underlying AWS client used for making requests. + public let client: AWSClient + /// The AWS region where requests are sent. + public let region: Region + /// Service configuration for AWS IAM. + public let config: AWSServiceConfig + + /// Initialize an IAM client. + /// - Parameters: + /// - client: The AWSClient to use for requests. + /// - region: The AWS region to target (IAM is global, defaults to us-east-1). + public init(client: AWSClient, region: Region = .useast1) { + self.client = client + self.region = region + self.config = AWSServiceConfig( + region: .useast1, + partition: .aws, + serviceName: "IAM", + serviceIdentifier: "iam", + signingName: "iam", + serviceProtocol: .query, + apiVersion: "2010-05-08", + endpoint: "https://iam.amazonaws.com", + errorType: IAMErrorType.self + ) + } + + /// Create a new version of the service with a patch applied. + public init(from: IAMClient, patch: AWSServiceConfig.Patch) { + self.client = from.client + self.region = from.region + self.config = from.config.with(patch: patch) + } + + // MARK: - Operations + + /// Creates a new IAM role. + /// - Parameter input: The request parameters. + /// - Returns: The newly created role. + @discardableResult + public func createRole(_ input: IAMCreateRoleRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> IAMCreateRoleResponse { + try await self.client.execute( + operation: "CreateRole", + path: "/", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Deletes the specified IAM role. + /// - Parameter input: The request parameters. + public func deleteRole(_ input: IAMDeleteRoleRequest, logger: Logger = AWSClient.loggingDisabled) async throws { + try await self.client.execute( + operation: "DeleteRole", + path: "/", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Retrieves information about the specified IAM role. + /// - Parameter input: The request parameters. + /// - Returns: The role details. + @discardableResult + public func getRole(_ input: IAMGetRoleRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> IAMGetRoleResponse { + try await self.client.execute( + operation: "GetRole", + path: "/", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Attaches the specified managed policy to the specified IAM role. + /// - Parameter input: The request parameters. + public func attachRolePolicy(_ input: IAMAttachRolePolicyRequest, logger: Logger = AWSClient.loggingDisabled) async throws { + try await self.client.execute( + operation: "AttachRolePolicy", + path: "/", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Removes the specified managed policy from the specified IAM role. + /// - Parameter input: The request parameters. + public func detachRolePolicy(_ input: IAMDetachRolePolicyRequest, logger: Logger = AWSClient.loggingDisabled) async throws { + try await self.client.execute( + operation: "DetachRolePolicy", + path: "/", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Adds or updates an inline policy document that is embedded in the specified IAM role. + /// - Parameter input: The request parameters. + public func putRolePolicy(_ input: IAMPutRolePolicyRequest, logger: Logger = AWSClient.loggingDisabled) async throws { + try await self.client.execute( + operation: "PutRolePolicy", + path: "/", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Deletes the specified inline policy from the specified IAM role. + /// - Parameter input: The request parameters. + public func deleteRolePolicy(_ input: IAMDeleteRolePolicyRequest, logger: Logger = AWSClient.loggingDisabled) async throws { + try await self.client.execute( + operation: "DeleteRolePolicy", + path: "/", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMErrors.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMErrors.swift new file mode 100644 index 000000000..edb9ba906 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMErrors.swift @@ -0,0 +1,86 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift AWS Lambda Runtime open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift AWS Lambda Runtime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift AWS Lambda Runtime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT + +import SotoCore + +/// Error type for AWS IAM service operations. +public struct IAMErrorType: AWSErrorType { + enum Code: String { + case entityAlreadyExistsException = "EntityAlreadyExists" + case entityTemporarilyUnmodifiableException = "EntityTemporarilyUnmodifiable" + case invalidInputException = "InvalidInput" + case limitExceededException = "LimitExceeded" + case malformedPolicyDocumentException = "MalformedPolicyDocument" + case noSuchEntityException = "NoSuchEntity" + case deleteConflictException = "DeleteConflict" + case serviceFailureException = "ServiceFailure" + case unmodifiableEntityException = "UnmodifiableEntity" + case policyNotAttachableException = "PolicyNotAttachable" + } + + private let error: Code + public let context: AWSErrorContext? + + /// The error code returned by IAM. + public var errorCode: String { self.error.rawValue } + + /// Initialize from error code string. + public init?(errorCode: String, context: AWSErrorContext) { + guard let error = Code(rawValue: errorCode) else { return nil } + self.error = error + self.context = context + } + + internal init(_ error: Code) { + self.error = error + self.context = nil + } + + /// Human-readable description. + public var description: String { + "\(self.error.rawValue): \(self.message ?? "")" + } + + /// The request was rejected because it attempted to create a resource that already exists. + public static var entityAlreadyExistsException: Self { .init(.entityAlreadyExistsException) } + + /// The request was rejected because the entity is temporarily unmodifiable. + public static var entityTemporarilyUnmodifiableException: Self { .init(.entityTemporarilyUnmodifiableException) } + + /// The request was rejected because an invalid or out-of-range value was supplied for an input parameter. + public static var invalidInputException: Self { .init(.invalidInputException) } + + /// The request was rejected because it attempted to create resources beyond the current AWS account limits. + public static var limitExceededException: Self { .init(.limitExceededException) } + + /// The request was rejected because the policy document was malformed. + public static var malformedPolicyDocumentException: Self { .init(.malformedPolicyDocumentException) } + + /// The request was rejected because it referenced a resource entity that does not exist. + public static var noSuchEntityException: Self { .init(.noSuchEntityException) } + + /// The request was rejected because it attempted to delete a resource that has attached subordinate entities. + public static var deleteConflictException: Self { .init(.deleteConflictException) } + + /// The request processing has failed because of an unknown error, exception, or failure. + public static var serviceFailureException: Self { .init(.serviceFailureException) } + + /// The request was rejected because only the service that depends on the service-linked role can modify or delete the role. + public static var unmodifiableEntityException: Self { .init(.unmodifiableEntityException) } + + /// The request failed because the provided policy is not attachable. + public static var policyNotAttachableException: Self { .init(.policyNotAttachableException) } +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMShapes.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMShapes.swift new file mode 100644 index 000000000..48f1a8393 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMShapes.swift @@ -0,0 +1,216 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift AWS Lambda Runtime open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift AWS Lambda Runtime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift AWS Lambda Runtime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT + +import SotoCore + +// MARK: - Common Shapes + +/// Represents an IAM role. +public struct IAMRole: AWSDecodableShape, Sendable { + /// The friendly name that identifies the role. + public let roleName: String? + /// The stable and unique string identifying the role. + public let roleId: String? + /// The Amazon Resource Name (ARN) specifying the role. + public let arn: String? + /// The path to the role. + public let path: String? + /// The date and time when the role was created. + public let createDate: String? + /// The policy document that grants an entity permission to assume the role. + public let assumeRolePolicyDocument: String? + /// A description of the role. + public let description: String? + + private enum CodingKeys: String, CodingKey { + case roleName = "RoleName" + case roleId = "RoleId" + case arn = "Arn" + case path = "Path" + case createDate = "CreateDate" + case assumeRolePolicyDocument = "AssumeRolePolicyDocument" + case description = "Description" + } +} + +// MARK: - CreateRole + +/// Request for the CreateRole operation. +public struct IAMCreateRoleRequest: AWSEncodableShape, Sendable { + /// The name of the role to create. + public let roleName: String + /// The trust relationship policy document that grants an entity permission to assume the role. + public let assumeRolePolicyDocument: String + /// The path to the role. + public let path: String? + /// A description of the role. + public let description: String? + + public init( + roleName: String, + assumeRolePolicyDocument: String, + path: String? = nil, + description: String? = nil + ) { + self.roleName = roleName + self.assumeRolePolicyDocument = assumeRolePolicyDocument + self.path = path + self.description = description + } + + private enum CodingKeys: String, CodingKey { + case roleName = "RoleName" + case assumeRolePolicyDocument = "AssumeRolePolicyDocument" + case path = "Path" + case description = "Description" + } +} + +/// Response for the CreateRole operation. +public struct IAMCreateRoleResponse: AWSDecodableShape, Sendable { + /// The role that was created. + public let role: IAMRole? + + private enum CodingKeys: String, CodingKey { + case role = "Role" + } +} + +// MARK: - DeleteRole + +/// Request for the DeleteRole operation. +public struct IAMDeleteRoleRequest: AWSEncodableShape, Sendable { + /// The name of the role to delete. + public let roleName: String + + public init(roleName: String) { + self.roleName = roleName + } + + private enum CodingKeys: String, CodingKey { + case roleName = "RoleName" + } +} + +// MARK: - GetRole + +/// Request for the GetRole operation. +public struct IAMGetRoleRequest: AWSEncodableShape, Sendable { + /// The name of the IAM role to get information about. + public let roleName: String + + public init(roleName: String) { + self.roleName = roleName + } + + private enum CodingKeys: String, CodingKey { + case roleName = "RoleName" + } +} + +/// Response for the GetRole operation. +public struct IAMGetRoleResponse: AWSDecodableShape, Sendable { + /// The role details. + public let role: IAMRole? + + private enum CodingKeys: String, CodingKey { + case role = "Role" + } +} + +// MARK: - AttachRolePolicy + +/// Request for the AttachRolePolicy operation. +public struct IAMAttachRolePolicyRequest: AWSEncodableShape, Sendable { + /// The name of the IAM role to attach the policy to. + public let roleName: String + /// The Amazon Resource Name (ARN) of the IAM policy to attach. + public let policyArn: String + + public init(roleName: String, policyArn: String) { + self.roleName = roleName + self.policyArn = policyArn + } + + private enum CodingKeys: String, CodingKey { + case roleName = "RoleName" + case policyArn = "PolicyArn" + } +} + +// MARK: - DetachRolePolicy + +/// Request for the DetachRolePolicy operation. +public struct IAMDetachRolePolicyRequest: AWSEncodableShape, Sendable { + /// The name of the IAM role to detach the policy from. + public let roleName: String + /// The Amazon Resource Name (ARN) of the IAM policy to detach. + public let policyArn: String + + public init(roleName: String, policyArn: String) { + self.roleName = roleName + self.policyArn = policyArn + } + + private enum CodingKeys: String, CodingKey { + case roleName = "RoleName" + case policyArn = "PolicyArn" + } +} + +// MARK: - PutRolePolicy + +/// Request for the PutRolePolicy operation. +public struct IAMPutRolePolicyRequest: AWSEncodableShape, Sendable { + /// The name of the role to associate the policy with. + public let roleName: String + /// The name of the policy document. + public let policyName: String + /// The policy document (JSON string). + public let policyDocument: String + + public init(roleName: String, policyName: String, policyDocument: String) { + self.roleName = roleName + self.policyName = policyName + self.policyDocument = policyDocument + } + + private enum CodingKeys: String, CodingKey { + case roleName = "RoleName" + case policyName = "PolicyName" + case policyDocument = "PolicyDocument" + } +} + +// MARK: - DeleteRolePolicy + +/// Request for the DeleteRolePolicy operation. +public struct IAMDeleteRolePolicyRequest: AWSEncodableShape, Sendable { + /// The name of the role the policy is associated with. + public let roleName: String + /// The name of the inline policy to delete. + public let policyName: String + + public init(roleName: String, policyName: String) { + self.roleName = roleName + self.policyName = policyName + } + + private enum CodingKeys: String, CodingKey { + case roleName = "RoleName" + case policyName = "PolicyName" + } +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaClient.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaClient.swift new file mode 100644 index 000000000..e80eaa2f4 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaClient.swift @@ -0,0 +1,187 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift AWS Lambda Runtime open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift AWS Lambda Runtime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift AWS Lambda Runtime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT + +import Logging +import NIOCore +import SotoCore + +/// AWS Lambda service client +/// +/// Provides operations for managing AWS Lambda functions. +public struct LambdaClient: AWSService, Sendable { + /// The underlying AWS client used for making requests. + public let client: AWSClient + /// The AWS region where requests are sent. + public let region: Region + /// Service configuration for AWS Lambda. + public let config: AWSServiceConfig + + /// Initialize a Lambda client. + /// - Parameters: + /// - client: The AWSClient to use for requests. + /// - region: The AWS region to target. + public init(client: AWSClient, region: Region) { + self.client = client + self.region = region + self.config = AWSServiceConfig( + region: region, + partition: region.partition, + serviceName: "Lambda", + serviceIdentifier: "lambda", + serviceProtocol: .restjson, + apiVersion: "2015-03-31", + errorType: LambdaErrorType.self + ) + } + + /// Create a new version of the service with a patch applied. + public init(from: LambdaClient, patch: AWSServiceConfig.Patch) { + self.client = from.client + self.region = from.region + self.config = from.config.with(patch: patch) + } + + // MARK: - Operations + + /// Returns information about the function or function version. + /// - Parameter input: The request parameters. + /// - Returns: The function configuration and code location. + @discardableResult + public func getFunction(_ input: GetFunctionRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> GetFunctionResponse { + try await self.client.execute( + operation: "GetFunction", + path: "/2015-03-31/functions/{FunctionName}", + httpMethod: .GET, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Creates a Lambda function. + /// - Parameter input: The request parameters. + /// - Returns: The function configuration. + @discardableResult + public func createFunction(_ input: CreateFunctionRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> CreateFunctionResponse { + try await self.client.execute( + operation: "CreateFunction", + path: "/2015-03-31/functions", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Updates a Lambda function's code. + /// - Parameter input: The request parameters. + /// - Returns: The updated function configuration. + @discardableResult + public func updateFunctionCode(_ input: UpdateFunctionCodeRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> UpdateFunctionCodeResponse { + try await self.client.execute( + operation: "UpdateFunctionCode", + path: "/2015-03-31/functions/{FunctionName}/code", + httpMethod: .PUT, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Deletes a Lambda function. + /// - Parameter input: The request parameters. + public func deleteFunction(_ input: DeleteFunctionRequest, logger: Logger = AWSClient.loggingDisabled) async throws { + try await self.client.execute( + operation: "DeleteFunction", + path: "/2015-03-31/functions/{FunctionName}", + httpMethod: .DELETE, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Creates a function URL with the specified configuration parameters. + /// - Parameter input: The request parameters. + /// - Returns: The function URL configuration. + @discardableResult + public func createFunctionUrlConfig(_ input: CreateFunctionUrlConfigRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> CreateFunctionUrlConfigResponse { + try await self.client.execute( + operation: "CreateFunctionUrlConfig", + path: "/2021-10-31/functions/{FunctionName}/url", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Deletes a Lambda function URL. + /// - Parameter input: The request parameters. + public func deleteFunctionUrlConfig(_ input: DeleteFunctionUrlConfigRequest, logger: Logger = AWSClient.loggingDisabled) async throws { + try await self.client.execute( + operation: "DeleteFunctionUrlConfig", + path: "/2021-10-31/functions/{FunctionName}/url", + httpMethod: .DELETE, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Returns the Function URL configuration for a Lambda function. + /// - Parameter input: The request parameters. + /// - Returns: The Function URL configuration including the URL endpoint. + @discardableResult + public func getFunctionUrlConfig(_ input: GetFunctionUrlConfigRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> GetFunctionUrlConfigResponse { + try await self.client.execute( + operation: "GetFunctionUrlConfig", + path: "/2021-10-31/functions/{FunctionName}/url", + httpMethod: .GET, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Grants an AWS service, AWS account, or AWS organization permission to use a function. + /// - Parameter input: The request parameters. + /// - Returns: The permission statement added to the function policy. + @discardableResult + public func addPermission(_ input: AddPermissionRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> AddPermissionResponse { + try await self.client.execute( + operation: "AddPermission", + path: "/2015-03-31/functions/{FunctionName}/policy", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Revokes function-use permission from an AWS service or another AWS account. + /// - Parameter input: The request parameters. + public func removePermission(_ input: RemovePermissionRequest, logger: Logger = AWSClient.loggingDisabled) async throws { + try await self.client.execute( + operation: "RemovePermission", + path: "/2015-03-31/functions/{FunctionName}/policy/{StatementId}", + httpMethod: .DELETE, + serviceConfig: self.config, + input: input, + logger: logger + ) + } +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaErrors.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaErrors.swift new file mode 100644 index 000000000..397446a58 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaErrors.swift @@ -0,0 +1,74 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift AWS Lambda Runtime open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift AWS Lambda Runtime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift AWS Lambda Runtime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT + +import SotoCore + +/// Error type for AWS Lambda service +public struct LambdaErrorType: AWSErrorType { + enum Code: String { + case invalidParameterValueException = "InvalidParameterValueException" + case resourceConflictException = "ResourceConflictException" + case resourceNotFoundException = "ResourceNotFoundException" + case serviceException = "ServiceException" + case tooManyRequestsException = "TooManyRequestsException" + case codeStorageExceededException = "CodeStorageExceededException" + case policyLengthExceededException = "PolicyLengthExceededException" + case preconditionFailedException = "PreconditionFailedException" + case resourceNotReadyException = "ResourceNotReadyException" + } + + private let error: Code + public let context: AWSErrorContext? + + /// Initialize from error code string + public init?(errorCode: String, context: AWSErrorContext) { + guard let error = Code(rawValue: errorCode) else { return nil } + self.error = error + self.context = context + } + + internal init(_ error: Code) { + self.error = error + self.context = nil + } + + /// The error code returned by the service + public var errorCode: String { self.error.rawValue } + + /// Human-readable description + public var description: String { + "\(self.error.rawValue): \(self.message ?? "")" + } + + /// One of the parameters in the request is not valid. + public static var invalidParameterValueException: Self { .init(.invalidParameterValueException) } + /// The resource already exists, or another operation is in progress. + public static var resourceConflictException: Self { .init(.resourceConflictException) } + /// The resource specified in the request does not exist. + public static var resourceNotFoundException: Self { .init(.resourceNotFoundException) } + /// The AWS Lambda service encountered an internal error. + public static var serviceException: Self { .init(.serviceException) } + /// The request throughput limit was exceeded. + public static var tooManyRequestsException: Self { .init(.tooManyRequestsException) } + /// Your AWS Lambda function code size exceeded the maximum allowed size. + public static var codeStorageExceededException: Self { .init(.codeStorageExceededException) } + /// The permissions policy for the resource is too large. + public static var policyLengthExceededException: Self { .init(.policyLengthExceededException) } + /// The RevisionId provided does not match the latest RevisionId for the Lambda function or alias. + public static var preconditionFailedException: Self { .init(.preconditionFailedException) } + /// The function is not in a ready state. + public static var resourceNotReadyException: Self { .init(.resourceNotReadyException) } +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaShapes.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaShapes.swift new file mode 100644 index 000000000..35f7eacda --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaShapes.swift @@ -0,0 +1,574 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift AWS Lambda Runtime open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift AWS Lambda Runtime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift AWS Lambda Runtime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT + +@_spi(SotoInternal) import SotoCore + +// MARK: - Enums + +/// Lambda function architecture +public enum LambdaArchitecture: String, Codable, Sendable { + case x86_64 = "x86_64" + case arm64 = "arm64" +} + +/// Lambda function runtime +public enum LambdaRuntime: String, Codable, Sendable { + case providedAl2023 = "provided.al2023" + case providedAl2 = "provided.al2" +} + +/// Lambda function packaging type +public enum LambdaPackageType: String, Codable, Sendable { + case zip = "Zip" + case image = "Image" +} + +/// Function URL auth type +public enum FunctionUrlAuthType: String, Codable, Sendable { + case awsIam = "AWS_IAM" + case none = "NONE" +} + +// MARK: - GetFunction + +/// Request for GetFunction operation +public struct GetFunctionRequest: AWSEncodableShape, Sendable { + /// The name of the Lambda function. + public let functionName: String + + public init(functionName: String) { + self.functionName = functionName + } + + public func encode(to encoder: Encoder) throws { + _ = encoder.container(keyedBy: CodingKeys.self) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.functionName, key: "FunctionName") + } + + private enum CodingKeys: CodingKey {} +} + +/// Response for GetFunction operation +public struct GetFunctionResponse: AWSDecodableShape, Sendable { + /// The configuration of the function. + public let configuration: FunctionConfiguration? + /// The deployment package of the function. + public let code: FunctionCodeLocation? + + private enum CodingKeys: String, CodingKey { + case configuration = "Configuration" + case code = "Code" + } +} + +/// Function configuration details +public struct FunctionConfiguration: Codable, Sendable { + /// The name of the function. + public let functionName: String? + /// The function's Amazon Resource Name (ARN). + public let functionArn: String? + /// The runtime environment. + public let runtime: String? + /// The function's execution role. + public let role: String? + /// The function's handler. + public let handler: String? + /// The size of the function's deployment package, in bytes. + public let codeSize: Int64? + /// The function's description. + public let description: String? + /// The amount of time in seconds that Lambda allows a function to run before stopping it. + public let timeout: Int? + /// The amount of memory available to the function at runtime. + public let memorySize: Int? + /// The latest updated date. + public let lastModified: String? + /// The SHA256 hash of the deployment package. + public let codeSha256: String? + /// The version of the Lambda function. + public let version: String? + /// The function's state. + public let state: String? + /// The reason for the function's current state. + public let stateReason: String? + /// The instruction set architecture. + public let architectures: [String]? + + private enum CodingKeys: String, CodingKey { + case functionName = "FunctionName" + case functionArn = "FunctionArn" + case runtime = "Runtime" + case role = "Role" + case handler = "Handler" + case codeSize = "CodeSize" + case description = "Description" + case timeout = "Timeout" + case memorySize = "MemorySize" + case lastModified = "LastModified" + case codeSha256 = "CodeSha256" + case version = "Version" + case state = "State" + case stateReason = "StateReason" + case architectures = "Architectures" + } +} + +/// Function code location details +public struct FunctionCodeLocation: Codable, Sendable { + /// The service that hosts the deployment package. + public let repositoryType: String? + /// A presigned URL to download the deployment package. + public let location: String? + + private enum CodingKeys: String, CodingKey { + case repositoryType = "RepositoryType" + case location = "Location" + } +} + +// MARK: - CreateFunction + +/// Function code for CreateFunction +public struct FunctionCode: Codable, Sendable { + /// The base64-encoded contents of the deployment package. + public let zipFile: String? + /// An Amazon S3 bucket in the same AWS Region as your function. + public let s3Bucket: String? + /// The Amazon S3 key of the deployment package. + public let s3Key: String? + + public init(zipFile: String? = nil, s3Bucket: String? = nil, s3Key: String? = nil) { + self.zipFile = zipFile + self.s3Bucket = s3Bucket + self.s3Key = s3Key + } + + private enum CodingKeys: String, CodingKey { + case zipFile = "ZipFile" + case s3Bucket = "S3Bucket" + case s3Key = "S3Key" + } +} + +/// Request for CreateFunction operation +public struct CreateFunctionRequest: AWSEncodableShape, Sendable { + /// The name of the Lambda function. + public let functionName: String + /// The Amazon Resource Name (ARN) of the function's execution role. + public let role: String + /// The identifier of the function's runtime. + public let runtime: LambdaRuntime + /// The name of the method within your code that Lambda calls to run your function. + public let handler: String + /// The code for the function. + public let code: FunctionCode + /// A description of the function. + public let description: String? + /// The amount of time (in seconds) that Lambda allows a function to run before stopping it. + public let timeout: Int? + /// The amount of memory available to the function at runtime. + public let memorySize: Int? + /// The instruction set architecture. + public let architectures: [LambdaArchitecture]? + /// The type of deployment package. + public let packageType: LambdaPackageType? + + public init( + functionName: String, + role: String, + runtime: LambdaRuntime, + handler: String, + code: FunctionCode, + description: String? = nil, + timeout: Int? = nil, + memorySize: Int? = nil, + architectures: [LambdaArchitecture]? = nil, + packageType: LambdaPackageType? = nil + ) { + self.functionName = functionName + self.role = role + self.runtime = runtime + self.handler = handler + self.code = code + self.description = description + self.timeout = timeout + self.memorySize = memorySize + self.architectures = architectures + self.packageType = packageType + } + + private enum CodingKeys: String, CodingKey { + case functionName = "FunctionName" + case role = "Role" + case runtime = "Runtime" + case handler = "Handler" + case code = "Code" + case description = "Description" + case timeout = "Timeout" + case memorySize = "MemorySize" + case architectures = "Architectures" + case packageType = "PackageType" + } +} + +/// Response for CreateFunction operation +public struct CreateFunctionResponse: AWSDecodableShape, Sendable { + /// The name of the function. + public let functionName: String? + /// The function's Amazon Resource Name (ARN). + public let functionArn: String? + /// The runtime environment. + public let runtime: String? + /// The function's execution role. + public let role: String? + /// The function's handler. + public let handler: String? + /// The size of the function's deployment package, in bytes. + public let codeSize: Int64? + /// The function's description. + public let description: String? + /// The function's state. + public let state: String? + /// The instruction set architecture. + public let architectures: [String]? + /// The version of the Lambda function. + public let version: String? + + private enum CodingKeys: String, CodingKey { + case functionName = "FunctionName" + case functionArn = "FunctionArn" + case runtime = "Runtime" + case role = "Role" + case handler = "Handler" + case codeSize = "CodeSize" + case description = "Description" + case state = "State" + case architectures = "Architectures" + case version = "Version" + } +} + +// MARK: - UpdateFunctionCode + +/// Request for UpdateFunctionCode operation +public struct UpdateFunctionCodeRequest: AWSEncodableShape, Sendable { + /// The name of the Lambda function. + public let functionName: String + /// The base64-encoded contents of the deployment package. + public let zipFile: String? + /// An Amazon S3 bucket in the same AWS Region as your function. + public let s3Bucket: String? + /// The Amazon S3 key of the deployment package. + public let s3Key: String? + /// The instruction set architecture. + public let architectures: [LambdaArchitecture]? + + public init( + functionName: String, + zipFile: String? = nil, + s3Bucket: String? = nil, + s3Key: String? = nil, + architectures: [LambdaArchitecture]? = nil + ) { + self.functionName = functionName + self.zipFile = zipFile + self.s3Bucket = s3Bucket + self.s3Key = s3Key + self.architectures = architectures + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(self.zipFile, forKey: .zipFile) + try container.encodeIfPresent(self.s3Bucket, forKey: .s3Bucket) + try container.encodeIfPresent(self.s3Key, forKey: .s3Key) + try container.encodeIfPresent(self.architectures, forKey: .architectures) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.functionName, key: "FunctionName") + } + + private enum CodingKeys: String, CodingKey { + case zipFile = "ZipFile" + case s3Bucket = "S3Bucket" + case s3Key = "S3Key" + case architectures = "Architectures" + } +} + +/// Response for UpdateFunctionCode operation +public struct UpdateFunctionCodeResponse: AWSDecodableShape, Sendable { + /// The name of the function. + public let functionName: String? + /// The function's Amazon Resource Name (ARN). + public let functionArn: String? + /// The runtime environment. + public let runtime: String? + /// The function's execution role. + public let role: String? + /// The function's handler. + public let handler: String? + /// The size of the function's deployment package, in bytes. + public let codeSize: Int64? + /// The function's state. + public let state: String? + /// The instruction set architecture. + public let architectures: [String]? + /// The version of the Lambda function. + public let version: String? + + private enum CodingKeys: String, CodingKey { + case functionName = "FunctionName" + case functionArn = "FunctionArn" + case runtime = "Runtime" + case role = "Role" + case handler = "Handler" + case codeSize = "CodeSize" + case state = "State" + case architectures = "Architectures" + case version = "Version" + } +} + +// MARK: - DeleteFunction + +/// Request for DeleteFunction operation +public struct DeleteFunctionRequest: AWSEncodableShape, Sendable { + /// The name of the Lambda function. + public let functionName: String + + public init(functionName: String) { + self.functionName = functionName + } + + public func encode(to encoder: Encoder) throws { + _ = encoder.container(keyedBy: CodingKeys.self) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.functionName, key: "FunctionName") + } + + private enum CodingKeys: CodingKey {} +} + +// MARK: - CreateFunctionUrlConfig + +/// Request for CreateFunctionUrlConfig operation +public struct CreateFunctionUrlConfigRequest: AWSEncodableShape, Sendable { + /// The name of the Lambda function. + public let functionName: String + /// The type of authentication that your function URL uses. + public let authType: FunctionUrlAuthType + + public init(functionName: String, authType: FunctionUrlAuthType) { + self.functionName = functionName + self.authType = authType + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.authType, forKey: .authType) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.functionName, key: "FunctionName") + } + + private enum CodingKeys: String, CodingKey { + case authType = "AuthType" + } +} + +/// Response for CreateFunctionUrlConfig operation +public struct CreateFunctionUrlConfigResponse: AWSDecodableShape, Sendable { + /// The HTTP URL endpoint for the function. + public let functionUrl: String? + /// The Amazon Resource Name (ARN) of the function. + public let functionArn: String? + /// The type of authentication. + public let authType: String? + /// When the function URL was created. + public let creationTime: String? + + private enum CodingKeys: String, CodingKey { + case functionUrl = "FunctionUrl" + case functionArn = "FunctionArn" + case authType = "AuthType" + case creationTime = "CreationTime" + } +} + +// MARK: - DeleteFunctionUrlConfig + +/// Request for DeleteFunctionUrlConfig operation +public struct DeleteFunctionUrlConfigRequest: AWSEncodableShape, Sendable { + /// The name of the Lambda function. + public let functionName: String + + public init(functionName: String) { + self.functionName = functionName + } + + public func encode(to encoder: Encoder) throws { + _ = encoder.container(keyedBy: CodingKeys.self) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.functionName, key: "FunctionName") + } + + private enum CodingKeys: CodingKey {} +} + +// MARK: - GetFunctionUrlConfig + +/// Request for GetFunctionUrlConfig operation +public struct GetFunctionUrlConfigRequest: AWSEncodableShape, Sendable { + /// The name of the Lambda function. + public let functionName: String + + public init(functionName: String) { + self.functionName = functionName + } + + public func encode(to encoder: Encoder) throws { + _ = encoder.container(keyedBy: CodingKeys.self) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.functionName, key: "FunctionName") + } + + private enum CodingKeys: CodingKey {} +} + +/// Response for GetFunctionUrlConfig operation +public struct GetFunctionUrlConfigResponse: AWSDecodableShape, Sendable { + /// The HTTP URL endpoint for the function. + public let functionUrl: String? + /// The type of authentication. + public let authType: String? + + private enum CodingKeys: String, CodingKey { + case functionUrl = "FunctionUrl" + case authType = "AuthType" + } +} + +// MARK: - AddPermission + +/// Request for AddPermission operation +public struct AddPermissionRequest: AWSEncodableShape, Sendable { + /// The name of the Lambda function. + public let functionName: String + /// A statement identifier that differentiates the statement from others in the same policy. + public let statementId: String + /// The action that the principal can use on the function. + public let action: String + /// The AWS service or AWS account that invokes the function. + public let principal: String + /// The identifier for your organization in AWS Organizations. + public let principalOrgID: String? + /// For AWS service principals, the ARN of the AWS resource that invokes the function. + public let sourceArn: String? + /// For Amazon S3 only, the AWS account ID of the bucket owner. + public let sourceAccount: String? + /// Specify a version or alias to add permissions to a published version of the function. + public let qualifier: String? + /// Only update the policy if the revision ID matches the ID that's specified. + public let revisionId: String? + /// The type of authentication that your function URL uses. + public let functionUrlAuthType: FunctionUrlAuthType? + + public init( + functionName: String, + statementId: String, + action: String, + principal: String, + principalOrgID: String? = nil, + sourceArn: String? = nil, + sourceAccount: String? = nil, + qualifier: String? = nil, + revisionId: String? = nil, + functionUrlAuthType: FunctionUrlAuthType? = nil + ) { + self.functionName = functionName + self.statementId = statementId + self.action = action + self.principal = principal + self.principalOrgID = principalOrgID + self.sourceArn = sourceArn + self.sourceAccount = sourceAccount + self.qualifier = qualifier + self.revisionId = revisionId + self.functionUrlAuthType = functionUrlAuthType + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.statementId, forKey: .statementId) + try container.encode(self.action, forKey: .action) + try container.encode(self.principal, forKey: .principal) + try container.encodeIfPresent(self.principalOrgID, forKey: .principalOrgID) + try container.encodeIfPresent(self.sourceArn, forKey: .sourceArn) + try container.encodeIfPresent(self.sourceAccount, forKey: .sourceAccount) + try container.encodeIfPresent(self.revisionId, forKey: .revisionId) + try container.encodeIfPresent(self.functionUrlAuthType, forKey: .functionUrlAuthType) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.functionName, key: "FunctionName") + if let qualifier = self.qualifier { + requestContainer.encodeQuery(qualifier, key: "Qualifier") + } + } + + private enum CodingKeys: String, CodingKey { + case statementId = "StatementId" + case action = "Action" + case principal = "Principal" + case principalOrgID = "PrincipalOrgID" + case sourceArn = "SourceArn" + case sourceAccount = "SourceAccount" + case revisionId = "RevisionId" + case functionUrlAuthType = "FunctionUrlAuthType" + } +} + +/// Response for AddPermission operation +public struct AddPermissionResponse: AWSDecodableShape, Sendable { + /// The permission statement that's added to the function policy. + public let statement: String? + + private enum CodingKeys: String, CodingKey { + case statement = "Statement" + } +} + +// MARK: - RemovePermission + +/// Request for RemovePermission operation +public struct RemovePermissionRequest: AWSEncodableShape, Sendable { + /// The name of the Lambda function. + public let functionName: String + /// Statement ID of the permission to remove. + public let statementId: String + + public init(functionName: String, statementId: String) { + self.functionName = functionName + self.statementId = statementId + } + + public func encode(to encoder: Encoder) throws { + _ = encoder.container(keyedBy: CodingKeys.self) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.functionName, key: "FunctionName") + requestContainer.encodePath(self.statementId, key: "StatementId") + } + + private enum CodingKeys: CodingKey {} +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Client.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Client.swift new file mode 100644 index 000000000..2a2f30f95 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Client.swift @@ -0,0 +1,129 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift AWS Lambda Runtime open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift AWS Lambda Runtime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift AWS Lambda Runtime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT + +import Logging +import SotoCore + +/// Client for Amazon Simple Storage Service (S3). +/// +/// S3 uses the REST-XML protocol with path-style addressing. +/// Endpoint: `https://s3..amazonaws.com` +struct S3Client: AWSService { + let client: AWSClient + let config: AWSServiceConfig + + /// Initialize the S3 client. + /// - Parameters: + /// - client: The AWSClient used for request signing and transport. + /// - region: The AWS region to send requests to. + init(client: AWSClient, region: Region) { + self.client = client + self.config = AWSServiceConfig( + region: region, + partition: region.partition, + serviceName: "S3", + serviceIdentifier: "s3", + signingName: "s3", + serviceProtocol: .restxml, + apiVersion: "2006-03-01", + errorType: S3ErrorType.self + ) + } + + /// Create a new version of the service with a patch applied. + init(from: S3Client, patch: AWSServiceConfig.Patch) { + self.client = from.client + self.config = from.config.with(patch: patch) + } + + // MARK: - Operations + + /// Creates a new S3 bucket. + /// + /// PUT /{Bucket} + /// + /// When the region is not `us-east-1`, a `CreateBucketConfiguration` XML body + /// with the `LocationConstraint` element is included. + /// + /// - Parameter input: The request parameters. + /// - Returns: The response containing the bucket location. + @discardableResult + func createBucket(_ input: CreateBucketRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> CreateBucketResponse { + try await self.client.execute( + operation: "CreateBucket", + path: "/{Bucket}", + httpMethod: .PUT, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Determines if a bucket exists and you have permission to access it. + /// + /// HEAD /{Bucket} + /// + /// Returns an empty response on success. Throws `NotFound` if the bucket + /// does not exist or you do not have permission to access it. + /// + /// - Parameter input: The request parameters. + func headBucket(_ input: HeadBucketRequest, logger: Logger = AWSClient.loggingDisabled) async throws { + try await self.client.execute( + operation: "HeadBucket", + path: "/{Bucket}", + httpMethod: .HEAD, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Adds an object to a bucket. + /// + /// PUT /{Bucket}/{Key+} + /// + /// - Parameter input: The request parameters including the object body data. + /// - Returns: The response containing ETag and version information. + @discardableResult + func putObject(_ input: PutObjectRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> PutObjectResponse { + try await self.client.execute( + operation: "PutObject", + path: "/{Bucket}/{Key+}", + httpMethod: .PUT, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Removes an object from a bucket. + /// + /// DELETE /{Bucket}/{Key+} + /// + /// - Parameter input: The request parameters. + /// - Returns: The response containing delete marker and version information. + @discardableResult + func deleteObject(_ input: DeleteObjectRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> DeleteObjectResponse { + try await self.client.execute( + operation: "DeleteObject", + path: "/{Bucket}/{Key+}", + httpMethod: .DELETE, + serviceConfig: self.config, + input: input, + logger: logger + ) + } +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Errors.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Errors.swift new file mode 100644 index 000000000..678fbde65 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Errors.swift @@ -0,0 +1,72 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift AWS Lambda Runtime open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift AWS Lambda Runtime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift AWS Lambda Runtime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT + +import SotoCore + +/// Error type for AWS S3 service +struct S3ErrorType: AWSErrorType { + enum Code: String { + case bucketAlreadyExists = "BucketAlreadyExists" + case bucketAlreadyOwnedByYou = "BucketAlreadyOwnedByYou" + case noSuchBucket = "NoSuchBucket" + case noSuchKey = "NoSuchKey" + case notFound = "NotFound" + case invalidBucketName = "InvalidBucketName" + case invalidObjectState = "InvalidObjectState" + } + + private let error: Code + let context: AWSErrorContext? + + /// The error code returned by S3. + var errorCode: String { self.error.rawValue } + + init?(errorCode: String, context: AWSErrorContext) { + guard let error = Code(rawValue: errorCode) else { return nil } + self.error = error + self.context = context + } + + private init(_ error: Code) { + self.error = error + self.context = nil + } + + /// The requested bucket name already exists. Select a different name and try again. + static var bucketAlreadyExists: S3ErrorType { .init(.bucketAlreadyExists) } + + /// The bucket you tried to create already exists, and you own it. + static var bucketAlreadyOwnedByYou: S3ErrorType { .init(.bucketAlreadyOwnedByYou) } + + /// The specified bucket does not exist. + static var noSuchBucket: S3ErrorType { .init(.noSuchBucket) } + + /// The specified key does not exist. + static var noSuchKey: S3ErrorType { .init(.noSuchKey) } + + /// The resource was not found (used by HeadBucket/HeadObject). + static var notFound: S3ErrorType { .init(.notFound) } + + /// The specified bucket name is not valid. + static var invalidBucketName: S3ErrorType { .init(.invalidBucketName) } + + /// The operation is not valid for the object's storage class. + static var invalidObjectState: S3ErrorType { .init(.invalidObjectState) } + + var description: String { + "\(self.errorCode): \(self.context?.message ?? "")" + } +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Shapes.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Shapes.swift new file mode 100644 index 000000000..57de5bbae --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Shapes.swift @@ -0,0 +1,182 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift AWS Lambda Runtime open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift AWS Lambda Runtime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift AWS Lambda Runtime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT + +@_spi(SotoInternal) import SotoCore + +// MARK: - CreateBucket + +/// Configuration for the bucket location constraint. +struct CreateBucketConfiguration: AWSEncodableShape, Sendable { + static let _xmlRootNodeName: String? = "CreateBucketConfiguration" + static let _xmlNamespace: String? = "http://s3.amazonaws.com/doc/2006-03-01/" + + /// The Region where the bucket will be created. If you don't specify a Region, + /// the bucket is created in US East (N. Virginia) Region (us-east-1). + let locationConstraint: String? + + init(locationConstraint: String? = nil) { + self.locationConstraint = locationConstraint + } + + private enum CodingKeys: String, CodingKey { + case locationConstraint = "LocationConstraint" + } +} + +/// Request for CreateBucket operation. +struct CreateBucketRequest: AWSEncodableShape, Sendable { + /// The name of the bucket to create. + let bucket: String + /// The configuration information for the bucket, including the location constraint. + let createBucketConfiguration: CreateBucketConfiguration? + + init(bucket: String, createBucketConfiguration: CreateBucketConfiguration? = nil) { + self.bucket = bucket + self.createBucketConfiguration = createBucketConfiguration + } + + func encode(to encoder: Encoder) throws { + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.bucket, key: "Bucket") + if let config = self.createBucketConfiguration { + try config.encode(to: encoder) + } else { + _ = encoder.container(keyedBy: CodingKeys.self) + } + } + + private enum CodingKeys: CodingKey {} +} + +/// Response for CreateBucket operation. +struct CreateBucketResponse: AWSDecodableShape, Sendable { + /// The URI that identifies the bucket. + let location: String? + + init(from decoder: Decoder) throws { + let responseContainer = decoder.userInfo[.awsResponse]! as! ResponseDecodingContainer + self.location = try? responseContainer.decodeHeaderIfPresent(String.self, key: "Location") + } +} + +// MARK: - HeadBucket + +/// Request for HeadBucket operation. +struct HeadBucketRequest: AWSEncodableShape, Sendable { + /// The bucket name. + let bucket: String + + init(bucket: String) { + self.bucket = bucket + } + + func encode(to encoder: Encoder) throws { + _ = encoder.container(keyedBy: CodingKeys.self) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.bucket, key: "Bucket") + } + + private enum CodingKeys: CodingKey {} +} + +// MARK: - PutObject + +/// Request for PutObject operation. +struct PutObjectRequest: AWSEncodableShape, Sendable { + static let _options: AWSShapeOptions = [.allowStreaming] + + /// The bucket name. + let bucket: String + /// Object key for which the PUT action was initiated. + let key: String + /// Object data. + let body: AWSHTTPBody + + init(bucket: String, key: String, body: AWSHTTPBody) { + self.bucket = bucket + self.key = key + self.body = body + } + + func encode(to encoder: Encoder) throws { + _ = encoder.container(keyedBy: CodingKeys.self) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.bucket, key: "Bucket") + requestContainer.encodePath(self.key, key: "Key") + try self.body.encode(to: encoder) + } + + private enum CodingKeys: CodingKey {} +} + +/// Response for PutObject operation. +struct PutObjectResponse: AWSDecodableShape, Sendable { + /// Entity tag for the uploaded object. + let eTag: String? + /// Version of the object. + let versionId: String? + + init(from decoder: Decoder) throws { + let responseContainer = decoder.userInfo[.awsResponse]! as! ResponseDecodingContainer + self.eTag = try? responseContainer.decodeHeaderIfPresent(String.self, key: "ETag") + self.versionId = try? responseContainer.decodeHeaderIfPresent(String.self, key: "x-amz-version-id") + } +} + +// MARK: - DeleteObject + +/// Request for DeleteObject operation. +struct DeleteObjectRequest: AWSEncodableShape, Sendable { + /// The bucket name. + let bucket: String + /// Key name of the object to delete. + let key: String + /// The version ID used to reference a specific version of the object. + let versionId: String? + + init(bucket: String, key: String, versionId: String? = nil) { + self.bucket = bucket + self.key = key + self.versionId = versionId + } + + func encode(to encoder: Encoder) throws { + _ = encoder.container(keyedBy: CodingKeys.self) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.bucket, key: "Bucket") + requestContainer.encodePath(self.key, key: "Key") + if let versionId = self.versionId { + requestContainer.encodeQuery(versionId, key: "versionId") + } + } + + private enum CodingKeys: CodingKey {} +} + +/// Response for DeleteObject operation. +struct DeleteObjectResponse: AWSDecodableShape, Sendable { + /// Indicates whether the specified object version that was permanently deleted + /// was a delete marker. + let deleteMarker: Bool? + /// Returns the version ID of the delete marker. + let versionId: String? + + init(from decoder: Decoder) throws { + let responseContainer = decoder.userInfo[.awsResponse]! as! ResponseDecodingContainer + self.deleteMarker = try? responseContainer.decodeHeaderIfPresent(Bool.self, key: "x-amz-delete-marker") + self.versionId = try? responseContainer.decodeHeaderIfPresent(String.self, key: "x-amz-version-id") + } +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSClient.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSClient.swift new file mode 100644 index 000000000..5bda287e5 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSClient.swift @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// 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 +// +//===----------------------------------------------------------------------===// +// +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT +// +//===----------------------------------------------------------------------===// + +import SotoCore + +/// Client for AWS Security Token Service (STS). +/// +/// STS uses the AWS Query protocol. The endpoint is regional: +/// `https://sts..amazonaws.com` +/// +/// Operations use POST with `Action=&Version=2011-06-15` +/// URL-encoded form body. Responses are XML. +struct STSClient: AWSService { + let client: AWSClient + let config: AWSServiceConfig + + /// Initialize the STS client. + /// - Parameters: + /// - client: The AWSClient used for request signing and transport. + /// - region: The AWS region to send requests to. + init(client: AWSClient, region: Region) { + self.client = client + self.config = AWSServiceConfig( + region: region, + partition: region.partition, + serviceName: "STS", + serviceIdentifier: "sts", + signingName: "sts", + serviceProtocol: .query, + apiVersion: "2011-06-15", + errorType: STSErrorType.self + ) + } + + /// Create a new version of the service with a patch applied. + init(from: STSClient, patch: AWSServiceConfig.Patch) { + self.client = from.client + self.config = from.config.with(patch: patch) + } + + // MARK: - Operations + + /// Returns details about the IAM user or role whose credentials are used + /// to call the operation. + /// + /// No permissions are required to perform this operation. If an administrator + /// attaches a policy to your identity that explicitly denies access to the + /// `sts:GetCallerIdentity` action, you can still perform this operation. + /// + /// - Returns: A ``STSGetCallerIdentityResponse`` containing the account, ARN, + /// and user ID of the calling entity. + func getCallerIdentity() async throws -> STSGetCallerIdentityResponse { + let input = STSGetCallerIdentityRequest() + return try await self.client.execute( + operation: "GetCallerIdentity", + path: "/", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: Self.logger + ) + } + + // MARK: - Private + + private static let logger = Logger(label: "STSClient") +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSErrors.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSErrors.swift new file mode 100644 index 000000000..4e66c2692 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSErrors.swift @@ -0,0 +1,75 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// 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 +// +//===----------------------------------------------------------------------===// +// +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT +// +//===----------------------------------------------------------------------===// + +import SotoCore + +/// Error type for STS service operations. +struct STSErrorType: AWSErrorType { + enum Code: String { + case expiredTokenException = "ExpiredTokenException" + case malformedPolicyDocumentException = "MalformedPolicyDocument" + case packedPolicyTooLargeException = "PackedPolicyTooLarge" + case regionDisabledException = "RegionDisabledException" + case idpRejectedClaimException = "IDPRejectedClaim" + case invalidIdentityTokenException = "InvalidIdentityToken" + case idpCommunicationErrorException = "IDPCommunicationError" + } + + private let error: Code + let context: AWSErrorContext? + + /// The error code returned by STS. + var errorCode: String { self.error.rawValue } + + init?(errorCode: String, context: AWSErrorContext) { + guard let error = Code(rawValue: errorCode) else { return nil } + self.error = error + self.context = context + } + + private init(_ error: Code) { + self.error = error + self.context = nil + } + + /// The security token included in the request is expired. + static var expiredTokenException: STSErrorType { .init(.expiredTokenException) } + + /// The request was rejected because the policy document was malformed. + static var malformedPolicyDocumentException: STSErrorType { .init(.malformedPolicyDocumentException) } + + /// The request was rejected because the total packed size of the session policies is too large. + static var packedPolicyTooLargeException: STSErrorType { .init(.packedPolicyTooLargeException) } + + /// STS is not activated in the requested region. + static var regionDisabledException: STSErrorType { .init(.regionDisabledException) } + + /// The identity provider rejected the request because the identity claim is invalid. + static var idpRejectedClaimException: STSErrorType { .init(.idpRejectedClaimException) } + + /// The web identity token is invalid or expired. + static var invalidIdentityTokenException: STSErrorType { .init(.invalidIdentityTokenException) } + + /// The request could not be fulfilled because the identity provider (IDP) is not accessible. + static var idpCommunicationErrorException: STSErrorType { .init(.idpCommunicationErrorException) } + + var description: String { + "\(self.errorCode): \(self.context?.message ?? "")" + } +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSShapes.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSShapes.swift new file mode 100644 index 000000000..877e72b43 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSShapes.swift @@ -0,0 +1,49 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// 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 +// +//===----------------------------------------------------------------------===// +// +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT +// +//===----------------------------------------------------------------------===// + +import SotoCore + +// MARK: - Input Shapes + +/// Request shape for the GetCallerIdentity operation. +/// This operation requires no input parameters. +struct STSGetCallerIdentityRequest: AWSEncodableShape { + init() {} +} + +// MARK: - Output Shapes + +/// Response shape for the GetCallerIdentity operation. +struct STSGetCallerIdentityResponse: AWSDecodableShape { + /// The unique identifier of the calling entity. + /// The exact value depends on the type of entity that is making the call. + let userId: String? + + /// The AWS account ID number of the account that owns or contains the calling entity. + let account: String? + + /// The AWS ARN associated with the calling entity. + let arn: String? + + private enum CodingKeys: String, CodingKey { + case userId = "UserId" + case account = "Account" + case arn = "Arn" + } +} diff --git a/Sources/AWSLambdaPluginHelper/Process.swift b/Sources/AWSLambdaPluginHelper/Process.swift index 50d6fe669..c996f9960 100644 --- a/Sources/AWSLambdaPluginHelper/Process.swift +++ b/Sources/AWSLambdaPluginHelper/Process.swift @@ -17,7 +17,7 @@ import Dispatch import Foundation import Synchronization -@available(macOS 15.0, *) +@available(LambdaSwift 2.0, *) struct Utils { @discardableResult static func execute( diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Array+Extensions.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Array+Extensions.swift deleted file mode 100644 index d1b6b7d43..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Array+Extensions.swift +++ /dev/null @@ -1,171 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// -// - -// -// CryptoSwift -// -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -extension Array { - @inlinable - init(reserveCapacity: Int) { - self = [Element]() - self.reserveCapacity(reserveCapacity) - } - - @inlinable - var slice: ArraySlice { - self[self.startIndex.. Element? { - indices.contains(index) ? self[index] : nil - } -} - -extension Array where Element == UInt8 { - public init(hex: String) { - self.init(reserveCapacity: hex.unicodeScalars.lazy.underestimatedCount) - var buffer: UInt8? - var skip = hex.hasPrefix("0x") ? 2 : 0 - for char in hex.unicodeScalars.lazy { - guard skip == 0 else { - skip -= 1 - continue - } - guard char.value >= 48 && char.value <= 102 else { - removeAll() - return - } - let v: UInt8 - let c: UInt8 = UInt8(char.value) - switch c { - case let c where c <= 57: - v = c - 48 - case let c where c >= 65 && c <= 70: - v = c - 55 - case let c where c >= 97: - v = c - 87 - default: - removeAll() - return - } - if let b = buffer { - append(b << 4 | v) - buffer = nil - } else { - buffer = v - } - } - if let b = buffer { - append(b) - } - } - - public func toHexString() -> String { - `lazy`.reduce(into: "") { - var s = String($1, radix: 16) - if s.count == 1 { - s = "0" + s - } - $0 += s - } - } -} - -extension Array where Element == UInt8 { - /// split in chunks with given chunk size - @available(*, deprecated) - public func chunks(size chunksize: Int) -> [[Element]] { - var words = [[Element]]() - words.reserveCapacity(count / chunksize) - for idx in stride(from: chunksize, through: count, by: chunksize) { - words.append(Array(self[idx - chunksize.. [Element] { - // Digest.md5(self) - // } - - // public func sha1() -> [Element] { - // Digest.sha1(self) - // } - - // public func sha224() -> [Element] { - // Digest.sha224(self) - // } - - public func sha256() -> [Element] { - Digest.sha256(self) - } - - public func sha384() -> [Element] { - Digest.sha384(self) - } - - public func sha512() -> [Element] { - Digest.sha512(self) - } - - public func sha2(_ variant: SHA2.Variant) -> [Element] { - Digest.sha2(self, variant: variant) - } - - public func sha3(_ variant: SHA3.Variant) -> [Element] { - Digest.sha3(self, variant: variant) - } - - // public func crc32(seed: UInt32? = nil, reflect: Bool = true) -> UInt32 { - // Checksum.crc32(self, seed: seed, reflect: reflect) - // } - - // public func crc32c(seed: UInt32? = nil, reflect: Bool = true) -> UInt32 { - // Checksum.crc32c(self, seed: seed, reflect: reflect) - // } - - // public func crc16(seed: UInt16? = nil) -> UInt16 { - // Checksum.crc16(self, seed: seed) - // } - - // public func encrypt(cipher: Cipher) throws -> [Element] { - // try cipher.encrypt(self.slice) - // } - - // public func decrypt(cipher: Cipher) throws -> [Element] { - // try cipher.decrypt(self.slice) - // } - - public func authenticate(with authenticator: A) throws -> [Element] { - try authenticator.authenticate(self) - } -} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Authenticator.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Authenticator.swift deleted file mode 100644 index 2af6a306c..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Authenticator.swift +++ /dev/null @@ -1,35 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// - -// -// CryptoSwift -// -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -/// Message authentication code. -public protocol Authenticator { - /// Calculate Message Authentication Code (MAC) for message. - func authenticate(_ bytes: [UInt8]) throws -> [UInt8] -} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/BatchedCollections.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/BatchedCollections.swift deleted file mode 100644 index c075ad299..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/BatchedCollections.swift +++ /dev/null @@ -1,102 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// - -// -// CryptoSwift -// -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -@usableFromInline -struct BatchedCollectionIndex { - let range: Range -} - -extension BatchedCollectionIndex: Comparable { - @usableFromInline - static func == ( - lhs: BatchedCollectionIndex, - rhs: BatchedCollectionIndex - ) -> Bool { - lhs.range.lowerBound == rhs.range.lowerBound - } - - @usableFromInline - static func < ( - lhs: BatchedCollectionIndex, - rhs: BatchedCollectionIndex - ) -> Bool { - lhs.range.lowerBound < rhs.range.lowerBound - } -} - -protocol BatchedCollectionType: Collection { - associatedtype Base: Collection -} - -@usableFromInline -struct BatchedCollection: Collection { - let base: Base - let size: Int - - @usableFromInline - init(base: Base, size: Int) { - self.base = base - self.size = size - } - - @usableFromInline - typealias Index = BatchedCollectionIndex - - private func nextBreak(after idx: Base.Index) -> Base.Index { - self.base.index(idx, offsetBy: self.size, limitedBy: self.base.endIndex) ?? self.base.endIndex - } - - @usableFromInline - var startIndex: Index { - Index(range: self.base.startIndex.. Index { - Index(range: idx.range.upperBound.. Base.SubSequence { - self.base[idx.range] - } -} - -extension Collection { - @inlinable - func batched(by size: Int) -> BatchedCollection { - BatchedCollection(base: self, size: size) - } -} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Bit.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Bit.swift deleted file mode 100644 index 6c9b25dbf..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Bit.swift +++ /dev/null @@ -1,41 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// - -// -// CryptoSwift -// -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -public enum Bit: Int { - case zero - case one -} - -extension Bit { - @inlinable - func inverted() -> Bit { - self == .zero ? .one : .zero - } -} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Collections+Extensions.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Collections+Extensions.swift deleted file mode 100644 index 21beb5f64..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Collections+Extensions.swift +++ /dev/null @@ -1,75 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// -// -// CryptoSwift -// -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// -extension Collection where Self.Element == UInt8, Self.Index == Int { - // Big endian order - @inlinable - func toUInt32Array() -> [UInt32] { - guard !isEmpty else { - return [] - } - - let c = strideCount(from: startIndex, to: endIndex, by: 4) - return [UInt32](unsafeUninitializedCapacity: c) { buf, count in - var counter = 0 - for idx in stride(from: startIndex, to: endIndex, by: 4) { - let val = UInt32(bytes: self, fromIndex: idx).bigEndian - buf[counter] = val - counter += 1 - } - count = counter - assert(counter == c) - } - } - - // Big endian order - @inlinable - func toUInt64Array() -> [UInt64] { - guard !isEmpty else { - return [] - } - - let c = strideCount(from: startIndex, to: endIndex, by: 8) - return [UInt64](unsafeUninitializedCapacity: c) { buf, count in - var counter = 0 - for idx in stride(from: startIndex, to: endIndex, by: 8) { - let val = UInt64(bytes: self, fromIndex: idx).bigEndian - buf[counter] = val - counter += 1 - } - count = counter - assert(counter == c) - } - } -} - -@usableFromInline -func strideCount(from: Int, to: Int, by: Int) -> Int { - let count = to - from - return count / by + (count % by > 0 ? 1 : 0) -} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Digest.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Digest.swift deleted file mode 100644 index dc655cc9f..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Digest.swift +++ /dev/null @@ -1,93 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// - -// -// CryptoSwift -// -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -@available(*, renamed: "Digest") -public typealias Hash = Digest - -/// Hash functions to calculate Digest. -public struct Digest { - /// Calculate MD5 Digest - /// - parameter bytes: input message - /// - returns: Digest bytes - // public static func md5(_ bytes: Array) -> Array { - // MD5().calculate(for: bytes) - // } - - /// Calculate SHA1 Digest - /// - parameter bytes: input message - /// - returns: Digest bytes - public static func sha1(_ bytes: [UInt8]) -> [UInt8] { - SHA1().calculate(for: bytes) - } - - /// Calculate SHA2-224 Digest - /// - parameter bytes: input message - /// - returns: Digest bytes - public static func sha224(_ bytes: [UInt8]) -> [UInt8] { - self.sha2(bytes, variant: .sha224) - } - - /// Calculate SHA2-256 Digest - /// - parameter bytes: input message - /// - returns: Digest bytes - public static func sha256(_ bytes: [UInt8]) -> [UInt8] { - self.sha2(bytes, variant: .sha256) - } - - /// Calculate SHA2-384 Digest - /// - parameter bytes: input message - /// - returns: Digest bytes - public static func sha384(_ bytes: [UInt8]) -> [UInt8] { - self.sha2(bytes, variant: .sha384) - } - - /// Calculate SHA2-512 Digest - /// - parameter bytes: input message - /// - returns: Digest bytes - public static func sha512(_ bytes: [UInt8]) -> [UInt8] { - self.sha2(bytes, variant: .sha512) - } - - /// Calculate SHA2 Digest - /// - parameter bytes: input message - /// - parameter variant: SHA-2 variant - /// - returns: Digest bytes - public static func sha2(_ bytes: [UInt8], variant: SHA2.Variant) -> [UInt8] { - SHA2(variant: variant).calculate(for: bytes) - } - - /// Calculate SHA3 Digest - /// - parameter bytes: input message - /// - parameter variant: SHA-3 variant - /// - returns: Digest bytes - public static func sha3(_ bytes: [UInt8], variant: SHA3.Variant) -> [UInt8] { - SHA3(variant: variant).calculate(for: bytes) - } -} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/DigestType.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/DigestType.swift deleted file mode 100644 index 12e3963a5..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/DigestType.swift +++ /dev/null @@ -1,33 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// - -// -// CryptoSwift -// -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -internal protocol DigestType { - func calculate(for bytes: [UInt8]) -> [UInt8] -} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Generics.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Generics.swift deleted file mode 100644 index cccc4114c..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Generics.swift +++ /dev/null @@ -1,58 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// - -// -// CryptoSwift -// -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -/// Array of bytes. Caution: don't use directly because generic is slow. -/// -/// - parameter value: integer value -/// - parameter length: length of output array. By default size of value type -/// -/// - returns: Array of bytes -@_specialize(where T == Int) -@_specialize(where T == UInt) -@_specialize(where T == UInt8) -@_specialize(where T == UInt16) -@_specialize(where T == UInt32) -@_specialize(where T == UInt64) -@inlinable -func arrayOfBytes(value: T, length totalBytes: Int = MemoryLayout.size) -> [UInt8] { - let valuePointer = UnsafeMutablePointer.allocate(capacity: 1) - valuePointer.pointee = value - - let bytesPointer = UnsafeMutablePointer(OpaquePointer(valuePointer)) - var bytes = [UInt8](repeating: 0, count: totalBytes) - for j in 0...size, totalBytes) { - bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee - } - - valuePointer.deinitialize(count: 1) - valuePointer.deallocate() - - return bytes -} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift deleted file mode 100644 index a3977ed2e..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/HMAC.swift +++ /dev/null @@ -1,140 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// - -// -// CryptoSwift -// -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -public final class HMAC: Authenticator { - public enum Error: Swift.Error { - case authenticateError - case invalidInput - } - - public enum Variant { - // case md5 - case sha1 - case sha2(SHA2.Variant) - case sha3(SHA3.Variant) - - @available(*, deprecated, message: "Use sha2(variant) instead.") - case sha256, sha384, sha512 - - var digestLength: Int { - switch self { - case .sha1: - return SHA1.digestLength - case .sha256: - return SHA2.Variant.sha256.digestLength - case .sha384: - return SHA2.Variant.sha384.digestLength - case .sha512: - return SHA2.Variant.sha512.digestLength - case .sha2(let variant): - return variant.digestLength - case .sha3(let variant): - return variant.digestLength - // case .md5: - // return MD5.digestLength - } - } - - func calculateHash(_ bytes: [UInt8]) -> [UInt8] { - switch self { - case .sha1: - return Digest.sha1(bytes) - case .sha256: - return Digest.sha256(bytes) - case .sha384: - return Digest.sha384(bytes) - case .sha512: - return Digest.sha512(bytes) - case .sha2(let variant): - return Digest.sha2(bytes, variant: variant) - case .sha3(let variant): - return Digest.sha3(bytes, variant: variant) - // case .md5: - // return Digest.md5(bytes) - } - } - - func blockSize() -> Int { - switch self { - // case .md5: - // return MD5.blockSize - case .sha1: - return SHA1.blockSize - case .sha256: - return SHA2.Variant.sha256.blockSize - case .sha384: - return SHA2.Variant.sha384.blockSize - case .sha512: - return SHA2.Variant.sha512.blockSize - case .sha2(let variant): - return variant.blockSize - case .sha3(let variant): - return variant.blockSize - } - } - } - - var key: [UInt8] - let variant: Variant - - // public init(key: Array, variant: HMAC.Variant = .md5) { - public init(key: [UInt8], variant: HMAC.Variant = .sha2(.sha256)) { - self.variant = variant - self.key = key - - if key.count > variant.blockSize() { - let hash = variant.calculateHash(key) - self.key = hash - } - - if key.count < variant.blockSize() { - self.key = ZeroPadding().add(to: key, blockSize: variant.blockSize()) - } - } - - // MARK: Authenticator - - public func authenticate(_ bytes: [UInt8]) throws -> [UInt8] { - var opad = [UInt8](repeating: 0x5c, count: variant.blockSize()) - for idx in self.key.indices { - opad[idx] = self.key[idx] ^ opad[idx] - } - var ipad = [UInt8](repeating: 0x36, count: variant.blockSize()) - for idx in self.key.indices { - ipad[idx] = self.key[idx] ^ ipad[idx] - } - - let ipadAndMessageHash = self.variant.calculateHash(ipad + bytes) - let result = self.variant.calculateHash(opad + ipadAndMessageHash) - - // return Array(result[0..<10]) // 80 bits - return result - } -} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Int+Extension.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Int+Extension.swift deleted file mode 100644 index a2021878c..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Int+Extension.swift +++ /dev/null @@ -1,48 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// - -// -// CryptoSwift -// -// Created by Marcin Krzyzanowski on 12/08/14. -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -#if canImport(Darwin) -import Darwin -#elseif canImport(Glibc) -import Glibc -#elseif canImport(ucrt) -import ucrt -#endif - -extension FixedWidthInteger { - @inlinable - func bytes(totalBytes: Int = MemoryLayout.size) -> [UInt8] { - arrayOfBytes(value: self.littleEndian, length: totalBytes) - // TODO: adjust bytes order - // var value = self.littleEndian - // return withUnsafeBytes(of: &value, Array.init).reversed() - } -} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/NoPadding.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/NoPadding.swift deleted file mode 100644 index 17a5f3859..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/NoPadding.swift +++ /dev/null @@ -1,42 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// - -// -// CryptoSwift -// -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -struct NoPadding: PaddingProtocol { - init() { - } - - func add(to data: [UInt8], blockSize _: Int) -> [UInt8] { - data - } - - func remove(from data: [UInt8], blockSize _: Int?) -> [UInt8] { - data - } -} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift deleted file mode 100644 index d2ea35278..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Padding.swift +++ /dev/null @@ -1,55 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// - -// -// CryptoSwift -// -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -public protocol PaddingProtocol { - func add(to: [UInt8], blockSize: Int) -> [UInt8] - func remove(from: [UInt8], blockSize: Int?) -> [UInt8] -} - -public enum Padding: PaddingProtocol { - case noPadding, zeroPadding - public func add(to: [UInt8], blockSize: Int) -> [UInt8] { - switch self { - case .noPadding: - return to // NoPadding().add(to: to, blockSize: blockSize) - case .zeroPadding: - return ZeroPadding().add(to: to, blockSize: blockSize) - } - } - - public func remove(from: [UInt8], blockSize: Int?) -> [UInt8] { - switch self { - case .noPadding: - return from //NoPadding().remove(from: from, blockSize: blockSize) - case .zeroPadding: - return ZeroPadding().remove(from: from, blockSize: blockSize) - } - } -} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA1.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA1.swift deleted file mode 100644 index c4dce58dc..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA1.swift +++ /dev/null @@ -1,179 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// - -// -// CryptoSwift -// -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -public final class SHA1: DigestType { - - @usableFromInline - static let digestLength: Int = 20 // 160 / 8 - - @usableFromInline - static let blockSize: Int = 64 - - @usableFromInline - static let hashInitialValue: ContiguousArray = [ - 0x6745_2301, 0xefcd_ab89, 0x98ba_dcfe, 0x1032_5476, 0xc3d2_e1f0, - ] - - @usableFromInline - var accumulated = [UInt8]() - - @usableFromInline - var processedBytesTotalCount: Int = 0 - - @usableFromInline - var accumulatedHash: ContiguousArray = SHA1.hashInitialValue - - public init() { - } - - @inlinable - public func calculate(for bytes: [UInt8]) -> [UInt8] { - do { - return try update(withBytes: bytes.slice, isLast: true) - } catch { - return [] - } - } - - public func callAsFunction(_ bytes: [UInt8]) -> [UInt8] { - calculate(for: bytes) - } - - @usableFromInline - func process(block chunk: ArraySlice, currentHash hh: inout ContiguousArray) { - // break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15, big-endian - // Extend the sixteen 32-bit words into eighty 32-bit words: - let M = UnsafeMutablePointer.allocate(capacity: 80) - M.initialize(repeating: 0, count: 80) - defer { - M.deinitialize(count: 80) - M.deallocate() - } - - for x in 0..<80 { - switch x { - case 0...15: - let start = chunk.startIndex.advanced(by: x * 4) // * MemoryLayout.size - M[x] = UInt32(bytes: chunk, fromIndex: start) - default: - M[x] = rotateLeft(M[x - 3] ^ M[x - 8] ^ M[x - 14] ^ M[x - 16], by: 1) - } - } - - var A = hh[0] - var B = hh[1] - var C = hh[2] - var D = hh[3] - var E = hh[4] - - // Main loop - for j in 0...79 { - var f: UInt32 = 0 - var k: UInt32 = 0 - - switch j { - case 0...19: - f = (B & C) | ((~B) & D) - k = 0x5a82_7999 - case 20...39: - f = B ^ C ^ D - k = 0x6ed9_eba1 - case 40...59: - f = (B & C) | (B & D) | (C & D) - k = 0x8f1b_bcdc - case 60...79: - f = B ^ C ^ D - k = 0xca62_c1d6 - default: - break - } - - let temp = rotateLeft(A, by: 5) &+ f &+ E &+ M[j] &+ k - E = D - D = C - C = rotateLeft(B, by: 30) - B = A - A = temp - } - - hh[0] = hh[0] &+ A - hh[1] = hh[1] &+ B - hh[2] = hh[2] &+ C - hh[3] = hh[3] &+ D - hh[4] = hh[4] &+ E - } -} - -extension SHA1: Updatable { - @discardableResult @inlinable - public func update(withBytes bytes: ArraySlice, isLast: Bool = false) throws -> [UInt8] { - self.accumulated += bytes - - if isLast { - let lengthInBits = (processedBytesTotalCount + self.accumulated.count) * 8 - let lengthBytes = lengthInBits.bytes(totalBytes: 64 / 8) // A 64-bit representation of b - - // Step 1. Append padding - bitPadding(to: &self.accumulated, blockSize: SHA1.blockSize, allowance: 64 / 8) - - // Step 2. Append Length a 64-bit representation of lengthInBits - self.accumulated += lengthBytes - } - - var processedBytes = 0 - for chunk in self.accumulated.batched(by: SHA1.blockSize) { - if isLast || (self.accumulated.count - processedBytes) >= SHA1.blockSize { - self.process(block: chunk, currentHash: &self.accumulatedHash) - processedBytes += chunk.count - } - } - self.accumulated.removeFirst(processedBytes) - self.processedBytesTotalCount += processedBytes - - // output current hash - var result = [UInt8](repeating: 0, count: SHA1.digestLength) - var pos = 0 - for idx in 0..> 24) & 0xff) - result[pos + 1] = UInt8((h >> 16) & 0xff) - result[pos + 2] = UInt8((h >> 8) & 0xff) - result[pos + 3] = UInt8(h & 0xff) - pos += 4 - } - - // reset hash value for instance - if isLast { - self.accumulatedHash = SHA1.hashInitialValue - } - - return result - } -} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift deleted file mode 100644 index d2be9d49f..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA2.swift +++ /dev/null @@ -1,417 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// - -// -// CryptoSwift -// -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -// TODO: generic for process32/64 (UInt32/UInt64) -// - -public final class SHA2: DigestType { - @usableFromInline - let variant: Variant - - @usableFromInline - let size: Int - - @usableFromInline - let blockSize: Int - - @usableFromInline - let digestLength: Int - - private let k: [UInt64] - - @usableFromInline - var accumulated = [UInt8]() - - @usableFromInline - var processedBytesTotalCount: Int = 0 - - @usableFromInline - var accumulatedHash32 = [UInt32]() - - @usableFromInline - var accumulatedHash64 = [UInt64]() - - @frozen - public enum Variant: RawRepresentable { - case sha224, sha256, sha384, sha512 - - public var digestLength: Int { - self.rawValue / 8 - } - - public var blockSize: Int { - switch self { - case .sha224, .sha256: - return 64 - case .sha384, .sha512: - return 128 - } - } - - public typealias RawValue = Int - public var rawValue: RawValue { - switch self { - case .sha224: - return 224 - case .sha256: - return 256 - case .sha384: - return 384 - case .sha512: - return 512 - } - } - - public init?(rawValue: RawValue) { - switch rawValue { - case 224: - self = .sha224 - case 256: - self = .sha256 - case 384: - self = .sha384 - case 512: - self = .sha512 - default: - return nil - } - } - - @usableFromInline - var h: [UInt64] { - switch self { - case .sha224: - return [ - 0xc105_9ed8, 0x367c_d507, 0x3070_dd17, 0xf70e_5939, 0xffc0_0b31, 0x6858_1511, 0x64f9_8fa7, - 0xbefa_4fa4, - ] - case .sha256: - return [ - 0x6a09_e667, 0xbb67_ae85, 0x3c6e_f372, 0xa54f_f53a, 0x510e_527f, 0x9b05_688c, 0x1f83_d9ab, - 0x5be0_cd19, - ] - case .sha384: - return [ - 0xcbbb_9d5d_c105_9ed8, 0x629a_292a_367c_d507, 0x9159_015a_3070_dd17, 0x152f_ecd8_f70e_5939, - 0x6733_2667_ffc0_0b31, 0x8eb4_4a87_6858_1511, 0xdb0c_2e0d_64f9_8fa7, 0x47b5_481d_befa_4fa4, - ] - case .sha512: - return [ - 0x6a09_e667_f3bc_c908, 0xbb67_ae85_84ca_a73b, 0x3c6e_f372_fe94_f82b, 0xa54f_f53a_5f1d_36f1, - 0x510e_527f_ade6_82d1, 0x9b05_688c_2b3e_6c1f, 0x1f83_d9ab_fb41_bd6b, 0x5be0_cd19_137e_2179, - ] - } - } - - @usableFromInline - var finalLength: Int { - switch self { - case .sha224: - return 7 - case .sha384: - return 6 - default: - return Int.max - } - } - } - - public init(variant: SHA2.Variant) { - self.variant = variant - switch self.variant { - case .sha224, .sha256: - self.accumulatedHash32 = variant.h.map { UInt32($0) } // FIXME: UInt64 for process64 - self.blockSize = variant.blockSize - self.size = variant.rawValue - self.digestLength = variant.digestLength - self.k = [ - 0x428a_2f98, 0x7137_4491, 0xb5c0_fbcf, 0xe9b5_dba5, 0x3956_c25b, 0x59f1_11f1, 0x923f_82a4, 0xab1c_5ed5, - 0xd807_aa98, 0x1283_5b01, 0x2431_85be, 0x550c_7dc3, 0x72be_5d74, 0x80de_b1fe, 0x9bdc_06a7, 0xc19b_f174, - 0xe49b_69c1, 0xefbe_4786, 0x0fc1_9dc6, 0x240c_a1cc, 0x2de9_2c6f, 0x4a74_84aa, 0x5cb0_a9dc, 0x76f9_88da, - 0x983e_5152, 0xa831_c66d, 0xb003_27c8, 0xbf59_7fc7, 0xc6e0_0bf3, 0xd5a7_9147, 0x06ca_6351, 0x1429_2967, - 0x27b7_0a85, 0x2e1b_2138, 0x4d2c_6dfc, 0x5338_0d13, 0x650a_7354, 0x766a_0abb, 0x81c2_c92e, 0x9272_2c85, - 0xa2bf_e8a1, 0xa81a_664b, 0xc24b_8b70, 0xc76c_51a3, 0xd192_e819, 0xd699_0624, 0xf40e_3585, 0x106a_a070, - 0x19a4_c116, 0x1e37_6c08, 0x2748_774c, 0x34b0_bcb5, 0x391c_0cb3, 0x4ed8_aa4a, 0x5b9c_ca4f, 0x682e_6ff3, - 0x748f_82ee, 0x78a5_636f, 0x84c8_7814, 0x8cc7_0208, 0x90be_fffa, 0xa450_6ceb, 0xbef9_a3f7, 0xc671_78f2, - ] - case .sha384, .sha512: - self.accumulatedHash64 = variant.h - self.blockSize = variant.blockSize - self.size = variant.rawValue - self.digestLength = variant.digestLength - self.k = [ - 0x428a_2f98_d728_ae22, 0x7137_4491_23ef_65cd, 0xb5c0_fbcf_ec4d_3b2f, 0xe9b5_dba5_8189_dbbc, - 0x3956_c25b_f348_b538, - 0x59f1_11f1_b605_d019, 0x923f_82a4_af19_4f9b, 0xab1c_5ed5_da6d_8118, 0xd807_aa98_a303_0242, - 0x1283_5b01_4570_6fbe, - 0x2431_85be_4ee4_b28c, 0x550c_7dc3_d5ff_b4e2, 0x72be_5d74_f27b_896f, 0x80de_b1fe_3b16_96b1, - 0x9bdc_06a7_25c7_1235, - 0xc19b_f174_cf69_2694, 0xe49b_69c1_9ef1_4ad2, 0xefbe_4786_384f_25e3, 0x0fc1_9dc6_8b8c_d5b5, - 0x240c_a1cc_77ac_9c65, - 0x2de9_2c6f_592b_0275, 0x4a74_84aa_6ea6_e483, 0x5cb0_a9dc_bd41_fbd4, 0x76f9_88da_8311_53b5, - 0x983e_5152_ee66_dfab, - 0xa831_c66d_2db4_3210, 0xb003_27c8_98fb_213f, 0xbf59_7fc7_beef_0ee4, 0xc6e0_0bf3_3da8_8fc2, - 0xd5a7_9147_930a_a725, - 0x06ca_6351_e003_826f, 0x1429_2967_0a0e_6e70, 0x27b7_0a85_46d2_2ffc, 0x2e1b_2138_5c26_c926, - 0x4d2c_6dfc_5ac4_2aed, - 0x5338_0d13_9d95_b3df, 0x650a_7354_8baf_63de, 0x766a_0abb_3c77_b2a8, 0x81c2_c92e_47ed_aee6, - 0x9272_2c85_1482_353b, - 0xa2bf_e8a1_4cf1_0364, 0xa81a_664b_bc42_3001, 0xc24b_8b70_d0f8_9791, 0xc76c_51a3_0654_be30, - 0xd192_e819_d6ef_5218, - 0xd699_0624_5565_a910, 0xf40e_3585_5771_202a, 0x106a_a070_32bb_d1b8, 0x19a4_c116_b8d2_d0c8, - 0x1e37_6c08_5141_ab53, - 0x2748_774c_df8e_eb99, 0x34b0_bcb5_e19b_48a8, 0x391c_0cb3_c5c9_5a63, 0x4ed8_aa4a_e341_8acb, - 0x5b9c_ca4f_7763_e373, - 0x682e_6ff3_d6b2_b8a3, 0x748f_82ee_5def_b2fc, 0x78a5_636f_4317_2f60, 0x84c8_7814_a1f0_ab72, - 0x8cc7_0208_1a64_39ec, - 0x90be_fffa_2363_1e28, 0xa450_6ceb_de82_bde9, 0xbef9_a3f7_b2c6_7915, 0xc671_78f2_e372_532b, - 0xca27_3ece_ea26_619c, - 0xd186_b8c7_21c0_c207, 0xeada_7dd6_cde0_eb1e, 0xf57d_4f7f_ee6e_d178, 0x06f0_67aa_7217_6fba, - 0x0a63_7dc5_a2c8_98a6, - 0x113f_9804_bef9_0dae, 0x1b71_0b35_131c_471b, 0x28db_77f5_2304_7d84, 0x32ca_ab7b_40c7_2493, - 0x3c9e_be0a_15c9_bebc, - 0x431d_67c4_9c10_0d4c, 0x4cc5_d4be_cb3e_42b6, 0x597f_299c_fc65_7e2a, 0x5fcb_6fab_3ad6_faec, - 0x6c44_198c_4a47_5817, - ] - } - } - - @inlinable - public func calculate(for bytes: [UInt8]) -> [UInt8] { - do { - return try update(withBytes: bytes.slice, isLast: true) - } catch { - return [] - } - } - - public func callAsFunction(_ bytes: [UInt8]) -> [UInt8] { - calculate(for: bytes) - } - - @usableFromInline - func process64(block chunk: ArraySlice, currentHash hh: inout [UInt64]) { - // break chunk into sixteen 64-bit words M[j], 0 ≤ j ≤ 15, big-endian - // Extend the sixteen 64-bit words into eighty 64-bit words: - let M = UnsafeMutablePointer.allocate(capacity: self.k.count) - M.initialize(repeating: 0, count: self.k.count) - defer { - M.deinitialize(count: self.k.count) - M.deallocate() - } - for x in 0...size - M[x] = UInt64(bytes: chunk, fromIndex: start) - default: - let s0 = rotateRight(M[x - 15], by: 1) ^ rotateRight(M[x - 15], by: 8) ^ (M[x - 15] >> 7) - let s1 = rotateRight(M[x - 2], by: 19) ^ rotateRight(M[x - 2], by: 61) ^ (M[x - 2] >> 6) - M[x] = M[x - 16] &+ s0 &+ M[x - 7] &+ s1 - } - } - - var A = hh[0] - var B = hh[1] - var C = hh[2] - var D = hh[3] - var E = hh[4] - var F = hh[5] - var G = hh[6] - var H = hh[7] - - // Main loop - for j in 0.., currentHash hh: inout [UInt32]) { - // break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15, big-endian - // Extend the sixteen 32-bit words into sixty-four 32-bit words: - let M = UnsafeMutablePointer.allocate(capacity: self.k.count) - M.initialize(repeating: 0, count: self.k.count) - defer { - M.deinitialize(count: self.k.count) - M.deallocate() - } - - for x in 0...size - M[x] = UInt32(bytes: chunk, fromIndex: start) - default: - let s0 = rotateRight(M[x - 15], by: 7) ^ rotateRight(M[x - 15], by: 18) ^ (M[x - 15] >> 3) - let s1 = rotateRight(M[x - 2], by: 17) ^ rotateRight(M[x - 2], by: 19) ^ (M[x - 2] >> 10) - M[x] = M[x - 16] &+ s0 &+ M[x - 7] &+ s1 - } - } - - var A = hh[0] - var B = hh[1] - var C = hh[2] - var D = hh[3] - var E = hh[4] - var F = hh[5] - var G = hh[6] - var H = hh[7] - - // Main loop - for j in 0.., isLast: Bool = false) throws -> [UInt8] { - self.accumulated += bytes - - if isLast { - let lengthInBits = (processedBytesTotalCount + self.accumulated.count) * 8 - let lengthBytes = lengthInBits.bytes(totalBytes: self.blockSize / 8) - // A 64-bit/128-bit representation of b. blockSize fit by accident. - - // Step 1. Append padding - bitPadding(to: &self.accumulated, blockSize: self.blockSize, allowance: self.blockSize / 8) - - // Step 2. Append Length a 64-bit representation of lengthInBits - self.accumulated += lengthBytes - } - - var processedBytes = 0 - for chunk in self.accumulated.batched(by: self.blockSize) { - if isLast || (self.accumulated.count - processedBytes) >= self.blockSize { - switch self.variant { - case .sha224, .sha256: - self.process32(block: chunk, currentHash: &self.accumulatedHash32) - case .sha384, .sha512: - self.process64(block: chunk, currentHash: &self.accumulatedHash64) - } - processedBytes += chunk.count - } - } - self.accumulated.removeFirst(processedBytes) - self.processedBytesTotalCount += processedBytes - - // output current hash - var result = [UInt8](repeating: 0, count: variant.digestLength) - switch self.variant { - case .sha224, .sha256: - var pos = 0 - for idx in 0..> 24) & 0xff) - result[pos + 1] = UInt8((h >> 16) & 0xff) - result[pos + 2] = UInt8((h >> 8) & 0xff) - result[pos + 3] = UInt8(h & 0xff) - pos += 4 - } - case .sha384, .sha512: - var pos = 0 - for idx in 0..> 56) & 0xff) - result[pos + 1] = UInt8((h >> 48) & 0xff) - result[pos + 2] = UInt8((h >> 40) & 0xff) - result[pos + 3] = UInt8((h >> 32) & 0xff) - result[pos + 4] = UInt8((h >> 24) & 0xff) - result[pos + 5] = UInt8((h >> 16) & 0xff) - result[pos + 6] = UInt8((h >> 8) & 0xff) - result[pos + 7] = UInt8(h & 0xff) - pos += 8 - } - } - - // reset hash value for instance - if isLast { - switch self.variant { - case .sha224, .sha256: - // FIXME: UInt64 for process64 - self.accumulatedHash32 = self.variant.h.lazy.map { UInt32($0) } - case .sha384, .sha512: - self.accumulatedHash64 = self.variant.h - } - } - - return result - } -} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA3.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA3.swift deleted file mode 100644 index 0662d6ba1..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/SHA3.swift +++ /dev/null @@ -1,317 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// - -// -// CryptoSwift -// -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -// http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf -// http://keccak.noekeon.org/specs_summary.html -// - -#if canImport(Darwin) -import Darwin -#elseif canImport(Glibc) -import Glibc -#elseif canImport(ucrt) -import ucrt -#endif - -public final class SHA3: DigestType { - let round_constants: [UInt64] = [ - 0x0000_0000_0000_0001, 0x0000_0000_0000_8082, 0x8000_0000_0000_808a, 0x8000_0000_8000_8000, - 0x0000_0000_0000_808b, 0x0000_0000_8000_0001, 0x8000_0000_8000_8081, 0x8000_0000_0000_8009, - 0x0000_0000_0000_008a, 0x0000_0000_0000_0088, 0x0000_0000_8000_8009, 0x0000_0000_8000_000a, - 0x0000_0000_8000_808b, 0x8000_0000_0000_008b, 0x8000_0000_0000_8089, 0x8000_0000_0000_8003, - 0x8000_0000_0000_8002, 0x8000_0000_0000_0080, 0x0000_0000_0000_800a, 0x8000_0000_8000_000a, - 0x8000_0000_8000_8081, 0x8000_0000_0000_8080, 0x0000_0000_8000_0001, 0x8000_0000_8000_8008, - ] - - public let blockSize: Int - public let digestLength: Int - public let markByte: UInt8 - - @usableFromInline - var accumulated = [UInt8]() - - @usableFromInline - var accumulatedHash: [UInt64] - - public enum Variant { - case sha224, sha256, sha384, sha512, keccak224, keccak256, keccak384, keccak512 - - var digestLength: Int { - 100 - (self.blockSize / 2) - } - - var blockSize: Int { - (1600 - self.outputLength * 2) / 8 - } - - var markByte: UInt8 { - switch self { - case .sha224, .sha256, .sha384, .sha512: - return 0x06 // 0x1F for SHAKE - case .keccak224, .keccak256, .keccak384, .keccak512: - return 0x01 - } - } - - public var outputLength: Int { - switch self { - case .sha224, .keccak224: - return 224 - case .sha256, .keccak256: - return 256 - case .sha384, .keccak384: - return 384 - case .sha512, .keccak512: - return 512 - } - } - } - - public init(variant: SHA3.Variant) { - self.blockSize = variant.blockSize - self.digestLength = variant.digestLength - self.markByte = variant.markByte - self.accumulatedHash = [UInt64](repeating: 0, count: self.digestLength) - } - - @inlinable - public func calculate(for bytes: [UInt8]) -> [UInt8] { - do { - return try update(withBytes: bytes.slice, isLast: true) - } catch { - return [] - } - } - - public func callAsFunction(_ bytes: [UInt8]) -> [UInt8] { - calculate(for: bytes) - } - - /// 1. For all pairs (x,z) such that 0≤x<5 and 0≤z.allocate(capacity: 5) - c.initialize(repeating: 0, count: 5) - defer { - c.deinitialize(count: 5) - c.deallocate() - } - let d = UnsafeMutablePointer.allocate(capacity: 5) - d.initialize(repeating: 0, count: 5) - defer { - d.deinitialize(count: 5) - d.deallocate() - } - - for i in 0..<5 { - c[i] = a[i] ^ a[i &+ 5] ^ a[i &+ 10] ^ a[i &+ 15] ^ a[i &+ 20] - } - - d[0] = rotateLeft(c[1], by: 1) ^ c[4] - d[1] = rotateLeft(c[2], by: 1) ^ c[0] - d[2] = rotateLeft(c[3], by: 1) ^ c[1] - d[3] = rotateLeft(c[4], by: 1) ^ c[2] - d[4] = rotateLeft(c[0], by: 1) ^ c[3] - - for i in 0..<5 { - a[i] ^= d[i] - a[i &+ 5] ^= d[i] - a[i &+ 10] ^= d[i] - a[i &+ 15] ^= d[i] - a[i &+ 20] ^= d[i] - } - } - - /// A′[x, y, z]=A[(x &+ 3y) mod 5, x, z] - private func π(_ a: inout [UInt64]) { - let a1 = a[1] - a[1] = a[6] - a[6] = a[9] - a[9] = a[22] - a[22] = a[14] - a[14] = a[20] - a[20] = a[2] - a[2] = a[12] - a[12] = a[13] - a[13] = a[19] - a[19] = a[23] - a[23] = a[15] - a[15] = a[4] - a[4] = a[24] - a[24] = a[21] - a[21] = a[8] - a[8] = a[16] - a[16] = a[5] - a[5] = a[3] - a[3] = a[18] - a[18] = a[17] - a[17] = a[11] - a[11] = a[7] - a[7] = a[10] - a[10] = a1 - } - - /// For all triples (x, y, z) such that 0≤x<5, 0≤y<5, and 0≤z, currentHash hh: inout [UInt64]) { - // expand - hh[0] ^= chunk[0].littleEndian - hh[1] ^= chunk[1].littleEndian - hh[2] ^= chunk[2].littleEndian - hh[3] ^= chunk[3].littleEndian - hh[4] ^= chunk[4].littleEndian - hh[5] ^= chunk[5].littleEndian - hh[6] ^= chunk[6].littleEndian - hh[7] ^= chunk[7].littleEndian - hh[8] ^= chunk[8].littleEndian - if self.blockSize > 72 { // 72 / 8, sha-512 - hh[9] ^= chunk[9].littleEndian - hh[10] ^= chunk[10].littleEndian - hh[11] ^= chunk[11].littleEndian - hh[12] ^= chunk[12].littleEndian - if self.blockSize > 104 { // 104 / 8, sha-384 - hh[13] ^= chunk[13].littleEndian - hh[14] ^= chunk[14].littleEndian - hh[15] ^= chunk[15].littleEndian - hh[16] ^= chunk[16].littleEndian - if self.blockSize > 136 { // 136 / 8, sha-256 - hh[17] ^= chunk[17].littleEndian - // FULL_SHA3_FAMILY_SUPPORT - if self.blockSize > 144 { // 144 / 8, sha-224 - hh[18] ^= chunk[18].littleEndian - hh[19] ^= chunk[19].littleEndian - hh[20] ^= chunk[20].littleEndian - hh[21] ^= chunk[21].littleEndian - hh[22] ^= chunk[22].littleEndian - hh[23] ^= chunk[23].littleEndian - hh[24] ^= chunk[24].littleEndian - } - } - } - } - - // Keccak-f - for round in 0..<24 { - self.θ(&hh) - - hh[1] = rotateLeft(hh[1], by: 1) - hh[2] = rotateLeft(hh[2], by: 62) - hh[3] = rotateLeft(hh[3], by: 28) - hh[4] = rotateLeft(hh[4], by: 27) - hh[5] = rotateLeft(hh[5], by: 36) - hh[6] = rotateLeft(hh[6], by: 44) - hh[7] = rotateLeft(hh[7], by: 6) - hh[8] = rotateLeft(hh[8], by: 55) - hh[9] = rotateLeft(hh[9], by: 20) - hh[10] = rotateLeft(hh[10], by: 3) - hh[11] = rotateLeft(hh[11], by: 10) - hh[12] = rotateLeft(hh[12], by: 43) - hh[13] = rotateLeft(hh[13], by: 25) - hh[14] = rotateLeft(hh[14], by: 39) - hh[15] = rotateLeft(hh[15], by: 41) - hh[16] = rotateLeft(hh[16], by: 45) - hh[17] = rotateLeft(hh[17], by: 15) - hh[18] = rotateLeft(hh[18], by: 21) - hh[19] = rotateLeft(hh[19], by: 8) - hh[20] = rotateLeft(hh[20], by: 18) - hh[21] = rotateLeft(hh[21], by: 2) - hh[22] = rotateLeft(hh[22], by: 61) - hh[23] = rotateLeft(hh[23], by: 56) - hh[24] = rotateLeft(hh[24], by: 14) - - self.π(&hh) - self.χ(&hh) - self.ι(&hh, round: round) - } - } -} - -extension SHA3: Updatable { - - @inlinable - public func update(withBytes bytes: ArraySlice, isLast: Bool = false) throws -> [UInt8] { - self.accumulated += bytes - - if isLast { - // Add padding - let markByteIndex = self.accumulated.count - - // We need to always pad the input. Even if the input is a multiple of blockSize. - let r = self.blockSize * 8 - let q = (r / 8) - (accumulated.count % (r / 8)) - self.accumulated += [UInt8](repeating: 0, count: q) - - self.accumulated[markByteIndex] |= self.markByte - self.accumulated[self.accumulated.count - 1] |= 0x80 - } - - var processedBytes = 0 - for chunk in self.accumulated.batched(by: self.blockSize) { - if isLast || (self.accumulated.count - processedBytes) >= self.blockSize { - self.process(block: chunk.toUInt64Array().slice, currentHash: &self.accumulatedHash) - processedBytes += chunk.count - } - } - self.accumulated.removeFirst(processedBytes) - - // TODO: verify performance, reduce vs for..in - let result = self.accumulatedHash.reduce(into: [UInt8]()) { (result, value) in - result += value.bigEndian.bytes() - } - - // reset hash value for instance - if isLast { - self.accumulatedHash = [UInt64](repeating: 0, count: self.digestLength) - } - - return Array(result[0.. -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -/// array of bytes -extension UInt16 { - @_specialize(where T == ArraySlice) - init(bytes: T) where T.Element == UInt8, T.Index == Int { - self = UInt16(bytes: bytes, fromIndex: bytes.startIndex) - } - - @_specialize(where T == ArraySlice) - init(bytes: T, fromIndex index: T.Index) where T.Element == UInt8, T.Index == Int { - if bytes.isEmpty { - self = 0 - return - } - - let count = bytes.count - - let val0 = count > 0 ? UInt16(bytes[index.advanced(by: 0)]) << 8 : 0 - let val1 = count > 1 ? UInt16(bytes[index.advanced(by: 1)]) : 0 - - self = val0 | val1 - } -} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt32+Extension.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt32+Extension.swift deleted file mode 100644 index ef71dd957..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt32+Extension.swift +++ /dev/null @@ -1,66 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// - -// -// CryptoSwift -// -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -#if canImport(Darwin) -import Darwin -#elseif canImport(Glibc) -import Glibc -#elseif canImport(ucrt) -import ucrt -#endif - -protocol _UInt32Type {} -extension UInt32: _UInt32Type {} - -/// array of bytes -extension UInt32 { - @_specialize(where T == ArraySlice) - init(bytes: T) where T.Element == UInt8, T.Index == Int { - self = UInt32(bytes: bytes, fromIndex: bytes.startIndex) - } - - @_specialize(where T == ArraySlice) - @inlinable - init(bytes: T, fromIndex index: T.Index) where T.Element == UInt8, T.Index == Int { - if bytes.isEmpty { - self = 0 - return - } - - let count = bytes.count - - let val0 = count > 0 ? UInt32(bytes[index.advanced(by: 0)]) << 24 : 0 - let val1 = count > 1 ? UInt32(bytes[index.advanced(by: 1)]) << 16 : 0 - let val2 = count > 2 ? UInt32(bytes[index.advanced(by: 2)]) << 8 : 0 - let val3 = count > 3 ? UInt32(bytes[index.advanced(by: 3)]) : 0 - - self = val0 | val1 | val2 | val3 - } -} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt64+Extension.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt64+Extension.swift deleted file mode 100644 index 652cce653..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt64+Extension.swift +++ /dev/null @@ -1,59 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// - -// -// CryptoSwift -// -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -/// array of bytes -extension UInt64 { - @_specialize(where T == ArraySlice) - init(bytes: T) where T.Element == UInt8, T.Index == Int { - self = UInt64(bytes: bytes, fromIndex: bytes.startIndex) - } - - @_specialize(where T == ArraySlice) - @inlinable - init(bytes: T, fromIndex index: T.Index) where T.Element == UInt8, T.Index == Int { - if bytes.isEmpty { - self = 0 - return - } - - let count = bytes.count - - let val0 = count > 0 ? UInt64(bytes[index.advanced(by: 0)]) << 56 : 0 - let val1 = count > 1 ? UInt64(bytes[index.advanced(by: 1)]) << 48 : 0 - let val2 = count > 2 ? UInt64(bytes[index.advanced(by: 2)]) << 40 : 0 - let val3 = count > 3 ? UInt64(bytes[index.advanced(by: 3)]) << 32 : 0 - let val4 = count > 4 ? UInt64(bytes[index.advanced(by: 4)]) << 24 : 0 - let val5 = count > 5 ? UInt64(bytes[index.advanced(by: 5)]) << 16 : 0 - let val6 = count > 6 ? UInt64(bytes[index.advanced(by: 6)]) << 8 : 0 - let val7 = count > 7 ? UInt64(bytes[index.advanced(by: 7)]) : 0 - - self = val0 | val1 | val2 | val3 | val4 | val5 | val6 | val7 - } -} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt8+Extension.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt8+Extension.swift deleted file mode 100644 index 4ff0c225a..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/UInt8+Extension.swift +++ /dev/null @@ -1,89 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// - -// -// CryptoSwift -// -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -#if canImport(Darwin) -import Darwin -#elseif canImport(Glibc) -import Glibc -#elseif canImport(ucrt) -import ucrt -#endif - -public protocol _UInt8Type {} -extension UInt8: _UInt8Type {} - -/// casting -extension UInt8 { - // cast because UInt8() because std initializer crash if value is > byte - static func with(value: UInt64) -> UInt8 { - let tmp = value & 0xff - return UInt8(tmp) - } - - static func with(value: UInt32) -> UInt8 { - let tmp = value & 0xff - return UInt8(tmp) - } - - static func with(value: UInt16) -> UInt8 { - let tmp = value & 0xff - return UInt8(tmp) - } -} - -/// Bits -extension UInt8 { - // array of bits - public func bits() -> [Bit] { - let totalBitsCount = MemoryLayout.size * 8 - - var bitsArray = [Bit](repeating: Bit.zero, count: totalBitsCount) - - for j in 0.. String { - var s = String() - let arr: [Bit] = self.bits() - for idx in arr.indices { - s += (arr[idx] == Bit.one ? "1" : "0") - if idx.advanced(by: 1) % 8 == 0 { s += " " } - } - return s - } -} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Updatable.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Updatable.swift deleted file mode 100644 index bb525da88..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Updatable.swift +++ /dev/null @@ -1,130 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// - -// -// CryptoSwift -// -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -/// A type that supports incremental updates. For example Digest or Cipher may be updatable -/// and calculate result incerementally. -public protocol Updatable { - /// Update given bytes in chunks. - /// - /// - parameter bytes: Bytes to process. - /// - parameter isLast: Indicate if given chunk is the last one. No more updates after this call. - /// - returns: Processed partial result data or empty array. - mutating func update(withBytes bytes: ArraySlice, isLast: Bool) throws -> [UInt8] - - /// Update given bytes in chunks. - /// - /// - Parameters: - /// - bytes: Bytes to process. - /// - isLast: Indicate if given chunk is the last one. No more updates after this call. - /// - output: Resulting bytes callback. - /// - Returns: Processed partial result data or empty array. - mutating func update(withBytes bytes: ArraySlice, isLast: Bool, output: (_ bytes: [UInt8]) -> Void) throws -} - -extension Updatable { - @inlinable - public mutating func update( - withBytes bytes: ArraySlice, - isLast: Bool = false, - output: (_ bytes: [UInt8]) -> Void - ) throws { - let processed = try update(withBytes: bytes, isLast: isLast) - if !processed.isEmpty { - output(processed) - } - } - - @inlinable - public mutating func update(withBytes bytes: ArraySlice, isLast: Bool = false) throws -> [UInt8] { - try self.update(withBytes: bytes, isLast: isLast) - } - - @inlinable - public mutating func update(withBytes bytes: [UInt8], isLast: Bool = false) throws -> [UInt8] { - try self.update(withBytes: bytes.slice, isLast: isLast) - } - - @inlinable - public mutating func update( - withBytes bytes: [UInt8], - isLast: Bool = false, - output: (_ bytes: [UInt8]) -> Void - ) throws { - try self.update(withBytes: bytes.slice, isLast: isLast, output: output) - } - - /// Finish updates. This may apply padding. - /// - parameter bytes: Bytes to process - /// - returns: Processed data. - @inlinable - public mutating func finish(withBytes bytes: ArraySlice) throws -> [UInt8] { - try self.update(withBytes: bytes, isLast: true) - } - - @inlinable - public mutating func finish(withBytes bytes: [UInt8]) throws -> [UInt8] { - try self.finish(withBytes: bytes.slice) - } - - /// Finish updates. May add padding. - /// - /// - Returns: Processed data - /// - Throws: Error - @inlinable - public mutating func finish() throws -> [UInt8] { - try self.update(withBytes: [], isLast: true) - } - - /// Finish updates. This may apply padding. - /// - parameter bytes: Bytes to process - /// - parameter output: Resulting data - /// - returns: Processed data. - @inlinable - public mutating func finish(withBytes bytes: ArraySlice, output: (_ bytes: [UInt8]) -> Void) throws { - let processed = try update(withBytes: bytes, isLast: true) - if !processed.isEmpty { - output(processed) - } - } - - @inlinable - public mutating func finish(withBytes bytes: [UInt8], output: (_ bytes: [UInt8]) -> Void) throws { - try self.finish(withBytes: bytes.slice, output: output) - } - - /// Finish updates. May add padding. - /// - /// - Parameter output: Processed data - /// - Throws: Error - @inlinable - public mutating func finish(output: ([UInt8]) -> Void) throws { - try self.finish(withBytes: [], output: output) - } -} diff --git a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Utils.swift b/Sources/AWSLambdaPluginHelper/Vendored/crypto/Utils.swift deleted file mode 100644 index 6c7b8118e..000000000 --- a/Sources/AWSLambdaPluginHelper/Vendored/crypto/Utils.swift +++ /dev/null @@ -1,146 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 -// -//===----------------------------------------------------------------------===// - -// -// CryptoSwift -// -// Copyright (C) 2014-2022 Marcin Krzyżanowski -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -@inlinable -func rotateLeft(_ value: UInt8, by: UInt8) -> UInt8 { - ((value << by) & 0xff) | (value >> (8 - by)) -} - -@inlinable -func rotateLeft(_ value: UInt16, by: UInt16) -> UInt16 { - ((value << by) & 0xffff) | (value >> (16 - by)) -} - -@inlinable -func rotateLeft(_ value: UInt32, by: UInt32) -> UInt32 { - ((value << by) & 0xffff_ffff) | (value >> (32 - by)) -} - -@inlinable -func rotateLeft(_ value: UInt64, by: UInt64) -> UInt64 { - (value << by) | (value >> (64 - by)) -} - -@inlinable -func rotateRight(_ value: UInt16, by: UInt16) -> UInt16 { - (value >> by) | (value << (16 - by)) -} - -@inlinable -func rotateRight(_ value: UInt32, by: UInt32) -> UInt32 { - (value >> by) | (value << (32 - by)) -} - -@inlinable -func rotateRight(_ value: UInt64, by: UInt64) -> UInt64 { - ((value >> by) | (value << (64 - by))) -} - -@inlinable -func reversed(_ uint8: UInt8) -> UInt8 { - var v = uint8 - v = (v & 0xf0) >> 4 | (v & 0x0f) << 4 - v = (v & 0xcc) >> 2 | (v & 0x33) << 2 - v = (v & 0xaa) >> 1 | (v & 0x55) << 1 - return v -} - -@inlinable -func reversed(_ uint32: UInt32) -> UInt32 { - var v = uint32 - v = ((v >> 1) & 0x5555_5555) | ((v & 0x5555_5555) << 1) - v = ((v >> 2) & 0x3333_3333) | ((v & 0x3333_3333) << 2) - v = ((v >> 4) & 0x0f0f_0f0f) | ((v & 0x0f0f_0f0f) << 4) - v = ((v >> 8) & 0x00ff_00ff) | ((v & 0x00ff_00ff) << 8) - v = ((v >> 16) & 0xffff) | ((v & 0xffff) << 16) - return v -} - -@inlinable -func xor(_ left: T, _ right: V) -> ArraySlice -where - T: RandomAccessCollection, - V: RandomAccessCollection, - T.Element == UInt8, - T.Index == Int, - V.Element == UInt8, - V.Index == Int -{ - xor(left, right).slice -} - -@inlinable -func xor(_ left: T, _ right: V) -> [UInt8] -where - T: RandomAccessCollection, - V: RandomAccessCollection, - T.Element == UInt8, - T.Index == Int, - V.Element == UInt8, - V.Index == Int -{ - let length = Swift.min(left.count, right.count) - - let buf = UnsafeMutablePointer.allocate(capacity: length) - buf.initialize(repeating: 0, count: length) - defer { - buf.deinitialize(count: length) - buf.deallocate() - } - - // xor - for i in 0.. -// This software is provided 'as-is', without any express or implied warranty. -// -// In no event will the authors be held liable for any damages arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -// -// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. -// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -// - This notice may not be removed or altered from any source or binary distribution. -// - -/// All the bytes that are required to be padded are padded with zero. -/// Zero padding may not be reversible if the original file ends with one or more zero bytes. -struct ZeroPadding: PaddingProtocol { - init() { - } - - @inlinable - func add(to bytes: [UInt8], blockSize: Int) -> [UInt8] { - let paddingCount = blockSize - (bytes.count % blockSize) - if paddingCount > 0 { - return bytes + [UInt8](repeating: 0, count: paddingCount) - } - return bytes - } - - @inlinable - func remove(from bytes: [UInt8], blockSize _: Int?) -> [UInt8] { - for (idx, value) in bytes.reversed().enumerated() { - if value != 0 { - return Array(bytes[0.. [UInt8] { - Digest.sha256(data) - } - - static func hash(data: Data) -> [UInt8] { - SHA256.hash(data: data.bytes) - } - - static func hash(data: UnsafeBufferPointer) -> [UInt8] { - SHA256.hash(data: [UInt8](data)) - } -} - -// adapter for the vendored hexDigest function -extension Array where Element == UInt8 { - fileprivate func hexDigest() -> String { - // call the hexEncoded function from the Array+Extensions.swift file - self.toHexString() - } -} - -/// Amazon Web Services V4 Signer -public struct AWSSigner { - /// security credentials for accessing AWS services - public let credentials: Credential - /// service signing name. In general this is the same as the service name - public let name: String - /// AWS region you are working in - public let region: String - - static let hashedEmptyBody = SHA256.hash(data: [UInt8]()).hexDigest() - - static private let timeStampDateFormatter: DateFormatter = createTimeStampDateFormatter() - - /// Initialise the Signer class with AWS credentials - public init(credentials: Credential, name: String, region: String) { - self.credentials = credentials - self.name = name - self.region = region - } - - /// Enum for holding your body data - public enum BodyData { - case string(String) - case data(Data) - case byteBuffer(ByteBuffer) - } - - /// Generate signed headers, for a HTTP request - public func signHeaders( - url: URL, - method: HTTPMethod = .GET, - headers: HTTPHeaders = HTTPHeaders(), - body: BodyData? = nil, - date: Date = Date() - ) -> HTTPHeaders { - let bodyHash = AWSSigner.hashedPayload(body) - let dateString = AWSSigner.timestamp(date) - var headers = headers - // add date, host, sha256 and if available security token headers - headers.add(name: "X-Amz-Date", value: dateString) - headers.add(name: "host", value: url.host ?? "") - headers.add(name: "x-amz-content-sha256", value: bodyHash) - if let sessionToken = credentials.sessionToken { - headers.add(name: "x-amz-security-token", value: sessionToken) - } - - // construct signing data. - // Do this after adding the headers as it uses data from the headers - let signingData = AWSSigner.SigningData( - url: url, - method: method, - headers: headers, - body: body, - bodyHash: bodyHash, - date: dateString, - signer: self - ) - - // construct authorization string - let authorization = - "AWS4-HMAC-SHA256 " - + "Credential=\(credentials.accessKeyId)/\(signingData.date)/\(region)/\(name)/aws4_request, " - + "SignedHeaders=\(signingData.signedHeaders), " + "Signature=\(signature(signingData: signingData))" - - // add Authorization header - headers.add(name: "Authorization", value: authorization) - - return headers - } - - /// Generate a signed URL, for a HTTP request - public func signURL( - url: URL, - method: HTTPMethod = .GET, - body: BodyData? = nil, - date: Date = Date(), - expires: Int = 86400 - ) -> URL { - let headers = HTTPHeaders([("host", url.host ?? "")]) - // Create signing data - var signingData = AWSSigner.SigningData( - url: url, - method: method, - headers: headers, - body: body, - date: AWSSigner.timestamp(date), - signer: self - ) - // Construct query string. - // Start with original query strings and append all the signing info. - var query = url.query ?? "" - if query.count > 0 { - query += "&" - } - query += "X-Amz-Algorithm=AWS4-HMAC-SHA256" - query += "&X-Amz-Credential=\(credentials.accessKeyId)/\(signingData.date)/\(region)/\(name)/aws4_request" - query += "&X-Amz-Date=\(signingData.datetime)" - query += "&X-Amz-Expires=\(expires)" - query += "&X-Amz-SignedHeaders=\(signingData.signedHeaders)" - if let sessionToken = credentials.sessionToken { - query += "&X-Amz-Security-Token=\(sessionToken.uriEncode())" - } - // Split the string and sort to ensure the order of query strings is the same as AWS - query = query.split(separator: "&") - .sorted() - .joined(separator: "&") - .queryEncode() - - // update unsignedURL in the signingData - // so when the canonical request is constructed it includes all the signing query items - signingData.unsignedURL = URL(string: url.absoluteString.split(separator: "?")[0] + "?" + query)! - // FIXME: NEED TO DEAL WITH SITUATION WHERE THIS FAILS - query += "&X-Amz-Signature=\(signature(signingData: signingData))" - - // Add signature to query items and build a new Request - let signedURL = URL(string: url.absoluteString.split(separator: "?")[0] + "?" + query)! - - return signedURL - } - - /// structure used to store data used throughout the signing process - struct SigningData { - let url: URL - let method: HTTPMethod - let hashedPayload: String - let datetime: String - let headersToSign: [String: String] - let signedHeaders: String - var unsignedURL: URL - - var date: String { String(datetime.prefix(8)) } - - init( - url: URL, - method: HTTPMethod = .GET, - headers: HTTPHeaders = HTTPHeaders(), - body: BodyData? = nil, - bodyHash: String? = nil, - date: String, - signer: AWSSigner - ) { - if url.path == "" { - //URL has to have trailing slash - self.url = url.appendingPathComponent("/") - } else { - self.url = url - } - self.method = method - self.datetime = date - self.unsignedURL = self.url - - if let hash = bodyHash { - self.hashedPayload = hash - } else if signer.name == "s3" { - self.hashedPayload = "UNSIGNED-PAYLOAD" - } else { - self.hashedPayload = AWSSigner.hashedPayload(body) - } - - let headersNotToSign: Set = [ - "Authorization" - ] - var headersToSign: [String: String] = [:] - var signedHeadersArray: [String] = [] - for header in headers { - if headersNotToSign.contains(header.name) { - continue - } - headersToSign[header.name] = header.value - signedHeadersArray.append(header.name.lowercased()) - } - self.headersToSign = headersToSign - self.signedHeaders = signedHeadersArray.sorted().joined(separator: ";") - } - } - - // Stage 3 Calculating signature as in - // https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html - func signature(signingData: SigningData) -> String { - - var signature = "" - do { - let kDate = try HMAC.authenticate( - for: Data(signingData.date.utf8), - using: "AWS4\(credentials.secretAccessKey)".array - ) - let kRegion = try HMAC.authenticate(for: Data(region.utf8), using: kDate) - let kService = try HMAC.authenticate(for: Data(name.utf8), using: kRegion) - let kSigning = try HMAC.authenticate(for: Data("aws4_request".utf8), using: kService) - let kSignature = try HMAC.authenticate(for: stringToSign(signingData: signingData), using: kSigning) - - signature = kSignature.toHexString() - } catch { - fatalError("HMAC computation is not supposed to throw") - } - - return signature - - // original code from Adam Fowler, using swift-crypto package: - - // let kDate = HMAC.authenticationCode(for: Data(signingData.date.utf8), using: SymmetricKey(data: Array("AWS4\(credentials.secretAccessKey)".utf8))) - // let kRegion = HMAC.authenticationCode(for: Data(region.utf8), using: SymmetricKey(data: kDate)) - // let kService = HMAC.authenticationCode(for: Data(name.utf8), using: SymmetricKey(data: kRegion)) - // let kSigning = HMAC.authenticationCode(for: Data("aws4_request".utf8), using: SymmetricKey(data: kService)) - // let kSignature = HMAC.authenticationCode(for: stringToSign(signingData: signingData), using: SymmetricKey(data: kSigning)) - // return kSignature.hexDigest() - - } - - /// Stage 2 Create the string to sign as in - // https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html - func stringToSign(signingData: SigningData) -> Data { - let stringToSign = - "AWS4-HMAC-SHA256\n" + "\(signingData.datetime)\n" + "\(signingData.date)/\(region)/\(name)/aws4_request\n" - + SHA256.hash(data: canonicalRequest(signingData: signingData)).hexDigest() - return Data(stringToSign.utf8) - } - - /// Stage 1 Create the canonical request as in - // https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html - func canonicalRequest(signingData: SigningData) -> Data { - let canonicalHeaders = signingData.headersToSign.map { - "\($0.key.lowercased()):\($0.value.trimmingCharacters(in: CharacterSet.whitespaces))" - } - .sorted() - .joined(separator: "\n") - let canonicalRequest = - "\(signingData.method.rawValue)\n" + "\(signingData.unsignedURL.path.uriEncodeWithSlash())\n" - // should really uriEncode all the query string values - + "\(signingData.unsignedURL.query ?? "")\n" - + "\(canonicalHeaders)\n\n" + "\(signingData.signedHeaders)\n" + signingData.hashedPayload - return Data(canonicalRequest.utf8) - } - - /// Create a SHA256 hash of the Requests body - static func hashedPayload(_ payload: BodyData?) -> String { - guard let payload = payload else { return hashedEmptyBody } - let hash: String? - switch payload { - case .string(let string): - hash = SHA256.hash(data: Data(string.utf8)).hexDigest() - case .data(let data): - hash = SHA256.hash(data: data).hexDigest() - case .byteBuffer(let byteBuffer): - let byteBufferView = byteBuffer.readableBytesView - hash = byteBufferView.withContiguousStorageIfAvailable { bytes in - SHA256.hash(data: bytes).hexDigest() - } - } - if let hash = hash { - return hash - } else { - return hashedEmptyBody - } - } - - /// return a hexEncoded string buffer from an array of bytes - static func hexEncoded(_ buffer: [UInt8]) -> String { - buffer.map { String(format: "%02x", $0) }.joined(separator: "") - } - /// create timestamp dateformatter - static private func createTimeStampDateFormatter() -> DateFormatter { - let formatter = DateFormatter() - formatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'" - formatter.timeZone = TimeZone(abbreviation: "UTC") - formatter.locale = Locale(identifier: "en_US_POSIX") - return formatter - } - - /// return a timestamp formatted for signing requests - static func timestamp(_ date: Date) -> String { - timeStampDateFormatter.string(from: date) - } -} - -extension String { - func queryEncode() -> String { - addingPercentEncoding(withAllowedCharacters: String.queryAllowedCharacters) ?? self - } - - func uriEncode() -> String { - addingPercentEncoding(withAllowedCharacters: String.uriAllowedCharacters) ?? self - } - - func uriEncodeWithSlash() -> String { - addingPercentEncoding(withAllowedCharacters: String.uriAllowedWithSlashCharacters) ?? self - } - - static let uriAllowedWithSlashCharacters = CharacterSet( - charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~/" - ) - static let uriAllowedCharacters = CharacterSet( - charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" - ) - static let queryAllowedCharacters = CharacterSet(charactersIn: "/;+").inverted -} - -extension Sequence where Element == UInt8 { - /// return a hexEncoded string buffer from an array of bytes - public func hexDigest() -> String { - self.map { String(format: "%02x", $0) }.joined(separator: "") - } -} diff --git a/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift b/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift index e1e52aaca..116cd5123 100644 --- a/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift @@ -19,7 +19,7 @@ import FoundationEssentials import Foundation #endif -@available(macOS 15.0, *) +@available(LambdaSwift 2.0, *) struct Builder { func build(arguments: [String]) async throws { let configuration = try BuilderConfiguration(arguments: arguments) @@ -29,12 +29,9 @@ struct Builder { return } - // display deprecation warning when building on or for Amazon Linux 2 - if self.isAmazonLinux(.al2) - || (configuration.baseDockerImage.contains("amazonlinux2") - && !configuration.baseDockerImage.contains("amazonlinux2023")) - { - self.displayDeprecationWarning() + // display informational warning only when user explicitly selects an AL2 image + if configuration.explicitAL2Image { + self.displayAL2Warning() } let builtProducts: [String: URL] @@ -45,6 +42,7 @@ struct Builder { packageIdentity: configuration.packageID, products: configuration.products, buildConfiguration: configuration.buildConfiguration, + noStrip: configuration.noStrip, verboseLogging: configuration.verboseLogging ) } else { @@ -54,11 +52,12 @@ struct Builder { packageDirectory: configuration.packageDirectory, products: configuration.products, containerCLIPath: configuration.dockerToolPath, - containerCLI: configuration.containerCLI, + containerCLI: configuration.crossCompileMethod, outputDirectory: configuration.outputDirectory, baseImage: configuration.baseDockerImage, disableDockerImageUpdate: configuration.disableDockerImageUpdate, buildConfiguration: configuration.buildConfiguration, + noStrip: configuration.noStrip, verboseLogging: configuration.verboseLogging ) } @@ -84,6 +83,7 @@ struct Builder { packageIdentity: String, products: [String], buildConfiguration: BuildConfiguration, + noStrip: Bool, verboseLogging: Bool ) throws -> [String: URL] { print("-------------------------------------------------------------------------") @@ -93,11 +93,14 @@ struct Builder { var results = [String: URL]() for product in products { print("building \"\(product)\"") - let buildArguments = [ + var buildArguments = [ "build", "-c", buildConfiguration.rawValue, "--product", product, "--static-swift-stdlib", ] + if !noStrip { + buildArguments += ["-Xlinker", "-s"] + } try Utils.execute( executable: URL(fileURLWithPath: "/usr/bin/swift"), arguments: buildArguments, @@ -127,16 +130,22 @@ struct Builder { packageDirectory: URL, products: [String], containerCLIPath: URL, - containerCLI: ContainerCLI, + containerCLI: CrossCompileMethod, outputDirectory: URL, baseImage: String, disableDockerImageUpdate: Bool, buildConfiguration: BuildConfiguration, + noStrip: Bool, verboseLogging: Bool ) throws -> [String: URL] { + // verify the container CLI binary exists at the resolved path + guard FileManager.default.fileExists(atPath: containerCLIPath.path()) else { + throw BuilderErrors.containerCLINotFound(containerCLI) + } + print("-------------------------------------------------------------------------") - print("building \"\(packageIdentity)\" in \(containerCLI.displayName)") + print("building \"\(packageIdentity)\" in \(containerCLI)") print("-------------------------------------------------------------------------") if !disableDockerImageUpdate { @@ -173,8 +182,11 @@ struct Builder { var builtProducts = [String: URL]() for product in products { print("building \"\(product)\"") - let buildCommand = + var buildCommand = "swift build -c \(buildConfiguration.rawValue) --product \(product) --static-swift-stdlib" + if !noStrip { + buildCommand += " -Xlinker -s" + } 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 ../.. @@ -329,27 +341,13 @@ struct Builder { } } - private func displayDeprecationWarning() { - let separator = String(repeating: "=", count: 68) - let red = "\u{001b}[38;2;255;66;69m" + private func displayAL2Warning() { + let yellow = "\u{001b}[33m" let reset = "\u{001b}[0m" - print("") - print("\(red)\(separator)") - print("WARNING: Amazon Linux 2 reaches End of Life on June 30, 2026.") - print("") - print("You must migrate to Amazon Linux 2023.") - print("Amazon Linux 2023 will become the default after June 30, 2026.") - print("") - print("To switch now, re-run with:") - print(" --base-docker-image swift:amazonlinux2023") - print("") - print("When using Amazon Linux 2023, you must also update your Lambda") - print("deployment to use the provided.al2023 runtime.") - print("") - print("For more information: https://aws.amazon.com/amazon-linux-2") - print("Available images: https://hub.docker.com/_/swift/tags?name=amazonlinux") - print("\(separator)\(reset)") - print("") + print( + "\(yellow)warning: Amazon Linux 2 is deprecated. " + + "Consider migrating to Amazon Linux 2023 (--base-docker-image swift:-amazonlinux2023).\(reset)" + ) } private func displayHelpMessage() { @@ -367,7 +365,8 @@ struct Builder { [--swift-version ] [--base-docker-image ] [--disable-docker-image-update] - [--container-cli ] + [--cross-compile ] + [--no-strip] OPTIONS: @@ -380,33 +379,36 @@ struct Builder { (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. + 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) - Note: Amazon Linux 2023 will become the default after June 30, 2026. + (default: swift:-amazonlinux2023) Visit Docker Hub for all available swift tags: https://hub.docker.com/_/swift/tags?name=amazonlinux This parameter cannot be used when --swift-version is specified. - --disable-docker-image-update Do not attempt to update the docker image - --container-cli The container CLI to use (docker or container) + --disable-docker-image-update Do not attempt to update the docker image. + --cross-compile The cross-compilation method to use. + Values: docker, container, swift-static-sdk, custom-sdk (default is docker) + Note: swift-static-sdk and custom-sdk are not yet supported. + --no-strip Do not strip debug symbols from the binary. --help Show help information. """ ) } } -@available(macOS 15.0, *) -private enum ContainerCLI: String, CustomStringConvertible { +@available(LambdaSwift 2.0, *) +enum CrossCompileMethod: String, CustomStringConvertible { case docker case container + case swiftStaticSdk = "swift-static-sdk" + case customSdk = "custom-sdk" - var executableName: String { - self.rawValue - } - - var displayName: String { - self.rawValue + var isSupported: Bool { + switch self { + case .docker, .container: return true + case .swiftStaticSdk, .customSdk: return false + } } static func parse(_ value: String?) throws -> Self { @@ -414,21 +416,32 @@ private enum ContainerCLI: String, CustomStringConvertible { return .docker } - guard let tool = ContainerCLI(rawValue: value.lowercased()) else { - throw BuilderErrors.invalidArgument("invalid container CLI '\(value)'. Use 'docker' or 'container'.") + guard let method = CrossCompileMethod(rawValue: value.lowercased()) else { + throw BuilderErrors.invalidArgument( + "invalid cross-compile method '\(value)'. Use 'docker', 'container', 'swift-static-sdk', or 'custom-sdk'." + ) } - return tool + + guard method.isSupported else { + throw BuilderErrors.unsupportedCrossCompileMethod(method) + } + + return method } + /// Returns the container CLI pull arguments for the given image. func pullArguments(image: String) -> [String] { switch self { case .docker: return ["pull", image] case .container: return ["image", "pull", image] + case .swiftStaticSdk, .customSdk: + fatalError("pullArguments should not be called for unsupported cross-compile methods") } } + /// Returns the container CLI run arguments for the given configuration. func runArguments( baseImage: String, workingDirectory: String, @@ -436,17 +449,22 @@ private enum ContainerCLI: String, CustomStringConvertible { env: [String: String]?, command: String ) -> [String] { - var args: [String] = ["run", "--rm"] - for mount in mounts { - args += ["-v", mount] - } - if let env { - for (key, value) in env.sorted(by: { $0.key < $1.key }) { - args += ["--env", "\(key)=\(value)"] + switch self { + case .docker, .container: + var args: [String] = ["run", "--rm"] + for mount in mounts { + args += ["-v", mount] + } + if let env { + for (key, value) in env.sorted(by: { $0.key < $1.key }) { + args += ["--env", "\(key)=\(value)"] + } } + args += ["-w", workingDirectory, baseImage, "bash", "-cl", command] + return args + case .swiftStaticSdk, .customSdk: + fatalError("runArguments should not be called for unsupported cross-compile methods") } - args += ["-w", workingDirectory, baseImage, "bash", "-cl", command] - return args } var description: String { @@ -454,8 +472,8 @@ private enum ContainerCLI: String, CustomStringConvertible { } } -@available(macOS 15.0, *) -private struct BuilderConfiguration: CustomStringConvertible { +@available(LambdaSwift 2.0, *) +struct BuilderConfiguration: CustomStringConvertible { // passed by the user public let help: Bool @@ -465,7 +483,9 @@ private struct BuilderConfiguration: CustomStringConvertible { public let verboseLogging: Bool public let baseDockerImage: String public let disableDockerImageUpdate: Bool - public let containerCLI: ContainerCLI + public let crossCompileMethod: CrossCompileMethod + public let noStrip: Bool + public let explicitAL2Image: Bool // passed by the plugin public let packageID: String @@ -479,6 +499,7 @@ private struct BuilderConfiguration: CustomStringConvertible { let verboseArgument = argumentExtractor.extractFlag(named: "verbose") > 0 let outputPathArgument = argumentExtractor.extractOption(named: "output-path") + let outputDirectoryArgument = argumentExtractor.extractOption(named: "output-directory") let packageIDArgument = argumentExtractor.extractOption(named: "package-id") let packageDisplayNameArgument = argumentExtractor.extractOption(named: "package-display-name") let packageDirectoryArgument = argumentExtractor.extractOption(named: "package-directory") @@ -489,7 +510,9 @@ private struct BuilderConfiguration: 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 containerCliArgument = argumentExtractor.extractOption(named: "container-cli") + let crossCompileArgument = argumentExtractor.extractOption(named: "cross-compile") + let containerCliArgument = argumentExtractor.extractOption(named: "container-cli") // deprecated alias + let noStripArgument = argumentExtractor.extractFlag(named: "no-strip") > 0 let helpArgument = argumentExtractor.extractFlag(named: "help") > 0 // help required ? @@ -529,10 +552,17 @@ private struct BuilderConfiguration: CustomStringConvertible { self.zipToolPath = URL(fileURLWithPath: zipToolPathArgument.first!) // output directory - guard !outputPathArgument.isEmpty else { + // --output-directory is a deprecated alias for --output-path (backward compatibility) + let resolvedOutputPath: String + if let outputPath = outputPathArgument.first { + resolvedOutputPath = outputPath + } else if let outputDirectory = outputDirectoryArgument.first { + print("warning: '--output-directory' is deprecated, use '--output-path' instead.") + resolvedOutputPath = outputDirectory + } else { throw BuilderErrors.invalidArgument("--output-path is required") } - self.outputDirectory = URL(fileURLWithPath: outputPathArgument.first!) + self.outputDirectory = URL(fileURLWithPath: resolvedOutputPath) // products guard !productsArgument.isEmpty else { @@ -556,12 +586,21 @@ private struct BuilderConfiguration: CustomStringConvertible { let swiftVersion = swiftVersionArgument.first ?? .none // undefined version will yield the latest docker image self.baseDockerImage = - baseDockerImageArgument.first ?? "swift:\(swiftVersion.map { $0 + "-" } ?? "")amazonlinux2" + baseDockerImageArgument.first ?? "swift:\(swiftVersion.map { $0 + "-" } ?? "")amazonlinux2023" self.disableDockerImageUpdate = disableDockerImageUpdateArgument - self.containerCLI = try ContainerCLI.parse( - containerCliArgument.first - ) + // --container-cli is a deprecated alias for --cross-compile (backward compatibility) + let resolvedCrossCompile = crossCompileArgument.first ?? containerCliArgument.first + self.crossCompileMethod = try CrossCompileMethod.parse(resolvedCrossCompile) + self.noStrip = noStripArgument + + // detect when user explicitly provides an AL2 (not AL2023) base image + if let explicitImage = baseDockerImageArgument.first { + self.explicitAL2Image = explicitImage.contains("amazonlinux2") + && !explicitImage.contains("amazonlinux2023") + } else { + self.explicitAL2Image = false + } if self.verboseLogging { print("-------------------------------------------------------------------------") @@ -580,7 +619,7 @@ private struct BuilderConfiguration: CustomStringConvertible { dockerToolPath: \(self.dockerToolPath) baseDockerImage: \(self.baseDockerImage) disableDockerImageUpdate: \(self.disableDockerImageUpdate) - containerCLI: \(self.containerCLI) + crossCompileMethod: \(self.crossCompileMethod) zipToolPath: \(self.zipToolPath) packageID: \(self.packageID) packageDisplayName: \(self.packageDisplayName) @@ -590,11 +629,14 @@ private struct BuilderConfiguration: CustomStringConvertible { } } -private enum BuilderErrors: Error, CustomStringConvertible { +@available(LambdaSwift 2.0, *) +enum BuilderErrors: Error, CustomStringConvertible { case invalidArgument(String) case unsupportedPlatform(String) case unknownProduct(String) case productExecutableNotFound(String) + case unsupportedCrossCompileMethod(CrossCompileMethod) + case containerCLINotFound(CrossCompileMethod) case failedWritingDockerfile case failedParsingDockerOutput(String) case processFailed([String], Int32) @@ -609,6 +651,27 @@ private enum BuilderErrors: Error, CustomStringConvertible { return description case .productExecutableNotFound(let product): return "product executable not found '\(product)'" + case .unsupportedCrossCompileMethod(let method): + return + "The '\(method)' cross-compilation method is not yet supported. " + + "For information on how to install and use Swift cross-compilation SDKs, visit: " + + "https://www.swift.org/documentation/articles/static-linux-getting-started.html" + case .containerCLINotFound(let method): + switch method { + case .docker: + return + "Docker is not installed or not found at the expected path. " + + "Install Docker from https://docs.docker.com/get-docker/" + case .container: + return + "Apple's 'container' CLI is not installed or not found at the expected path. " + + "Install it from https://github.com/apple/container" + case .swiftStaticSdk, .customSdk: + return + "The '\(method)' cross-compilation method is not yet supported. " + + "For information on how to install and use Swift cross-compilation SDKs, visit: " + + "https://www.swift.org/documentation/articles/static-linux-getting-started.html" + } case .failedWritingDockerfile: return "failed writing dockerfile" case .failedParsingDockerOutput(let output): @@ -619,7 +682,8 @@ private enum BuilderErrors: Error, CustomStringConvertible { } } -private enum BuildConfiguration: String { +@available(LambdaSwift 2.0, *) +enum BuildConfiguration: String { case debug case release } diff --git a/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift b/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift index 3bba4169d..9b978a5a1 100644 --- a/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift @@ -18,8 +18,519 @@ import FoundationEssentials import Foundation #endif +import Logging +import SotoCore + +@available(LambdaSwift 2.0, *) +enum DeploymentAction: Equatable { + /// Create a new function (function does not exist yet). + case create + /// Update an existing function's code. + case update + /// Delete an existing function. + case delete +} + +@available(LambdaSwift 2.0, *) struct Deployer { + // MARK: - Account ID and Function Existence + + /// Resolves the AWS account ID by calling STS GetCallerIdentity. + func resolveAccountId(using stsClient: STSClient) async throws -> String { + do { + let response = try await stsClient.getCallerIdentity() + guard let accountId = response.account else { + throw DeployerErrors.credentialResolutionFailed( + "STS GetCallerIdentity returned no account ID" + ) + } + return accountId + } catch let error as DeployerErrors { + throw error + } catch { + throw DeployerErrors.awsAPIError( + service: "STS", + operation: "GetCallerIdentity", + message: "\(error)" + ) + } + } + + /// Checks whether a Lambda function with the given name already exists. + func functionExists( + _ functionName: String, + using lambdaClient: LambdaClient + ) async throws -> Bool { + do { + _ = try await lambdaClient.getFunction( + GetFunctionRequest(functionName: functionName) + ) + return true + } catch { + // If the error indicates the resource was not found, the function doesn't exist + let errorDescription = "\(error)" + if errorDescription.contains("ResourceNotFoundException") + || errorDescription.contains("Function not found") + { + return false + } + throw DeployerErrors.awsAPIError( + service: "Lambda", + operation: "GetFunction", + message: errorDescription + ) + } + } + + /// Determines the deployment action based on function existence and the `--delete` flag. + func determineDeploymentAction( + functionExists: Bool, + delete: Bool + ) throws -> DeploymentAction { + if delete { + guard functionExists else { + throw DeployerErrors.awsAPIError( + service: "Lambda", + operation: "DeleteFunction", + message: "cannot delete function: function does not exist" + ) + } + return .delete + } + return functionExists ? .update : .create + } + + // MARK: - S3 Staging + + /// AWS Lambda direct upload limit (50 MB compressed). + /// Archives larger than this must be staged through S3. + static let directUploadLimit: Int64 = 50 * 1024 * 1024 + + /// Constructs the deployment bucket name per the naming convention. + /// Format: `swift-aws-lambda-runtime--` + static func deploymentBucketName(region: String, accountId: String) -> String { + "swift-aws-lambda-runtime-\(region)-\(accountId)" + } + + /// Ensures the S3 deployment bucket exists. If the bucket does not exist, it is created. + /// - Parameters: + /// - bucket: The bucket name. + /// - region: The AWS region for the bucket. + /// - s3Client: The S3 client to use. + /// - verbose: Whether to emit verbose progress output. + func ensureBucketExists(bucket: String, region: Region, using s3Client: S3Client, verbose: Bool) async throws { + if verbose { + print("[verbose] Checking if deployment bucket '\(bucket)' exists...") + } + + do { + try await s3Client.headBucket(HeadBucketRequest(bucket: bucket)) + if verbose { + print("[verbose] Deployment bucket '\(bucket)' exists") + } + } catch let error as S3ErrorType where error.context?.responseCode == .notFound { + // Bucket does not exist, create it + try await createBucket(bucket: bucket, region: region, using: s3Client, verbose: verbose) + } catch let error as AWSResponseError where error.context?.responseCode == .notFound { + // Bucket does not exist (fallback for unrecognized error codes) + try await createBucket(bucket: bucket, region: region, using: s3Client, verbose: verbose) + } catch let error as AWSRawError where error.context.responseCode == .notFound { + // Bucket does not exist (fallback for HEAD responses with no body) + try await createBucket(bucket: bucket, region: region, using: s3Client, verbose: verbose) + } + } + + /// Creates an S3 bucket. Includes `LocationConstraint` when the region is not `us-east-1`. + private func createBucket(bucket: String, region: Region, using s3Client: S3Client, verbose: Bool) async throws { + if verbose { + print("[verbose] Creating deployment bucket '\(bucket)' in region '\(region.rawValue)'...") + } + + let request: CreateBucketRequest + if region == .useast1 { + request = CreateBucketRequest(bucket: bucket) + } else { + let locationConstraint = CreateBucketConfiguration(locationConstraint: region.rawValue) + request = CreateBucketRequest(bucket: bucket, createBucketConfiguration: locationConstraint) + } + + do { + try await s3Client.createBucket(request) + if verbose { + print("[verbose] Deployment bucket '\(bucket)' created successfully") + } + } catch let error as S3ErrorType { + throw DeployerErrors.awsAPIError( + service: "S3", + operation: "CreateBucket", + message: error.message ?? error.errorCode + ) + } + } + + /// Uploads a ZIP archive to S3 for deployment staging. + /// - Parameters: + /// - bucket: The bucket to upload to. + /// - key: The object key. + /// - data: The ZIP archive data. + /// - s3Client: The S3 client to use. + /// - verbose: Whether to emit verbose progress output. + func uploadToS3(bucket: String, key: String, data: Data, using s3Client: S3Client, verbose: Bool) async throws { + if verbose { + let sizeMB = Double(data.count) / (1024 * 1024) + print("[verbose] Uploading archive to s3://\(bucket)/\(key) (\(String(format: "%.1f", sizeMB)) MB)...") + } + + let request = PutObjectRequest(bucket: bucket, key: key, body: AWSHTTPBody(bytes: data)) + + do { + try await s3Client.putObject(request) + if verbose { + print("[verbose] Upload to S3 completed successfully") + } + } catch let error as S3ErrorType { + throw DeployerErrors.awsAPIError( + service: "S3", + operation: "PutObject", + message: error.message ?? error.errorCode + ) + } + } + + /// Deletes a staged S3 object after deployment completes. + /// The bucket is retained for reuse by future deployments. + /// - Parameters: + /// - bucket: The bucket containing the object. + /// - key: The object key to delete. + /// - s3Client: The S3 client to use. + /// - verbose: Whether to emit verbose progress output. + func deleteFromS3(bucket: String, key: String, using s3Client: S3Client, verbose: Bool) async throws { + if verbose { + print("[verbose] Cleaning up staged object s3://\(bucket)/\(key)...") + } + + let request = DeleteObjectRequest(bucket: bucket, key: key) + + do { + try await s3Client.deleteObject(request) + if verbose { + print("[verbose] Staged object deleted successfully") + } + } catch let error as S3ErrorType { + throw DeployerErrors.awsAPIError( + service: "S3", + operation: "DeleteObject", + message: error.message ?? error.errorCode + ) + } + } + + // MARK: - Function Orchestration + + /// Maps the deployer architecture enum to the Lambda API architecture enum. + private static func lambdaArchitecture( + from architecture: DeployerConfiguration.Architecture + ) -> LambdaArchitecture { + switch architecture { + case .x64: return .x86_64 + case .arm64: return .arm64 + } + } + + /// Determines the upload strategy based on archive size. + /// - Parameter archiveSize: The size of the ZIP archive in bytes. + /// - Returns: `true` if the archive should be uploaded directly (base64), `false` if S3 staging is required. + static func shouldUploadDirectly(archiveSize: Int64) -> Bool { + archiveSize <= directUploadLimit + } + + /// Creates a new Lambda function with the `provided.al2023` runtime. + /// + /// The function code is provided either as a base64-encoded ZIP payload (direct upload) + /// or as an S3 bucket/key reference (for archives exceeding the direct upload limit). + /// + /// - Parameters: + /// - name: The Lambda function name. + /// - architecture: The target architecture (x64 or arm64). + /// - role: The IAM role ARN for the function's execution role. + /// - zipData: The ZIP archive data for direct upload (mutually exclusive with bucket/key). + /// - bucket: The S3 bucket containing the deployment package (mutually exclusive with zipData). + /// - key: The S3 key of the deployment package (mutually exclusive with zipData). + /// - lambdaClient: The Lambda client to use for the API call. + /// - verbose: Whether to emit verbose progress output. + /// - Returns: The response from the CreateFunction API including the function ARN. + @discardableResult + func createFunction( + name: String, + architecture: DeployerConfiguration.Architecture, + role: String, + zipData: Data? = nil, + bucket: String? = nil, + key: String? = nil, + using lambdaClient: LambdaClient, + verbose: Bool + ) async throws -> CreateFunctionResponse { + if verbose { + if zipData != nil { + let sizeMB = Double(zipData!.count) / (1024 * 1024) + print("[verbose] Creating Lambda function '\(name)' with direct upload (\(String(format: "%.1f", sizeMB)) MB)...") + } else { + print("[verbose] Creating Lambda function '\(name)' with S3 reference s3://\(bucket ?? "")/\(key ?? "")...") + } + } + + // Build the function code — either direct ZIP or S3 reference + let code: FunctionCode + if let zipData { + code = FunctionCode(zipFile: zipData.base64EncodedString()) + } else { + code = FunctionCode(s3Bucket: bucket, s3Key: key) + } + + let request = CreateFunctionRequest( + functionName: name, + role: role, + runtime: .providedAl2023, + handler: "bootstrap", + code: code, + architectures: [Self.lambdaArchitecture(from: architecture)], + packageType: .zip + ) + + do { + let response = try await lambdaClient.createFunction(request) + if verbose { + print("[verbose] Lambda function '\(name)' created successfully") + if let arn = response.functionArn { + print("[verbose] Function ARN: \(arn)") + } + } + return response + } catch let error as LambdaErrorType { + throw DeployerErrors.awsAPIError( + service: "Lambda", + operation: "CreateFunction", + message: error.message ?? error.errorCode + ) + } + } + + /// Updates an existing Lambda function's code. + /// + /// The function code is provided either as a base64-encoded ZIP payload (direct upload) + /// or as an S3 bucket/key reference (for archives exceeding the direct upload limit). + /// + /// - Parameters: + /// - name: The Lambda function name. + /// - zipData: The ZIP archive data for direct upload (mutually exclusive with bucket/key). + /// - bucket: The S3 bucket containing the deployment package (mutually exclusive with zipData). + /// - key: The S3 key of the deployment package (mutually exclusive with zipData). + /// - lambdaClient: The Lambda client to use for the API call. + /// - verbose: Whether to emit verbose progress output. + /// - Returns: The response from the UpdateFunctionCode API. + @discardableResult + func updateFunctionCode( + name: String, + zipData: Data? = nil, + bucket: String? = nil, + key: String? = nil, + using lambdaClient: LambdaClient, + verbose: Bool + ) async throws -> UpdateFunctionCodeResponse { + if verbose { + if zipData != nil { + let sizeMB = Double(zipData!.count) / (1024 * 1024) + print("[verbose] Updating function code for '\(name)' with direct upload (\(String(format: "%.1f", sizeMB)) MB)...") + } else { + print("[verbose] Updating function code for '\(name)' with S3 reference s3://\(bucket ?? "")/\(key ?? "")...") + } + } + + let request: UpdateFunctionCodeRequest + if let zipData { + request = UpdateFunctionCodeRequest( + functionName: name, + zipFile: zipData.base64EncodedString() + ) + } else { + request = UpdateFunctionCodeRequest( + functionName: name, + s3Bucket: bucket, + s3Key: key + ) + } + + do { + let response = try await lambdaClient.updateFunctionCode(request) + if verbose { + print("[verbose] Function code for '\(name)' updated successfully") + if let arn = response.functionArn { + print("[verbose] Function ARN: \(arn)") + } + } + return response + } catch let error as LambdaErrorType { + throw DeployerErrors.awsAPIError( + service: "Lambda", + operation: "UpdateFunctionCode", + message: error.message ?? error.errorCode + ) + } + } + + /// Deletes a Lambda function and its associated IAM role. + /// + /// This first deletes the Lambda function using the DeleteFunction API, + /// then cleans up the IAM role and its attached policies. + /// + /// - Parameters: + /// - name: The Lambda function name. + /// - lambdaClient: The Lambda client to use for the API call. + /// - iamClient: The IAM client to use for role cleanup. + /// - verbose: Whether to emit verbose progress output. + func deleteFunction( + name: String, + using lambdaClient: LambdaClient, + iamClient: IAMClient, + verbose: Bool + ) async throws { + if verbose { + print("[verbose] Deleting Lambda function '\(name)'...") + } + + // Delete the function URL config first (ignore errors if not configured) + do { + try await lambdaClient.deleteFunctionUrlConfig( + DeleteFunctionUrlConfigRequest(functionName: name) + ) + if verbose { + print("[verbose] Deleted Function URL configuration for '\(name)'") + } + } catch { + if verbose { + print("[verbose] No Function URL to delete (or already deleted)") + } + } + + // Delete the Lambda function + let request = DeleteFunctionRequest(functionName: name) + do { + try await lambdaClient.deleteFunction(request) + if verbose { + print("[verbose] Lambda function '\(name)' deleted successfully") + } + } catch let error as LambdaErrorType { + throw DeployerErrors.awsAPIError( + service: "Lambda", + operation: "DeleteFunction", + message: error.message ?? error.errorCode + ) + } + + print("Deleted Lambda function '\(name)'") + + // Delete the associated IAM role and its policies + try await deleteIAMRole(functionName: name, using: iamClient, verbose: verbose) + } + + // MARK: - Function URL + + /// Configures a Function URL for the Lambda function with IAM authentication + /// and adds a resource-based permission allowing Function URL invocation. + /// + /// - Parameters: + /// - functionName: The Lambda function name. + /// - lambdaClient: The Lambda client to use for API calls. + /// - verbose: Whether to emit verbose progress output. + /// - Returns: The Function URL string (HTTPS endpoint). + @discardableResult + func setupFunctionURL( + functionName: String, + accountId: String, + using lambdaClient: LambdaClient, + verbose: Bool + ) async throws -> String { + if verbose { + print("[verbose] Creating Function URL for '\(functionName)' with AWS_IAM auth type...") + } + + // Create the Function URL configuration with IAM authentication + let createUrlRequest = CreateFunctionUrlConfigRequest( + functionName: functionName, + authType: .awsIam + ) + + let createUrlResponse: CreateFunctionUrlConfigResponse + do { + createUrlResponse = try await lambdaClient.createFunctionUrlConfig(createUrlRequest) + } catch let error as LambdaErrorType { + throw DeployerErrors.functionURLCreationFailed( + "CreateFunctionUrlConfig failed: \(error.message ?? error.errorCode)" + ) + } + + guard let functionUrl = createUrlResponse.functionUrl else { + throw DeployerErrors.functionURLCreationFailed( + "CreateFunctionUrlConfig succeeded but no URL was returned" + ) + } + + if verbose { + print("[verbose] Function URL created: \(functionUrl)") + } + + // Add resource-based permission for Function URL invocation + // Scoped to the account to avoid overly-permissive resource policy + let addPermissionRequest = AddPermissionRequest( + functionName: functionName, + statementId: "FunctionURLAllowAccountAccess", + action: "lambda:InvokeFunctionUrl", + principal: accountId, + functionUrlAuthType: .awsIam + ) + + do { + try await lambdaClient.addPermission(addPermissionRequest) + if verbose { + print("[verbose] Added resource-based permission for Function URL invocation") + } + } catch let error as LambdaErrorType { + throw DeployerErrors.functionURLCreationFailed( + "AddPermission failed: \(error.message ?? error.errorCode)" + ) + } + + return functionUrl + } + + // MARK: - Source Code Detection + + /// Scans the Sources directory for usage of `FunctionURLRequest`, indicating + /// the project was scaffolded with `lambda-init --with-url` and needs a Function URL. + /// This allows `lambda-deploy` to auto-detect the need for `--with-url`. + private func detectFunctionURLUsage() -> Bool { + let sourcesDir = URL(fileURLWithPath: "Sources") + guard let enumerator = FileManager.default.enumerator( + at: sourcesDir, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) else { + return false + } + + for case let fileURL as URL in enumerator { + guard fileURL.pathExtension == "swift" else { continue } + guard let contents = try? String(contentsOf: fileURL, encoding: .utf8) else { continue } + if contents.contains("FunctionURLRequest") || contents.contains("FunctionURLResponse") { + return true + } + } + return false + } + + // MARK: - Deploy + func deploy(arguments: [String]) async throws { let configuration = try DeployerConfiguration(arguments: arguments) @@ -28,49 +539,717 @@ struct Deployer { return } - //FIXME: use Logger - print("TODO: deploy") + if configuration.verboseLogging { + print("-------------------------------------------------------------------------") + print("configuration") + print("-------------------------------------------------------------------------") + print(configuration) + } + + // Check for AWS configuration files and emit non-blocking warning if absent + self.checkAWSConfigurationFiles(verbose: configuration.verboseLogging) + + // Initialize AWSClient with the default credential provider chain. + // Use a verbose logger when --verbose is set so SotoCore reports which + // credential provider is attempted and why it fails. + let clientLogger: Logger = { + var logger = Logger(label: "AWSLambdaDeployer") + logger.logLevel = configuration.verboseLogging ? .debug : .info + return logger + }() + + let awsClient = AWSClient( + credentialProvider: .default, + logger: clientLogger + ) + + do { + // Resolve the AWS region: use --region override, or fall through to environment/config resolution + let region: Region + if let regionOverride = configuration.region { + region = Region(rawValue: regionOverride) + if configuration.verboseLogging { + print("[verbose] Using region override: \(regionOverride)") + } + } else { + // Resolve region from environment variables (AWS_REGION or AWS_DEFAULT_REGION) + if let envRegion = ProcessInfo.processInfo.environment["AWS_REGION"] + ?? ProcessInfo.processInfo.environment["AWS_DEFAULT_REGION"] + { + region = Region(rawValue: envRegion) + if configuration.verboseLogging { + print("[verbose] Using region from environment: \(envRegion)") + } + } else { + // Default to us-east-1 if no region can be resolved + region = .useast1 + if configuration.verboseLogging { + print("[verbose] No region specified or found in environment, defaulting to us-east-1") + } + } + } + + // Verify credentials can be resolved by attempting to get them + do { + _ = try await awsClient.getCredential() + if configuration.verboseLogging { + print("[verbose] AWS credentials resolved successfully") + } + } catch { + throw DeployerErrors.credentialResolutionFailed( + "Unable to resolve AWS credentials. " + + "If your session has expired, run 'aws sso login' or 'aws login' to refresh it. " + + "Otherwise, ensure credentials are configured via " + + "environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY), " + + "~/.aws/credentials file, ECS/EKS container credentials, or EC2 instance metadata. " + + "Error: \(error)" + ) + } + + // Initialize service clients + let stsClient = STSClient(client: awsClient, region: region) + let lambdaClient = LambdaClient(client: awsClient, region: region) + let iamClient = IAMClient(client: awsClient, region: region) + let s3Client = S3Client(client: awsClient, region: region) + + // Resolve account ID via STS + print("Resolving AWS account ID...") + let accountId = try await resolveAccountId(using: stsClient) + if configuration.verboseLogging { + print("[verbose] AWS account ID: \(accountId)") + } + + // Determine function name from products + guard let functionName = configuration.products.first else { + throw DeployerErrors.missingProduct + } + if configuration.delete { + print("Deleting function '\(functionName)' from \(region.rawValue)...") + } else { + print("Deploying function '\(functionName)' to \(region.rawValue)...") + } + + // Check if function already exists + print("Checking if function '\(functionName)' exists...") + let exists = try await functionExists(functionName, using: lambdaClient) + let action = try determineDeploymentAction(functionExists: exists, delete: configuration.delete) + + if configuration.verboseLogging { + print("[verbose] Function '\(functionName)' exists: \(exists), action: \(action)") + } + + switch action { + case .delete: + print("Deleting function '\(functionName)'...") + try await deleteFunction( + name: functionName, + using: lambdaClient, + iamClient: iamClient, + verbose: configuration.verboseLogging + ) + print("🗑️ Function '\(functionName)' deleted successfully.") + + case .create, .update: + // Resolve the ZIP archive path + let archiveURL: URL + if let inputDir = configuration.inputDirectory { + archiveURL = inputDir.appendingPathComponent("\(functionName)/\(functionName).zip") + } else { + // Default build output path. + // Check both the current Builder plugin path and the legacy Packager plugin path. + // The legacy AWSLambdaPackager path can be removed when the archive plugin is retired. + let builderPath = URL(fileURLWithPath: ".build/plugins/AWSLambdaBuilder/outputs/AWSLambdaPackager/\(functionName)/\(functionName).zip") + let packagerPath = URL(fileURLWithPath: ".build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/\(functionName)/\(functionName).zip") + + if FileManager.default.fileExists(atPath: builderPath.path) { + archiveURL = builderPath + } else { + // Fallback to legacy packager path (used by `swift package archive`) + // TODO: remove this fallback when the AWSLambdaPackager plugin is retired + archiveURL = packagerPath + } + } + + guard FileManager.default.fileExists(atPath: archiveURL.path) else { + throw DeployerErrors.archiveNotFound(archiveURL) + } + + let zipData = try Data(contentsOf: archiveURL) + let archiveSize = Int64(zipData.count) + + if configuration.verboseLogging { + let sizeMB = Double(archiveSize) / (1024 * 1024) + print("[verbose] Archive: \(archiveURL.path) (\(String(format: "%.1f", sizeMB)) MB)") + print("[verbose] Upload strategy: \(Self.shouldUploadDirectly(archiveSize: archiveSize) ? "direct" : "S3 staging")") + } + + // Determine upload strategy + var s3Bucket: String? = nil + var s3Key: String? = nil + + if !Self.shouldUploadDirectly(archiveSize: archiveSize) { + // Stage to S3 + print("Archive exceeds 50 MB, staging to S3...") + let bucketName = Self.deploymentBucketName(region: region.rawValue, accountId: accountId) + s3Key = "\(functionName)/\(functionName).zip" + try await ensureBucketExists(bucket: bucketName, region: region, using: s3Client, verbose: configuration.verboseLogging) + try await uploadToS3(bucket: bucketName, key: s3Key!, data: zipData, using: s3Client, verbose: configuration.verboseLogging) + s3Bucket = bucketName + } + + let functionArn: String? + + if action == .create { + // Resolve IAM role + print("Resolving IAM role...") + let roleArn = try await resolveIAMRole( + functionName: functionName, + iamRole: configuration.iamRole, + using: iamClient, + verbose: configuration.verboseLogging + ) + + // Create the function + print("Creating Lambda function '\(functionName)'...") + let response: CreateFunctionResponse + if let bucket = s3Bucket, let key = s3Key { + response = try await createFunction( + name: functionName, + architecture: configuration.architecture, + role: roleArn, + bucket: bucket, + key: key, + using: lambdaClient, + verbose: configuration.verboseLogging + ) + } else { + response = try await createFunction( + name: functionName, + architecture: configuration.architecture, + role: roleArn, + zipData: zipData, + using: lambdaClient, + verbose: configuration.verboseLogging + ) + } + functionArn = response.functionArn + } else { + // Update the function code + print("Updating Lambda function '\(functionName)'...") + let response: UpdateFunctionCodeResponse + if let bucket = s3Bucket, let key = s3Key { + response = try await updateFunctionCode( + name: functionName, + bucket: bucket, + key: key, + using: lambdaClient, + verbose: configuration.verboseLogging + ) + } else { + response = try await updateFunctionCode( + name: functionName, + zipData: zipData, + using: lambdaClient, + verbose: configuration.verboseLogging + ) + } + functionArn = response.functionArn + } + + // Clean up S3 staged object + if let bucket = s3Bucket, let key = s3Key { + try await deleteFromS3(bucket: bucket, key: key, using: s3Client, verbose: configuration.verboseLogging) + } + + // Set up Function URL if requested (or auto-detected from source code) + var functionURL: String? = nil + let shouldSetupURL = configuration.withURL || detectFunctionURLUsage() + if shouldSetupURL { + if action == .create { + print("Configuring Function URL...") + functionURL = try await setupFunctionURL( + functionName: functionName, + accountId: accountId, + using: lambdaClient, + verbose: configuration.verboseLogging + ) + } else { + // On update, retrieve the existing Function URL + do { + let urlConfig = try await lambdaClient.getFunctionUrlConfig( + GetFunctionUrlConfigRequest(functionName: functionName) + ) + functionURL = urlConfig.functionUrl + } catch { + // No URL configured yet — set it up + print("Configuring Function URL...") + functionURL = try await setupFunctionURL( + functionName: functionName, + accountId: accountId, + using: lambdaClient, + verbose: configuration.verboseLogging + ) + } + } + } + + // Report success + reportDeploymentSuccess( + functionName: functionName, + functionArn: functionArn ?? "arn:aws:lambda:\(region.rawValue):\(accountId):function:\(functionName)", + region: region.rawValue, + functionURL: functionURL + ) + } + + try await awsClient.shutdown() + } catch { + try await awsClient.shutdown() + throw error + } + } + + /// Check for the presence of AWS configuration files and emit an informational + /// warning if they are absent. This is non-blocking — deployment continues + /// regardless, because credentials may be available from other sources in the + /// credential provider chain (environment variables, ECS/EKS, EC2 IMDS). + private func checkAWSConfigurationFiles(verbose: Bool) { + let homeDirectory: String + #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) + homeDirectory = NSHomeDirectory() + #else + homeDirectory = ProcessInfo.processInfo.environment["HOME"] ?? "~" + #endif + + let configPath = "\(homeDirectory)/.aws/config" + let credentialsPath = "\(homeDirectory)/.aws/credentials" + + let configExists = FileManager.default.fileExists(atPath: configPath) + let credentialsExists = FileManager.default.fileExists(atPath: credentialsPath) + + if !configExists && !credentialsExists { + print( + """ + ⚠️ AWS configuration files not found (~/.aws/config and ~/.aws/credentials). + If you are running on a developer machine, install the AWS CLI and run + 'aws configure' to set up your credentials and default region. + On EC2, ECS, or EKS, credentials are typically provided automatically + by the instance or task role. + """ + ) + } else if verbose { + print("[verbose] AWS configuration files found:") + if configExists { print(" - \(configPath)") } + if credentialsExists { print(" - \(credentialsPath)") } + } + } + + // MARK: - IAM Role Management + + /// The ARN of the AWS managed policy for basic Lambda execution (CloudWatch Logs access). + private static let lambdaBasicExecutionRolePolicyARN = + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + + /// Constructs the IAM role name for a Lambda function. + /// Format: `swift-lambda--role` + static func iamRoleName(for functionName: String) -> String { + "swift-lambda-\(functionName)-role" + } + + /// The trust policy document that allows Lambda to assume the role. + private static let lambdaTrustPolicy = """ + {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]} + """ + + /// Creates a new IAM role for the Lambda function with the Lambda trust policy, + /// attaches the AWSLambdaBasicExecutionRole managed policy, and waits for + /// role propagation before returning. + /// + /// - Parameters: + /// - functionName: The Lambda function name used to derive the role name. + /// - iamClient: The IAM client to use for API calls. + /// - verbose: Whether to emit verbose progress output. + /// - Returns: The ARN of the created role. + @discardableResult + func createIAMRole(functionName: String, using iamClient: IAMClient, verbose: Bool) async throws -> String { + let roleName = Self.iamRoleName(for: functionName) + + if verbose { + print("[verbose] Creating IAM role '\(roleName)' with Lambda trust policy...") + } + + // Create the role with the Lambda assume-role trust policy + let createRoleRequest = IAMCreateRoleRequest( + roleName: roleName, + assumeRolePolicyDocument: Self.lambdaTrustPolicy, + path: "/", + description: "Execution role for Lambda function '\(functionName)' created by swift-aws-lambda-runtime deploy plugin" + ) + + let createRoleResponse: IAMCreateRoleResponse + do { + createRoleResponse = try await iamClient.createRole(createRoleRequest) + } catch { + throw DeployerErrors.iamRoleCreationFailed( + "CreateRole failed for '\(roleName)': \(error)" + ) + } + + guard let roleARN = createRoleResponse.role?.arn else { + throw DeployerErrors.iamRoleCreationFailed( + "CreateRole succeeded but no ARN was returned for '\(roleName)'" + ) + } + + if verbose { + print("[verbose] IAM role created: \(roleARN)") + } + + // Attach the AWSLambdaBasicExecutionRole managed policy + let attachPolicyRequest = IAMAttachRolePolicyRequest( + roleName: roleName, + policyArn: Self.lambdaBasicExecutionRolePolicyARN + ) + + do { + try await iamClient.attachRolePolicy(attachPolicyRequest) + } catch { + throw DeployerErrors.iamRoleCreationFailed( + "AttachRolePolicy failed for '\(roleName)': \(error)" + ) + } + + if verbose { + print("[verbose] Attached AWSLambdaBasicExecutionRole policy to '\(roleName)'") + } + + // Wait for role propagation — IAM is eventually consistent and the role + // may not be usable by Lambda immediately after creation. + if verbose { + print("[verbose] Waiting 10 seconds for IAM role propagation...") + } + try await Task.sleep(for: .seconds(10)) + + if verbose { + print("[verbose] IAM role '\(roleName)' is ready") + } + + return roleARN + } + + /// Deletes the IAM role associated with a Lambda function, including + /// detaching managed policies and deleting inline policies. + /// + /// - Parameters: + /// - functionName: The Lambda function name used to derive the role name. + /// - iamClient: The IAM client to use for API calls. + /// - verbose: Whether to emit verbose progress output. + func deleteIAMRole(functionName: String, using iamClient: IAMClient, verbose: Bool) async throws { + let roleName = Self.iamRoleName(for: functionName) + + if verbose { + print("[verbose] Deleting IAM role '\(roleName)'...") + } + + // Detach the AWSLambdaBasicExecutionRole managed policy + let detachPolicyRequest = IAMDetachRolePolicyRequest( + roleName: roleName, + policyArn: Self.lambdaBasicExecutionRolePolicyARN + ) + + do { + try await iamClient.detachRolePolicy(detachPolicyRequest) + if verbose { + print("[verbose] Detached AWSLambdaBasicExecutionRole from '\(roleName)'") + } + } catch { + // If the policy is not attached, ignore the error and continue + if verbose { + print("[verbose] Note: detaching managed policy failed (may not be attached): \(error)") + } + } + + // Delete any inline policies that may have been added + // We use a known inline policy name pattern for cleanup + let inlinePolicyName = "\(roleName)-inline-policy" + do { + let deleteInlinePolicyRequest = IAMDeleteRolePolicyRequest( + roleName: roleName, + policyName: inlinePolicyName + ) + try await iamClient.deleteRolePolicy(deleteInlinePolicyRequest) + if verbose { + print("[verbose] Deleted inline policy '\(inlinePolicyName)' from '\(roleName)'") + } + } catch { + // Inline policy may not exist, which is fine + if verbose { + print("[verbose] Note: deleting inline policy failed (may not exist): \(error)") + } + } + + // Delete the role itself + let deleteRoleRequest = IAMDeleteRoleRequest(roleName: roleName) + do { + try await iamClient.deleteRole(deleteRoleRequest) + if verbose { + print("[verbose] IAM role '\(roleName)' deleted successfully") + } + } catch { + throw DeployerErrors.awsAPIError( + service: "IAM", + operation: "DeleteRole", + message: "Failed to delete role '\(roleName)': \(error)" + ) + } + + print("Deleted IAM role '\(roleName)'") + } + + /// Resolves the IAM role for a Lambda function deployment. + /// + /// If an IAM role ARN is provided via `--iam-role`, it is returned directly. + /// Otherwise, a new role is created with the Lambda trust policy and the + /// AWSLambdaBasicExecutionRole managed policy attached. + /// + /// - Parameters: + /// - functionName: The Lambda function name. + /// - iamRole: An optional user-specified IAM role ARN. + /// - iamClient: The IAM client to use for API calls. + /// - verbose: Whether to emit verbose progress output. + /// - Returns: The IAM role ARN to use for the Lambda function. + func resolveIAMRole( + functionName: String, + iamRole: String?, + using iamClient: IAMClient, + verbose: Bool + ) async throws -> String { + // If the user specified an IAM role, use it directly + if let iamRole { + if verbose { + print("[verbose] Using user-specified IAM role: \(iamRole)") + } + return iamRole + } + + // Check if the role already exists + let roleName = Self.iamRoleName(for: functionName) + do { + let getRoleResponse = try await iamClient.getRole( + IAMGetRoleRequest(roleName: roleName) + ) + if let existingARN = getRoleResponse.role?.arn { + if verbose { + print("[verbose] Found existing IAM role: \(existingARN)") + } + return existingARN + } + } catch { + // Role does not exist — we will create it + if verbose { + print("[verbose] IAM role '\(roleName)' not found, creating a new one...") + } + } + + // Create a new role + return try await createIAMRole(functionName: functionName, using: iamClient, verbose: verbose) + } + + // MARK: - Success Reporting + + /// Reports a successful deployment to the developer, including the function ARN, + /// deployment region, and a ready-to-use invocation command. + /// + /// - Parameters: + /// - functionName: The deployed Lambda function name. + /// - functionArn: The ARN of the deployed Lambda function. + /// - region: The AWS region where the function was deployed. + /// - functionURL: The Function URL if `--with-url` was used, or `nil` otherwise. + func reportDeploymentSuccess( + functionName: String, + functionArn: String, + region: String, + functionURL: String? + ) { + print("") + print("🚀 Deployment complete!") + print(" Function ARN: \(functionArn)") + print(" Region: \(region)") + + if let functionURL { + print(" Function URL: \(functionURL)") + print("") + print("Invoke your function with:") + print("") + print(" (eval $(aws configure export-credentials --format env) && curl --aws-sigv4 \"aws:amz:\(region):lambda\" --user \"$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY\" -H \"x-amz-security-token: $AWS_SESSION_TOKEN\" \"\(functionURL)?name=World\" )") + } else { + print("") + print("Invoke your function with:") + print( + #" aws lambda invoke --function-name \#(functionName) --region \#(region) --payload $(echo '{"name":"World","age":30}' | base64) /tmp/out.json > /dev/null && cat /tmp/out.json"# + ) + } + print("") } private func displayHelpMessage() { print( """ - OVERVIEW: A SwiftPM plugin to deploy a Lambda function. + OVERVIEW: A SwiftPM plugin to deploy a Lambda function to AWS. - USAGE: swift package lambda-deploy - [--with-url] + USAGE: swift package --allow-network-connections all:443 lambda-deploy [--help] [--verbose] + [--with-url] + [--delete] + [--region ] + [--iam-role ] + [--input-directory ] + [--architecture ] + [--products ] OPTIONS: - --with-url Add an URL to access the Lambda function - --verbose Produce verbose output for debugging. - --help Show help information. + --verbose Produce verbose output for debugging. + --with-url Create a Function URL for the Lambda function. + The URL uses AWS_IAM authentication, restricted to + authenticated principals in your AWS account. + --delete Delete the Lambda function, its IAM role, and + Function URL (if any). + --region The AWS region to deploy to. + (default: resolved from AWS configuration) + --iam-role The ARN of an existing IAM role for the Lambda function. + (default: create a new role) + --input-directory The path to the directory containing the deployment + ZIP archive produced by lambda-build. + (default: .build/plugins/AWSLambdaBuilder/outputs/...) + --architecture The Lambda function architecture (x64 or arm64). + (default: host architecture - \(DeployerConfiguration.Architecture.host.rawValue)) + --products The list of executable targets to deploy. + (default is taken from Package.swift) + --help Show help information. """ ) } } -private struct DeployerConfiguration: CustomStringConvertible { - public let help: Bool - public let verboseLogging: Bool +@available(LambdaSwift 2.0, *) +struct DeployerConfiguration: CustomStringConvertible { + let help: Bool + let verboseLogging: Bool + let withURL: Bool + let delete: Bool + let region: String? + let iamRole: String? + let inputDirectory: URL? + let architecture: Architecture + let products: [String] + + enum Architecture: String { + case x64 + case arm64 + + static var host: Architecture { + #if arch(x86_64) + return .x64 + #else + return .arm64 + #endif + } + } - public init(arguments: [String]) throws { + init(arguments: [String]) throws { var argumentExtractor = ArgumentExtractor(arguments) - let verboseArgument = argumentExtractor.extractFlag(named: "verbose") > 0 + let helpArgument = argumentExtractor.extractFlag(named: "help") > 0 + let verboseArgument = argumentExtractor.extractFlag(named: "verbose") > 0 + let withURLArgument = argumentExtractor.extractFlag(named: "with-url") > 0 + let deleteArgument = argumentExtractor.extractFlag(named: "delete") > 0 + let regionArgument = argumentExtractor.extractOption(named: "region") + let iamRoleArgument = argumentExtractor.extractOption(named: "iam-role") + let inputDirectoryArgument = argumentExtractor.extractOption(named: "input-directory") + let architectureArgument = argumentExtractor.extractOption(named: "architecture") + let productsArgument = argumentExtractor.extractOption(named: "products") - // help required ? + // help required? self.help = helpArgument - // verbose logging required ? + // verbose logging required? self.verboseLogging = verboseArgument + + // create a Function URL? + self.withURL = withURLArgument + + // delete the function? + self.delete = deleteArgument + + // AWS region (nil means Soto resolves it) + self.region = regionArgument.first + + // IAM role ARN (nil means create a new role) + self.iamRole = iamRoleArgument.first + + // input directory for the ZIP archive + if let inputDir = inputDirectoryArgument.first { + self.inputDirectory = URL(fileURLWithPath: inputDir) + } else { + self.inputDirectory = nil + } + + // architecture + if let archString = architectureArgument.first { + guard let arch = Architecture(rawValue: archString) else { + throw DeployerErrors.invalidArchitecture(archString) + } + self.architecture = arch + } else { + self.architecture = .host + } + + // products + self.products = productsArgument.flatMap { $0.split(separator: ",").map(String.init) } } var description: String { """ { verboseLogging: \(self.verboseLogging) + withURL: \(self.withURL) + delete: \(self.delete) + region: \(self.region ?? "") + iamRole: \(self.iamRole ?? "") + inputDirectory: \(self.inputDirectory?.path() ?? "") + architecture: \(self.architecture.rawValue) + products: \(self.products) } """ } } + +@available(LambdaSwift 2.0, *) +enum DeployerErrors: Error, CustomStringConvertible { + case invalidArchitecture(String) + case credentialResolutionFailed(String) + case awsAPIError(service: String, operation: String, message: String) + case archiveNotFound(URL) + case functionURLCreationFailed(String) + case iamRoleCreationFailed(String) + case missingProduct + + var description: String { + switch self { + case .invalidArchitecture(let value): + return "invalid architecture '\(value)'. Use 'x64' or 'arm64'." + case .credentialResolutionFailed(let message): + return "AWS credential resolution failed: \(message)" + case .awsAPIError(let service, let operation, let message): + return "AWS \(service) \(operation) error: \(message)" + case .archiveNotFound(let url): + return "deployment archive not found at '\(url.path())'" + case .functionURLCreationFailed(let message): + return "failed to create Function URL: \(message)" + case .iamRoleCreationFailed(let message): + return "failed to create IAM role: \(message)" + case .missingProduct: + return "no product specified. Use --products or define an executable target in Package.swift." + } + } +} diff --git a/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift b/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift index 83ec14b09..4bda99c08 100644 --- a/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift @@ -18,10 +18,9 @@ import FoundationEssentials import Foundation #endif +@available(LambdaSwift 2.0, *) struct Initializer { - private let destFileName = "Sources/main.swift" - func initialize(arguments: [String]) async throws { let configuration = try InitializerConfiguration(arguments: arguments) @@ -31,23 +30,118 @@ struct Initializer { return } - let destFileURL = configuration.destinationDir.appendingPathComponent(destFileName) - do { + // Find the main entry point file in the Sources directory + let sourcesDir = configuration.destinationDir.appendingPathComponent("Sources") + let entryPoint = try findEntryPoint(in: sourcesDir) + + // Back up the original file + let backupURL = entryPoint.appendingPathExtension("bak") + if FileManager.default.fileExists(atPath: entryPoint.path) { + try? FileManager.default.copyItem(at: entryPoint, to: backupURL) + if configuration.verboseLogging { + print("Backed up original file to: \(backupURL.path)") + } + } + // Overwrite with the Lambda template + do { let template = TemplateType.template(for: configuration.templateType) - try template.write(to: destFileURL, atomically: true, encoding: .utf8) + try template.write(to: entryPoint, atomically: true, encoding: .utf8) if configuration.verboseLogging { - print("File created at: \(destFileURL)") + print("File written at: \(entryPoint.path)") } - print("✅ Lambda function written to \(destFileName)") + let relativePath = entryPoint.path.replacingOccurrences( + of: configuration.destinationDir.path + "/", + with: "" + ) + print("✅ Lambda function written to \(relativePath)") print("📦 You can now package with: 'swift package lambda-build'") } catch { - print("🛑Failed to create the Lambda function file: \(error)") + print("🛑 Failed to create the Lambda function file: \(error)") } } + /// Finds the main entry point Swift file in the Sources directory. + /// + /// Strategy: + /// 1. Look for a file containing `@main` or a `main.swift` + /// 2. If Sources has a single subdirectory, look for `.swift` in it + /// 3. Fall back to `Sources/main.swift` + private func findEntryPoint(in sourcesDir: URL) throws -> URL { + guard FileManager.default.fileExists(atPath: sourcesDir.path) else { + // No Sources directory yet — use the classic path + return sourcesDir.appendingPathComponent("main.swift") + } + + // List immediate children of Sources/ + let contents = try FileManager.default.contentsOfDirectory( + at: sourcesDir, + includingPropertiesForKeys: [.isDirectoryKey], + options: [.skipsHiddenFiles] + ) + + // Find subdirectories (typical Swift package layout: Sources//) + let subdirs = contents.filter { url in + (try? url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) == true + } + + // If there's exactly one subdirectory, look inside it + if let targetDir = subdirs.first, subdirs.count == 1 { + let targetName = targetDir.lastPathComponent + + // Check for main.swift first + let mainSwift = targetDir.appendingPathComponent("main.swift") + if FileManager.default.fileExists(atPath: mainSwift.path) { + return mainSwift + } + + // Check for .swift (what `swift package init --type executable` creates) + let namedFile = targetDir.appendingPathComponent("\(targetName).swift") + if FileManager.default.fileExists(atPath: namedFile.path) { + return namedFile + } + + // Look for any .swift file containing @main + let swiftFiles = try FileManager.default.contentsOfDirectory( + at: targetDir, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ).filter { $0.pathExtension == "swift" } + + for file in swiftFiles { + if let content = try? String(contentsOf: file, encoding: .utf8), + content.contains("@main") + { + return file + } + } + + // No match found — default to .swift (will be created) + return namedFile + } + + // No subdirectory or multiple subdirectories — check for main.swift directly in Sources/ + let mainSwift = sourcesDir.appendingPathComponent("main.swift") + if FileManager.default.fileExists(atPath: mainSwift.path) { + return mainSwift + } + + // Check for any .swift file in Sources/ containing @main + let topLevelSwiftFiles = contents.filter { $0.pathExtension == "swift" } + for file in topLevelSwiftFiles { + if let content = try? String(contentsOf: file, encoding: .utf8), + content.contains("@main") + { + return file + } + } + + // Fall back to Sources/main.swift + return mainSwift + } + private func displayHelpMessage() { print( """ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Deployment.md b/Sources/AWSLambdaRuntime/Docs.docc/Deployment.md index d9bbc1510..52b46faf1 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/Deployment.md +++ b/Sources/AWSLambdaRuntime/Docs.docc/Deployment.md @@ -11,9 +11,7 @@ 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. +There are multiple ways to deploy your Swift code to AWS Lambda. The simplest way is to use the `lambda-deploy` plugin that handles IAM role creation, code upload, and function management automatically. For more complex deployments, 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. @@ -22,8 +20,8 @@ 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 lambda-deploy plugin](#deploy-your-lambda-function-with-the-lambda-deploy-plugin) * [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) @@ -63,16 +61,30 @@ Here is the content of this guide: } ``` -3. A Swift Lambda function to deploy. +3. AWS CLI and credentials configuration. + + To deploy with the `lambda-deploy` plugin from your local machine, install the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and run `aws configure` to create the AWS configuration files (`~/.aws/config` and `~/.aws/credentials`). This stores your default region and credentials so the deploy plugin can access AWS. + + ```sh + aws configure + ``` + + > On EC2, ECS, or EKS, credentials are typically provided automatically by the instance or task role, so running `aws configure` is not required in those environments. - 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/awslabs/swift-aws-lambda-runtime/tree/main/Examples) directory. +4. A Swift Lambda function to deploy. - Compile and package the function using the following command + You need a Swift Lambda function to deploy. If you don't have one yet, you can scaffold one using the `lambda-init` plugin: ```sh - swift package archive \ - --allow-network-connections docker \ - --base-docker-image swift:amazonlinux2023 + swift package lambda-init --allow-writing-to-package-directory + ``` + + Or use one of the examples in the [Examples](https://github.com/awslabs/swift-aws-lambda-runtime/tree/main/Examples) directory. + + Compile and package the function using the following command: + + ```sh + swift package --allow-network-connections docker lambda-build ``` 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. @@ -101,6 +113,75 @@ A Lambda execution role is an AWS Identity and Access Management (IAM) role that 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 lambda-deploy plugin + +The `lambda-deploy` plugin provides the simplest way to deploy your Lambda function from the command line. It handles IAM role creation, code upload, and function creation or update automatically. + +In this example, we're building the HelloWorld example from the [Examples](https://github.com/awslabs/swift-aws-lambda-runtime/tree/main/Examples) directory. + +#### Prerequisites + +Ensure you have configured your AWS credentials by running `aws configure`. This creates the `~/.aws/config` and `~/.aws/credentials` files that the deploy plugin reads to authenticate with AWS. + +```sh +aws configure +``` + +#### Create or update the function + +The `lambda-deploy` plugin automatically detects whether the function exists. If the function does not exist, it creates a new one (including the IAM role). If the function already exists, it updates the code. + +The command assumes you've already built the ZIP file with `swift package lambda-build`, as described in the [Prerequisites](#prerequisites) section. + +```sh +swift package --allow-network-connections all:443 lambda-deploy +``` + +When the deployment succeeds, the plugin reports the function ARN, region, and a ready-to-use invocation command. + +#### Invoke the function + +Use the command displayed by the plugin after deployment: + +```sh +aws lambda invoke \ + --function-name MyLambda \ + --payload $(echo '{"name":"World","age":30}' | base64) \ + /dev/stdout +``` + +#### Deploy with a Function URL + +To expose the function as an HTTPS endpoint, add the `--with-url` option: + +```sh +swift package --allow-network-connections all:443 lambda-deploy --with-url +``` + +> **Security:** The Function URL uses IAM authentication (`AWS_IAM`) and the resource policy restricts access to authenticated IAM principals in your AWS account only. Unauthenticated requests and requests from other accounts are rejected. Callers must sign requests with AWS Signature Version 4. See [Lambda Function URL security and auth model](https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html) for details. + +> **Note:** Function URLs deliver a `FunctionURLRequest` and expect a `FunctionURLResponse`. Your Lambda function code must use these types (from `AWSLambdaEvents`) instead of plain JSON structs. Use `swift package lambda-init --with-url` to scaffold a function with the correct request/response pattern, or see the [Streaming+FunctionUrl](https://github.com/awslabs/swift-aws-lambda-runtime/tree/main/Examples/Streaming+FunctionUrl) example. + +The plugin reports the Function URL and a ready-to-use `curl` command. Invoke it with: + +```sh +(eval $(aws configure export-credentials --format env) && \ + curl --aws-sigv4 "aws:amz:us-east-1:lambda" \ + --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \ + -H "x-amz-security-token: $AWS_SESSION_TOKEN" \ + "https://.lambda-url..on.aws/") +``` + +> The `eval $(aws configure export-credentials --format env)` command exports your AWS credentials as environment variables from whatever credential source you have configured (SSO, config file, assumed role, etc.). + +#### Delete the function + +Remove the Lambda function and its associated IAM role: + +```sh +swift package --allow-network-connections all:443 lambda-deploy --delete +``` + ### 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`. @@ -126,7 +207,7 @@ 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` command as described in the [Prerequisites](#prerequisites) section. +Select the zip file created with the `swift package lambda-build` command as described in the [Prerequisites](#prerequisites) section. Select **Save** @@ -180,120 +261,6 @@ Select the `HelloWorld-role-xxxx` role and select **Delete**. Confirm the deleti ![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/awslabs/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` 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.al2023 \ ---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. @@ -512,7 +479,7 @@ export class LambdaApiStack extends cdk.Stack { } } ``` -The code assumes you already built and packaged the APIGateway Lambda function with the `swift package archive` command, as described in the [Prerequisites](#prerequisites) section. +The code assumes you already built and packaged the APIGateway Lambda function with the `swift package lambda-build` 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. 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 index cd0126aa4..7d5086cdd 100644 --- 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 @@ -1 +1 @@ -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build 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 index c230f61cb..fc6e56308 100644 --- 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 @@ -1,9 +1,9 @@ -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ------------------------------------------------------------------------- building "palindrome" in docker ------------------------------------------------------------------------- -updating "swift:amazonlinux2023" docker image +updating "swift:6.1-amazonlinux2023" docker image amazonlinux2023: Pulling from library/swift Digest: sha256:df06a50f70e2e87f237bd904d2fc48195742ebda9f40b4a821c4d39766434009 Status: Image is up to date for swift:amazonlinux2023 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 index a3ab15611..7e1205aa7 100644 --- 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 @@ -1,9 +1,9 @@ -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ------------------------------------------------------------------------- building "palindrome" in docker ------------------------------------------------------------------------- -updating "swift:amazonlinux2023" docker image +updating "swift:6.1-amazonlinux2023" docker image amazonlinux2023: Pulling from library/swift Digest: sha256:df06a50f70e2e87f237bd904d2fc48195742ebda9f40b4a821c4d39766434009 Status: Image is up to date for swift:amazonlinux2023 diff --git a/Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md b/Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md index a32724e1d..1b2dc069b 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md +++ b/Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md @@ -113,7 +113,7 @@ 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 archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ------------------------------------------------------------------------- building "MyFirstLambdaFunction" in docker @@ -130,27 +130,34 @@ building "MyFirstLambdaFunction" archiving "MyFirstLambdaFunction" ------------------------------------------------------------------------- 1 archive created - * MyFirstLambdaFunction at /Users/YourUserName/MyFirstLambdaFunction/.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyFirstLambdaFunction/MyFirstLambdaFunction.zip + * MyFirstLambdaFunction at /Users/YourUserName/MyFirstLambdaFunction/.build/plugins/AWSLambdaBuilder/outputs/AWSLambdaPackager/MyFirstLambdaFunction/MyFirstLambdaFunction.zip +``` +6. Deploy on AWS Lambda -cp .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyFirstLambdaFunction/MyFirstLambdaFunction.zip ~/Desktop -``` +> Be sure [to have an AWS Account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-creating.html) and the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) installed and configured (`aws configure`) to follow these steps. -> Note: The archive command currently defaults to Amazon Linux 2 (`swift:amazonlinux2`) as the build environment. Amazon Linux 2 reaches End of Life on June 30, 2026 and the default will change to Amazon Linux 2023 after that date. To migrate early, re-run the archive command with `--base-docker-image swift:amazonlinux2023`. When deploying a function built on Amazon Linux 2023, you must use the `provided.al2023` Lambda runtime instead of `provided.al2`. +Deploy your function using the `lambda-deploy` plugin: -6. Deploy on AWS Lambda +```sh +swift package --allow-network-connections all:443 lambda-deploy +``` -> Be sure [to have an AWS Account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-creating.html) to follow these steps. +The plugin creates the IAM role, uploads the code, and creates the Lambda function automatically. When the deployment succeeds, it reports the function ARN and a ready-to-use `aws lambda invoke` command. -- Connect to the [AWS Console](https://console.aws.amazon.com) -- Navigate to Lambda -- Create a function -- 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 `{"name": "Seb", "age": 50}` and select **Test** +Invoke your function: -If the test succeeds, you will see the result: `{"greetings":"Hello Seb. You look younger than your age."}`. +```sh +aws lambda invoke \ + --function-name MyFirstLambdaFunction \ + --payload $(echo '{"name":"World","age":30}' | base64) \ + /dev/stdout +``` + +When you're done, clean up the function and its IAM role: +```sh +swift package --allow-network-connections all:443 lambda-deploy --delete +``` -Congratulations 🎉! You just wrote, test, build, and deployed a Lambda function written in Swift. +Congratulations 🎉! You just wrote, tested, built, and deployed a Lambda function written in Swift. diff --git a/Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-write-function.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-write-function.tutorial index 45a850049..2681147fd 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-write-function.tutorial +++ b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-write-function.tutorial @@ -63,7 +63,7 @@ @Step { Add the `platform` section. - It defines on which Apple platforms the code can be executed. Since Lambda functions are supposed to be run on Linux servers with Amazon Linux 2, it is reasonable to make them run only on macOS, for debugging for example. It does not make sense to run this code on iOS, iPadOS, tvOS, and watchOS. + It defines on which Apple platforms the code can be executed. Since Lambda functions are supposed to be run on Linux servers with Amazon Linux 2023, it is reasonable to make them run only on macOS, for debugging for example. It does not make sense to run this code on iOS, iPadOS, tvOS, and watchOS. @Code(name: "Package.swift", file: 03-02-02-package.swift) } @Step { diff --git a/Sources/AWSLambdaRuntime/Docs.docc/tutorials/04-deploy-function.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/04-deploy-function.tutorial index 702135322..4d8a81e07 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/tutorials/04-deploy-function.tutorial +++ b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/04-deploy-function.tutorial @@ -12,7 +12,7 @@ @Steps { - AWS Lambda runs on top of [Amazon Linux 2](https://aws.amazon.com/amazon-linux-2/). You must therefore compile your code for Linux. The AWS Lambda Runtime for Swift uses Docker to do so. Once the code is compiled, it must be assembled in a ZIP file before being deployed in the cloud. + AWS Lambda runs on top of [Amazon Linux 2023](https://aws.amazon.com/linux/amazon-linux-2023/). You must therefore compile your code for Linux. The AWS Lambda Runtime for Swift uses Docker to do so. Once the code is compiled, it must be assembled in a ZIP file before being deployed in the cloud. The AWS Lambda Runtime for Swift provides a [Swift Package Manager plugin](https://github.com/apple/swift-package-manager/blob/main/Documentation/Plugins.md) to compile and zip your Lambda function in one simple step. @Step { @@ -22,13 +22,13 @@ } @Step { - In a terminal, invoke the `archive` command to build and zip your Lambda function. + In a terminal, invoke the `lambda-build` command to build and zip your Lambda function. @Code(name: "Commands in a Terminal", file: 04-01-02-plugin-archive.sh) } @Step { - The plugin starts a Docker container running Amazon Linux 2 and compile your Lambda function code. It then creates a zip file. When everything goes well, you should see an output similar to this one. + The plugin starts a Docker container running Amazon Linux 2023 and compiles your Lambda function code. It then creates a zip file. When everything goes well, you should see an output similar to this one. @Code(name: "Commands in a Terminal", file: 04-01-03-plugin-archive.sh) } @@ -87,7 +87,7 @@ } @Step { - 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. + Enter a **Function name**. I choose `PalindromeLambda`. Select `Amazon Linux 2023` 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`. diff --git a/Tests/AWSLambdaPluginHelperTests/AWSSignerTests.swift b/Tests/AWSLambdaPluginHelperTests/AWSSignerTests.swift deleted file mode 100644 index e9c70cfcf..000000000 --- a/Tests/AWSLambdaPluginHelperTests/AWSSignerTests.swift +++ /dev/null @@ -1,90 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 Testing - -@testable import AWSLambdaPluginHelper - -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import Foundation -#endif - -@Suite -struct SignerTests { - let credentials: Credential = StaticCredential(accessKeyId: "MYACCESSKEY", secretAccessKey: "MYSECRETACCESSKEY") - - @Test - func testSignGetHeaders() { - let signer = AWSSigner(credentials: credentials, name: "glacier", region: "us-east-1") - let headers = signer.signHeaders( - url: URL(string: "https://glacier.us-east-1.amazonaws.com/-/vaults")!, - method: .GET, - headers: ["x-amz-glacier-version": "2012-06-01"], - date: Date(timeIntervalSinceReferenceDate: 2_000_000) - ) - #expect( - headers["Authorization"].first - == "AWS4-HMAC-SHA256 Credential=MYACCESSKEY/20010124/us-east-1/glacier/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-glacier-version, Signature=acfa9b03fca6b098d7b88bfd9bbdb4687f5b34e944a9c6ed9f4814c1b0b06d62" - ) - } - - @Test - func testSignPutHeaders() { - let signer = AWSSigner(credentials: credentials, name: "sns", region: "eu-west-1") - let headers = signer.signHeaders( - url: URL(string: "https://sns.eu-west-1.amazonaws.com/")!, - method: .POST, - headers: ["Content-Type": "application/x-www-form-urlencoded; charset=utf-8"], - body: .string("Action=ListTopics&Version=2010-03-31"), - date: Date(timeIntervalSinceReferenceDate: 200) - ) - #expect( - headers["Authorization"].first - == "AWS4-HMAC-SHA256 Credential=MYACCESSKEY/20010101/eu-west-1/sns/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=1d29943055a8ad094239e8de06082100f2426ebbb2c6a5bbcbb04c63e6a3f274" - ) - } - - @Test - func testSignS3GetURL() { - let signer = AWSSigner(credentials: credentials, name: "s3", region: "us-east-1") - let url = signer.signURL( - url: URL(string: "https://s3.us-east-1.amazonaws.com/")!, - method: .GET, - date: Date(timeIntervalSinceReferenceDate: 100000) - ) - #expect( - url.absoluteString - == "https://s3.us-east-1.amazonaws.com/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=MYACCESSKEY%2F20010102%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20010102T034640Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=27957103c8bfdff3560372b1d85976ed29c944f34295eca2d4fdac7fc02c375a" - ) - } - - @Test - func testSignS3PutURL() { - let signer = AWSSigner(credentials: credentials, name: "s3", region: "eu-west-1") - let url = signer.signURL( - url: URL(string: "https://test-bucket.s3.amazonaws.com/test-put.txt")!, - method: .PUT, - body: .string("Testing signed URLs"), - date: Date(timeIntervalSinceReferenceDate: 100000) - ) - #expect( - url.absoluteString - == "https://test-bucket.s3.amazonaws.com/test-put.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=MYACCESSKEY%2F20010102%2Feu-west-1%2Fs3%2Faws4_request&X-Amz-Date=20010102T034640Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=13d665549a6ea5eb6a1615ede83440eaed3e0ee25c964e62d188c896d916d96f" - ) - } -} diff --git a/Tests/AWSLambdaPluginHelperTests/BuilderConfigurationTests.swift b/Tests/AWSLambdaPluginHelperTests/BuilderConfigurationTests.swift new file mode 100644 index 000000000..7ec33a767 --- /dev/null +++ b/Tests/AWSLambdaPluginHelperTests/BuilderConfigurationTests.swift @@ -0,0 +1,210 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// 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 AWSLambdaPluginHelper + +@Suite("BuilderConfiguration argument parsing") +struct BuilderConfigurationTests { + + // MARK: - Helper + + /// Provides the mandatory arguments required for BuilderConfiguration to parse successfully. + private func defaultArgs( + outputPath: String = "/tmp/output", + products: String = "MyLambda", + configuration: String = "release" + ) -> [String] { + [ + "--package-id", "my-package", + "--package-display-name", "MyPackage", + "--package-directory", "/tmp/project", + "--docker-tool-path", "/usr/local/bin/docker", + "--zip-tool-path", "/usr/bin/zip", + "--output-path", outputPath, + "--products", products, + "--configuration", configuration, + ] + } + + // MARK: - Cross-compile parsing (Requirement 2.7) + + @Test("--cross-compile with valid value 'docker'") + func crossCompileDocker() throws { + let args = defaultArgs() + ["--cross-compile", "docker"] + let config = try BuilderConfiguration(arguments: args) + #expect(config.crossCompileMethod == .docker) + } + + @Test("--cross-compile with valid value 'container'") + func crossCompileContainer() throws { + let args = defaultArgs() + ["--cross-compile", "container"] + let config = try BuilderConfiguration(arguments: args) + #expect(config.crossCompileMethod == .container) + } + + @Test("--cross-compile with 'swift-static-sdk' throws unsupported error") + func crossCompileSwiftStaticSdk() throws { + let args = defaultArgs() + ["--cross-compile", "swift-static-sdk"] + #expect(throws: (any Error).self) { + _ = try BuilderConfiguration(arguments: args) + } + } + + @Test("--cross-compile with 'custom-sdk' throws unsupported error") + func crossCompileCustomSdk() throws { + let args = defaultArgs() + ["--cross-compile", "custom-sdk"] + #expect(throws: (any Error).self) { + _ = try BuilderConfiguration(arguments: args) + } + } + + @Test("--cross-compile with invalid value throws error") + func crossCompileInvalidValue() throws { + let args = defaultArgs() + ["--cross-compile", "invalid-method"] + #expect(throws: (any Error).self) { + _ = try BuilderConfiguration(arguments: args) + } + } + + @Test("--cross-compile defaults to docker when omitted") + func crossCompileDefaultsToDocker() throws { + let args = defaultArgs() + let config = try BuilderConfiguration(arguments: args) + #expect(config.crossCompileMethod == .docker) + } + + // MARK: - No-strip flag (Requirements 2.5, 2.6) + + @Test("--no-strip flag is detected when present") + func noStripFlagPresent() throws { + let args = defaultArgs() + ["--no-strip"] + let config = try BuilderConfiguration(arguments: args) + #expect(config.noStrip == true) + } + + @Test("--no-strip flag defaults to false when omitted") + func noStripFlagAbsent() throws { + let args = defaultArgs() + let config = try BuilderConfiguration(arguments: args) + #expect(config.noStrip == false) + } + + // MARK: - Output directory deprecated alias (Requirement 7.5) + + @Test("--output-directory deprecated alias maps to outputDirectory") + func outputDirectoryAlias() throws { + let args: [String] = [ + "--package-id", "my-package", + "--package-display-name", "MyPackage", + "--package-directory", "/tmp/project", + "--docker-tool-path", "/usr/local/bin/docker", + "--zip-tool-path", "/usr/bin/zip", + "--output-directory", "/custom/output/path", + "--products", "MyLambda", + "--configuration", "release", + ] + let config = try BuilderConfiguration(arguments: args) + #expect(config.outputDirectory.path().hasSuffix("custom/output/path")) + } + + @Test("--output-path takes precedence when both are provided") + func outputPathTakesPrecedence() throws { + let args: [String] = [ + "--package-id", "my-package", + "--package-display-name", "MyPackage", + "--package-directory", "/tmp/project", + "--docker-tool-path", "/usr/local/bin/docker", + "--zip-tool-path", "/usr/bin/zip", + "--output-path", "/primary/path", + "--output-directory", "/deprecated/path", + "--products", "MyLambda", + "--configuration", "release", + ] + let config = try BuilderConfiguration(arguments: args) + #expect(config.outputDirectory.path().hasSuffix("primary/path")) + } + + // MARK: - Mutual exclusion of --swift-version and --base-docker-image (Requirement 2.17) + + @Test("--swift-version and --base-docker-image together throws error") + func mutualExclusionSwiftVersionAndBaseImage() throws { + let args = defaultArgs() + ["--swift-version", "6.0", "--base-docker-image", "swift:6.0-amazonlinux2023"] + #expect(throws: (any Error).self) { + _ = try BuilderConfiguration(arguments: args) + } + } + + @Test("--swift-version alone is accepted") + func swiftVersionAlone() throws { + let args = defaultArgs() + ["--swift-version", "6.0"] + let config = try BuilderConfiguration(arguments: args) + #expect(config.baseDockerImage == "swift:6.0-amazonlinux2023") + } + + @Test("--base-docker-image alone is accepted") + func baseDockerImageAlone() throws { + let args = defaultArgs() + ["--base-docker-image", "swift:5.10-amazonlinux2023"] + let config = try BuilderConfiguration(arguments: args) + #expect(config.baseDockerImage == "swift:5.10-amazonlinux2023") + } + + // MARK: - Default base image is amazonlinux2023 (Requirement 6.1) + + @Test("Default base image contains amazonlinux2023") + func defaultBaseImageIsAL2023() throws { + let args = defaultArgs() + let config = try BuilderConfiguration(arguments: args) + #expect(config.baseDockerImage.contains("amazonlinux2023")) + } + + @Test("Default base image format without swift-version is swift:amazonlinux2023") + func defaultBaseImageFormatNoVersion() throws { + let args = defaultArgs() + let config = try BuilderConfiguration(arguments: args) + #expect(config.baseDockerImage == "swift:amazonlinux2023") + } + + @Test("Base image with --swift-version includes version prefix") + func baseImageWithSwiftVersion() throws { + let args = defaultArgs() + ["--swift-version", "6.1"] + let config = try BuilderConfiguration(arguments: args) + #expect(config.baseDockerImage == "swift:6.1-amazonlinux2023") + } + + // MARK: - Explicit AL2 image detection + + @Test("Explicit AL2 image is detected") + func explicitAL2ImageDetected() throws { + let args = defaultArgs() + ["--base-docker-image", "swift:5.10-amazonlinux2"] + let config = try BuilderConfiguration(arguments: args) + #expect(config.explicitAL2Image == true) + } + + @Test("AL2023 image is not flagged as explicit AL2") + func al2023ImageNotFlaggedAsAL2() throws { + let args = defaultArgs() + ["--base-docker-image", "swift:6.0-amazonlinux2023"] + let config = try BuilderConfiguration(arguments: args) + #expect(config.explicitAL2Image == false) + } + + @Test("Default image (no --base-docker-image) is not flagged as explicit AL2") + func defaultImageNotFlaggedAsAL2() throws { + let args = defaultArgs() + let config = try BuilderConfiguration(arguments: args) + #expect(config.explicitAL2Image == false) + } +} diff --git a/Tests/AWSLambdaPluginHelperTests/CryptoTests.swift b/Tests/AWSLambdaPluginHelperTests/CryptoTests.swift deleted file mode 100644 index 3f9e7aec7..000000000 --- a/Tests/AWSLambdaPluginHelperTests/CryptoTests.swift +++ /dev/null @@ -1,79 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// 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 Testing - -@testable import AWSLambdaPluginHelper - -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import Foundation -#endif - -@Suite -struct CryptoTests { - - @Test - func testSHA256() { - - // given - let input = "hello world" - let expected = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" - - // when - let result = Digest.sha256(input.array).toHexString() - - // then - #expect(result == expected) - } - - @Test - func testHMAC() throws { - - // given - let input = "hello world" - let secret = "secretkey" - let expected = "ae6cd2605d622316564d1f76bfc0c04f89d9fafb14f45b3e18c2a3e28bdef29d" - - // when - let authenticator = HMAC(key: secret.array, variant: .sha2(.sha256)) - - #expect(throws: Never.self) { - let result = try authenticator.authenticate(input.array).toHexString() - // then - #expect(result == expected) - } - - } - - @Test - func testHMACExtension() throws { - - // given - let input = "hello world" - let secret = "secretkey" - let expected = "ae6cd2605d622316564d1f76bfc0c04f89d9fafb14f45b3e18c2a3e28bdef29d" - - // when - let result = try HMAC.authenticate(for: input.array, using: secret.array).toHexString() - - // then - #expect(result == expected) - - } - -} diff --git a/Tests/AWSLambdaPluginHelperTests/DeployerConfigurationTests.swift b/Tests/AWSLambdaPluginHelperTests/DeployerConfigurationTests.swift new file mode 100644 index 000000000..61d84b6cf --- /dev/null +++ b/Tests/AWSLambdaPluginHelperTests/DeployerConfigurationTests.swift @@ -0,0 +1,229 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// 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 AWSLambdaPluginHelper + +@Suite("DeployerConfiguration argument parsing") +struct DeployerConfigurationTests { + + // MARK: - Architecture parsing (Requirement 3.14) + + @Test("Valid architecture x64 is parsed correctly") + func architectureX64() throws { + let config = try DeployerConfiguration(arguments: ["--architecture", "x64"]) + #expect(config.architecture == .x64) + } + + @Test("Valid architecture arm64 is parsed correctly") + func architectureArm64() throws { + let config = try DeployerConfiguration(arguments: ["--architecture", "arm64"]) + #expect(config.architecture == .arm64) + } + + @Test("Invalid architecture throws error") + func invalidArchitectureThrows() throws { + #expect(throws: DeployerErrors.self) { + _ = try DeployerConfiguration(arguments: ["--architecture", "mips"]) + } + } + + @Test("Invalid architecture value produces descriptive error") + func invalidArchitectureMessage() throws { + do { + _ = try DeployerConfiguration(arguments: ["--architecture", "sparc"]) + Issue.record("Expected an error to be thrown") + } catch let error as DeployerErrors { + let description = error.description + #expect(description.contains("sparc")) + #expect(description.contains("x64") || description.contains("arm64")) + } + } + + // MARK: - Default architecture matches host (Requirement 3.13) + + @Test("Default architecture matches host when --architecture is omitted") + func defaultArchitectureMatchesHost() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.architecture == .host) + // Verify .host resolves to a valid value on this machine + #if arch(x86_64) + #expect(config.architecture == .x64) + #else + #expect(config.architecture == .arm64) + #endif + } + + // MARK: - Region parsing (Requirement 3.25) + + @Test("Region is parsed from arguments") + func regionParsing() throws { + let config = try DeployerConfiguration(arguments: ["--region", "eu-west-1"]) + #expect(config.region == "eu-west-1") + } + + @Test("Region is nil when not specified") + func regionDefaultNil() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.region == nil) + } + + @Test("Region with equals syntax is parsed") + func regionEqualsSyntax() throws { + let config = try DeployerConfiguration(arguments: ["--region=us-west-2"]) + #expect(config.region == "us-west-2") + } + + // MARK: - IAM role parsing + + @Test("IAM role is parsed from arguments") + func iamRoleParsing() throws { + let roleArn = "arn:aws:iam::123456789012:role/my-role" + let config = try DeployerConfiguration(arguments: ["--iam-role", roleArn]) + #expect(config.iamRole == roleArn) + } + + @Test("IAM role is nil when not specified") + func iamRoleDefaultNil() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.iamRole == nil) + } + + // MARK: - Input directory parsing + + @Test("Input directory is parsed from arguments") + func inputDirectoryParsing() throws { + let config = try DeployerConfiguration(arguments: ["--input-directory", "/tmp/build/output"]) + #expect(config.inputDirectory != nil) + #expect(config.inputDirectory?.path().contains("/tmp/build/output") == true) + } + + @Test("Input directory is nil when not specified") + func inputDirectoryDefaultNil() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.inputDirectory == nil) + } + + // MARK: - With URL flag parsing + + @Test("--with-url flag is detected") + func withURLFlag() throws { + let config = try DeployerConfiguration(arguments: ["--with-url"]) + #expect(config.withURL == true) + } + + @Test("--with-url defaults to false") + func withURLDefaultFalse() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.withURL == false) + } + + // MARK: - Delete flag parsing + + @Test("--delete flag is detected") + func deleteFlag() throws { + let config = try DeployerConfiguration(arguments: ["--delete"]) + #expect(config.delete == true) + } + + @Test("--delete defaults to false") + func deleteDefaultFalse() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.delete == false) + } + + // MARK: - Help flag (Requirement 3.25) + + @Test("--help flag is detected") + func helpFlag() throws { + let config = try DeployerConfiguration(arguments: ["--help"]) + #expect(config.help == true) + } + + @Test("--help defaults to false") + func helpDefaultFalse() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.help == false) + } + + // MARK: - Verbose flag + + @Test("--verbose flag is detected") + func verboseFlag() throws { + let config = try DeployerConfiguration(arguments: ["--verbose"]) + #expect(config.verboseLogging == true) + } + + @Test("--verbose defaults to false") + func verboseDefaultFalse() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.verboseLogging == false) + } + + // MARK: - Products parsing + + @Test("Products are parsed from arguments") + func productsParsing() throws { + let config = try DeployerConfiguration(arguments: ["--products", "MyLambda"]) + #expect(config.products == ["MyLambda"]) + } + + @Test("Multiple comma-separated products are parsed") + func multipleProductsParsing() throws { + let config = try DeployerConfiguration(arguments: ["--products", "FuncA,FuncB,FuncC"]) + #expect(config.products == ["FuncA", "FuncB", "FuncC"]) + } + + @Test("Products default to empty array") + func productsDefaultEmpty() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.products.isEmpty) + } + + // MARK: - Combined arguments + + @Test("Multiple options parsed together") + func combinedArguments() throws { + let config = try DeployerConfiguration(arguments: [ + "--region", "ap-southeast-1", + "--architecture", "arm64", + "--with-url", + "--verbose", + "--iam-role", "arn:aws:iam::123456789012:role/test", + "--input-directory", "/tmp/output", + "--products", "MyFunc", + ]) + #expect(config.region == "ap-southeast-1") + #expect(config.architecture == .arm64) + #expect(config.withURL == true) + #expect(config.verboseLogging == true) + #expect(config.iamRole == "arn:aws:iam::123456789012:role/test") + #expect(config.inputDirectory?.path().contains("/tmp/output") == true) + #expect(config.products == ["MyFunc"]) + } + + @Test("Delete with region and products") + func deleteWithOptions() throws { + let config = try DeployerConfiguration(arguments: [ + "--delete", + "--region", "us-east-1", + "--products", "MyFunc", + ]) + #expect(config.delete == true) + #expect(config.region == "us-east-1") + #expect(config.products == ["MyFunc"]) + } +} diff --git a/Tests/AWSLambdaPluginHelperTests/DeployerS3Tests.swift b/Tests/AWSLambdaPluginHelperTests/DeployerS3Tests.swift new file mode 100644 index 000000000..f18c32483 --- /dev/null +++ b/Tests/AWSLambdaPluginHelperTests/DeployerS3Tests.swift @@ -0,0 +1,143 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// 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 AWSLambdaPluginHelper + +// MARK: - Deployment Bucket Name Construction Tests + +@Suite("Deployment bucket name construction") +struct DeploymentBucketNameTests { + + @Test("Bucket name has correct format: swift-aws-lambda-runtime--") + func bucketNameFormat() { + let name = Deployer.deploymentBucketName(region: "us-east-1", accountId: "123456789012") + #expect(name == "swift-aws-lambda-runtime-us-east-1-123456789012") + } + + @Test("Bucket name with eu-west-1 region") + func bucketNameEuWest1() { + let name = Deployer.deploymentBucketName(region: "eu-west-1", accountId: "987654321098") + #expect(name == "swift-aws-lambda-runtime-eu-west-1-987654321098") + } + + @Test("Bucket name with ap-southeast-2 region") + func bucketNameApSoutheast2() { + let name = Deployer.deploymentBucketName(region: "ap-southeast-2", accountId: "111222333444") + #expect(name == "swift-aws-lambda-runtime-ap-southeast-2-111222333444") + } + + @Test("Bucket name with us-west-2 region and different account") + func bucketNameUsWest2() { + let name = Deployer.deploymentBucketName(region: "us-west-2", accountId: "000000000000") + #expect(name == "swift-aws-lambda-runtime-us-west-2-000000000000") + } + + @Test("Bucket name is always lowercase") + func bucketNameIsLowercase() { + let name = Deployer.deploymentBucketName(region: "us-east-1", accountId: "123456789012") + #expect(name == name.lowercased()) + } + + @Test( + "Bucket name length is between 3 and 63 characters (valid S3 name)", + arguments: [ + ("us-east-1", "123456789012"), + ("eu-west-1", "987654321098"), + ("ap-southeast-2", "111222333444"), + ("us-gov-west-1", "555666777888"), + ("me-south-1", "000000000001"), + ] + ) + func bucketNameLengthIsValid(region: String, accountId: String) { + let name = Deployer.deploymentBucketName(region: region, accountId: accountId) + #expect(name.count >= 3, "Bucket name must be at least 3 characters") + #expect(name.count <= 63, "Bucket name must be at most 63 characters") + } + + @Test( + "Bucket name contains only valid S3 characters (lowercase, digits, hyphens)", + arguments: [ + ("us-east-1", "123456789012"), + ("ap-northeast-1", "999888777666"), + ("eu-central-1", "012345678901"), + ] + ) + func bucketNameContainsOnlyValidCharacters(region: String, accountId: String) { + let name = Deployer.deploymentBucketName(region: region, accountId: accountId) + let validCharacters = "abcdefghijklmnopqrstuvwxyz0123456789-" + let allValid = name.allSatisfy { validCharacters.contains($0) } + #expect(allValid, "Bucket name must contain only lowercase letters, digits, and hyphens") + } +} + +// MARK: - Archive Size Threshold Tests + +@Suite("Archive size threshold and upload strategy") +struct ArchiveSizeThresholdTests { + + @Test("directUploadLimit is exactly 50 MB") + func directUploadLimitValue() { + let expectedLimit: Int64 = 50 * 1024 * 1024 + #expect(Deployer.directUploadLimit == expectedLimit) + } + + @Test("Archive of exactly 50 MB should upload directly") + func exactly50MBUploadsDirectly() { + let fiftyMB: Int64 = 50 * 1024 * 1024 + #expect(Deployer.shouldUploadDirectly(archiveSize: fiftyMB) == true) + } + + @Test("Archive of 50 MB + 1 byte should use S3 staging") + func fiftyMBPlusOneUsesS3() { + let fiftyMBPlusOne: Int64 = 50 * 1024 * 1024 + 1 + #expect(Deployer.shouldUploadDirectly(archiveSize: fiftyMBPlusOne) == false) + } + + @Test("Archive of 0 bytes should upload directly") + func zeroBytesUploadsDirectly() { + #expect(Deployer.shouldUploadDirectly(archiveSize: 0) == true) + } + + @Test("Archive of 1 byte should upload directly") + func oneByteUploadsDirectly() { + #expect(Deployer.shouldUploadDirectly(archiveSize: 1) == true) + } + + @Test("Archive of 49 MB should upload directly") + func fortyNineMBUploadsDirectly() { + let fortyNineMB: Int64 = 49 * 1024 * 1024 + #expect(Deployer.shouldUploadDirectly(archiveSize: fortyNineMB) == true) + } + + @Test("Archive of 51 MB should use S3 staging") + func fiftyOneMBUsesS3() { + let fiftyOneMB: Int64 = 51 * 1024 * 1024 + #expect(Deployer.shouldUploadDirectly(archiveSize: fiftyOneMB) == false) + } + + @Test("Archive of 100 MB should use S3 staging") + func hundredMBUsesS3() { + let hundredMB: Int64 = 100 * 1024 * 1024 + #expect(Deployer.shouldUploadDirectly(archiveSize: hundredMB) == false) + } + + @Test("Archive of 250 MB (Lambda max) should use S3 staging") + func lambdaMaxSizeUsesS3() { + let lambdaMax: Int64 = 250 * 1024 * 1024 + #expect(Deployer.shouldUploadDirectly(archiveSize: lambdaMax) == false) + } +} diff --git a/Tests/AWSLambdaPluginHelperTests/Placeholder.swift b/Tests/AWSLambdaPluginHelperTests/Placeholder.swift new file mode 100644 index 000000000..4954eb87a --- /dev/null +++ b/Tests/AWSLambdaPluginHelperTests/Placeholder.swift @@ -0,0 +1,3 @@ +// Placeholder file to keep the test target valid. +// Tests for the vendored crypto/signer code were removed when the vendored code +// was replaced by soto-core (task 5). New unit and property tests will be added in task 11. diff --git a/Tests/AWSLambdaPluginHelperTests/PropertyTests.swift b/Tests/AWSLambdaPluginHelperTests/PropertyTests.swift new file mode 100644 index 000000000..f1e922f56 --- /dev/null +++ b/Tests/AWSLambdaPluginHelperTests/PropertyTests.swift @@ -0,0 +1,614 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// 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 canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif +import Testing + +@testable import AWSLambdaPluginHelper + +// MARK: - Property 2: Deprecated option alias equivalence + +/// **Validates: Requirements 7.5, 7.6** +/// +/// For any path value, `--output-directory ` produces the same `outputDirectory` +/// as `--output-path `. +@Suite("Property 2: Deprecated option alias equivalence") +struct DeprecatedAliasEquivalencePropertyTests { + + static let samplePaths: [String] = [ + "/tmp/output", + "/usr/local/build", + "/home/user/projects/my-lambda/output", + "/var/folders/abc/xyz", + "/a", + "/output-with-dashes", + "/path/with spaces", + "/path_with_underscores/nested/deep/dir", + "/simple", + "/very/long/path/that/goes/deep/into/the/filesystem/structure/for/testing", + "/tmp", + "/usr", + "/build", + "/opt/lambda/out", + "/Users/developer/Desktop/project/dist", + "/root/deploy", + "/mnt/data/builds/release", + "/srv/app/output", + "/home/ci/workspace/artifacts", + "/tmp/swift-build-output", + "/Volumes/External/builds", + "/private/tmp/xcode-build", + "/workspace/output", + "/code/bin", + "/artifacts/v1.0", + "/release/arm64", + "/debug/output", + "/home/user/.build/output", + "/tmp/output-2024", + "/project/dist/lambda", + "/builds/nightly/latest", + "/ci/artifacts/staging", + "/deploy/packages", + "/lambda/archives", + "/swift/build/products", + "/output123", + "/tmp/a/b/c/d/e/f/g", + "/data/out", + "/results/final", + "/packages/compiled", + "/snapshots/build-42", + "/Users/test/output", + "/tmp/build-output-1", + "/tmp/build-output-2", + "/tmp/build-output-3", + "/var/output/lambda", + "/opt/builds/release-2", + "/srv/builds/debug-1", + "/home/dev/out", + "/workspace/dist", + "/project/target", + "/builds/latest", + "/artifacts/snapshot", + "/deploy/staging", + "/lambda/output", + "/archive/dir", + "/compiled/bins", + "/packaged/zips", + "/release/packages", + "/debug/bins", + "/test/output", + "/ci/output", + "/cd/output", + "/dev/output", + "/staging/output", + "/prod/output", + "/alpha/output", + "/beta/output", + "/gamma/output", + "/delta/output", + "/epsilon/output", + "/zeta/output", + "/eta/output", + "/theta/output", + "/iota/output", + "/kappa/output", + "/lambda-out", + "/mu/output", + "/nu/output", + "/xi/output", + "/omicron/output", + "/pi/output", + "/rho/output", + "/sigma/output", + "/tau/output", + "/upsilon/output", + "/phi/output", + "/chi/output", + "/psi/output", + "/omega/output", + "/final/output", + "/last/output", + "/end/output", + "/done/output", + "/complete/output", + "/finished/output", + "/ready/output", + "/built/output", + "/assembled/output", + "/crafted/output", + "/forged/output", + "/made/output", + "/created/output", + "/generated/output", + "/produced/output", + ] + + private func baseArgs(excludingOutputArgs: Bool = true) -> [String] { + [ + "--package-id", "my-package", + "--package-display-name", "MyPackage", + "--package-directory", "/tmp/project", + "--docker-tool-path", "/usr/local/bin/docker", + "--zip-tool-path", "/usr/bin/zip", + "--products", "MyLambda", + "--configuration", "release", + ] + } + + @Test("--output-directory produces same outputDirectory as --output-path", arguments: samplePaths) + func deprecatedAliasEquivalence(path: String) throws { + let argsWithOutputPath = baseArgs() + ["--output-path", path] + let configWithOutputPath = try BuilderConfiguration(arguments: argsWithOutputPath) + + let argsWithOutputDirectory = baseArgs() + ["--output-directory", path] + let configWithOutputDirectory = try BuilderConfiguration(arguments: argsWithOutputDirectory) + + #expect( + configWithOutputPath.outputDirectory == configWithOutputDirectory.outputDirectory, + "--output-directory '\(path)' should produce same outputDirectory as --output-path '\(path)'" + ) + } +} + +// MARK: - Property 3: Cross-compile method parsing round-trip + +/// **Validates: Requirements 2.7** +/// +/// For any valid `CrossCompileMethod` enum case, `rawValue` → parse → original case. +/// Note: swift-static-sdk and custom-sdk throw "unsupported" on `parse()`, but their +/// rawValue round-trips through the enum initializer correctly. +@Suite("Property 3: Cross-compile method parsing round-trip") +struct CrossCompileMethodRoundTripPropertyTests { + + static let allCases: [CrossCompileMethod] = [ + .docker, + .container, + .swiftStaticSdk, + .customSdk, + ] + + @Test("rawValue → init(rawValue:) round-trips for all CrossCompileMethod cases", arguments: allCases) + func rawValueRoundTrip(method: CrossCompileMethod) { + let rawValue = method.rawValue + let parsed = CrossCompileMethod(rawValue: rawValue) + #expect(parsed == method, "CrossCompileMethod(rawValue: \"\(rawValue)\") should produce \(method)") + } +} + +// MARK: - Property 4: Mutual exclusion of --swift-version and --base-docker-image + +/// **Validates: Requirements 2.17** +/// +/// For any non-empty swift-version and any non-empty base-docker-image, parsing throws an error. +@Suite("Property 4: Mutual exclusion of --swift-version and --base-docker-image") +struct MutualExclusionPropertyTests { + + static let swiftVersions: [String] = [ + "5.9", "5.10", "6.0", "6.1", "6.2", + "5.9.1", "5.9.2", "5.10.1", "6.0.1", "6.0.2", + "6.1.0", "6.1.1", "6.2.0", "7.0", "8.0", + "5.0", "5.1", "5.2", "5.3", "5.4", + "5.5", "5.6", "5.7", "5.8", "4.2", + ] + + static let dockerImages: [String] = [ + "swift:5.9-amazonlinux2023", + "swift:6.0-amazonlinux2023", + "swift:6.1-amazonlinux2023", + "swift:latest-amazonlinux2023", + "swift:5.10-amazonlinux2", + "myregistry/swift:6.0-al2023", + "custom-image:latest", + "ubuntu:22.04", + "swift:nightly-amazonlinux2023", + "ghcr.io/swift/swift:6.0", + ] + + static let combinations: [(String, String)] = { + var result: [(String, String)] = [] + for version in swiftVersions { + for image in dockerImages { + result.append((version, image)) + } + } + // Return first 100 + return Array(result.prefix(100)) + }() + + private func baseArgs() -> [String] { + [ + "--package-id", "my-package", + "--package-display-name", "MyPackage", + "--package-directory", "/tmp/project", + "--docker-tool-path", "/usr/local/bin/docker", + "--zip-tool-path", "/usr/bin/zip", + "--output-path", "/tmp/output", + "--products", "MyLambda", + "--configuration", "release", + ] + } + + @Test( + "Both --swift-version and --base-docker-image throws error", + arguments: combinations + ) + func mutualExclusionThrows(version: String, image: String) { + let args = baseArgs() + ["--swift-version", version, "--base-docker-image", image] + #expect(throws: (any Error).self) { + _ = try BuilderConfiguration(arguments: args) + } + } +} + +// MARK: - Property 5: Deployment bucket name construction + +/// **Validates: Requirements 3.17, 3.18** +/// +/// For any valid region and 12-digit account ID, result matches +/// "swift-aws-lambda-runtime--" and is a valid S3 bucket name. +@Suite("Property 5: Deployment bucket name construction") +struct DeploymentBucketNamePropertyTests { + + static let regions: [String] = [ + "us-east-1", "us-east-2", "us-west-1", "us-west-2", + "eu-west-1", "eu-west-2", "eu-west-3", "eu-central-1", "eu-central-2", + "eu-north-1", "eu-south-1", "eu-south-2", + "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", + "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", + "ap-south-1", "ap-south-2", "ap-east-1", + "sa-east-1", "ca-central-1", "ca-west-1", + "me-south-1", "me-central-1", + "af-south-1", "il-central-1", + "us-gov-west-1", "us-gov-east-1", + ] + + static let accountIds: [String] = [ + "123456789012", "000000000000", "999999999999", + "111111111111", "222222222222", "333333333333", + "444444444444", "555555555555", "666666666666", + "777777777777", "888888888888", "012345678901", + "109876543210", "100200300400", "001002003004", + ] + + static let combinations: [(String, String)] = { + var result: [(String, String)] = [] + for region in regions { + for accountId in accountIds { + result.append((region, accountId)) + } + } + // Return first 100 + return Array(result.prefix(100)) + }() + + @Test( + "Bucket name matches expected format and is a valid S3 name", + arguments: combinations + ) + func bucketNameConstruction(region: String, accountId: String) { + let name = Deployer.deploymentBucketName(region: region, accountId: accountId) + + // Verify format + let expectedName = "swift-aws-lambda-runtime-\(region)-\(accountId)" + #expect(name == expectedName, "Expected '\(expectedName)' but got '\(name)'") + + // Verify it's lowercase + #expect(name == name.lowercased(), "Bucket name must be all lowercase") + + // Verify length is between 3 and 63 characters (valid S3 bucket name) + #expect(name.count >= 3, "Bucket name must be at least 3 characters, got \(name.count)") + #expect(name.count <= 63, "Bucket name must be at most 63 characters, got \(name.count)") + + // Verify only valid S3 bucket name characters (lowercase letters, digits, hyphens) + let validChars = "abcdefghijklmnopqrstuvwxyz0123456789-" + let allValid = name.allSatisfy { validChars.contains($0) } + #expect(allValid, "Bucket name must contain only lowercase letters, digits, and hyphens") + + // Verify doesn't start or end with hyphen + #expect(!name.hasPrefix("-"), "Bucket name must not start with a hyphen") + #expect(!name.hasSuffix("-"), "Bucket name must not end with a hyphen") + } +} + +// MARK: - Property 6: Archive size determines upload strategy + +/// **Validates: Requirements 3.15, 3.19** +/// +/// For any size ≤ 50 MB → direct upload; for any size > 50 MB → S3 staging. +@Suite("Property 6: Archive size determines upload strategy") +struct ArchiveSizeUploadStrategyPropertyTests { + + static let fiftyMB: Int64 = 50 * 1024 * 1024 + + static let sizesAtOrBelowLimit: [Int64] = { + var sizes: [Int64] = [0, 1, 100, 1024, 10_000, 100_000, 1_000_000] + // Add sizes approaching the boundary + let limit: Int64 = fiftyMB + for offset: Int64 in stride(from: 50, through: 0, by: -1) { + sizes.append(limit - offset) + } + // Add various sizes below limit + let moreSizes: [Int64] = [ + Int64(1024 * 1024), + Int64(5 * 1024 * 1024), + Int64(10 * 1024 * 1024), + Int64(20 * 1024 * 1024), + Int64(25 * 1024 * 1024), + Int64(30 * 1024 * 1024), + Int64(40 * 1024 * 1024), + Int64(45 * 1024 * 1024), + Int64(49 * 1024 * 1024), + ] + sizes.append(contentsOf: moreSizes) + return Array(Set(sizes).sorted().prefix(100)) + }() + + static let sizesAboveLimit: [Int64] = { + var sizes: [Int64] = [] + let limit: Int64 = fiftyMB + // Add sizes just above the boundary + for offset: Int64 in 1...51 { + sizes.append(limit + offset) + } + // Add various sizes above limit + let moreSizes: [Int64] = [ + Int64(51 * 1024 * 1024), + Int64(55 * 1024 * 1024), + Int64(60 * 1024 * 1024), + Int64(75 * 1024 * 1024), + Int64(100 * 1024 * 1024), + Int64(150 * 1024 * 1024), + Int64(200 * 1024 * 1024), + Int64(250 * 1024 * 1024), + Int64(300 * 1024 * 1024), + Int64(500 * 1024 * 1024), + Int64(1024 * 1024 * 1024), + ] + sizes.append(contentsOf: moreSizes) + return Array(Set(sizes).sorted().prefix(100)) + }() + + @Test("Sizes at or below 50 MB should upload directly", arguments: sizesAtOrBelowLimit) + func sizeAtOrBelowLimitUploadsDirect(size: Int64) { + #expect( + Deployer.shouldUploadDirectly(archiveSize: size) == true, + "Archive of \(size) bytes (≤ 50 MB) should upload directly" + ) + } + + @Test("Sizes above 50 MB should use S3 staging", arguments: sizesAboveLimit) + func sizeAboveLimitUsesS3(size: Int64) { + #expect( + Deployer.shouldUploadDirectly(archiveSize: size) == false, + "Archive of \(size) bytes (> 50 MB) should use S3 staging" + ) + } +} + +// MARK: - Property 8: AL2 warning logic + +/// **Validates: Requirements 6.2, 6.3** +/// +/// For any image containing "amazonlinux2" but NOT "amazonlinux2023" → explicitAL2Image is true. +/// For "amazonlinux2023" or default → explicitAL2Image is false. +@Suite("Property 8: AL2 warning emitted only for explicit AL2 image selection") +struct AL2WarningLogicPropertyTests { + + static let al2Images: [String] = [ + "swift:5.9-amazonlinux2", + "swift:5.10-amazonlinux2", + "swift:6.0-amazonlinux2", + "swift:6.1-amazonlinux2", + "swift:latest-amazonlinux2", + "myregistry/swift:5.9-amazonlinux2", + "custom-amazonlinux2-image:v1", + "swift:nightly-amazonlinux2", + "ghcr.io/swift:6.0-amazonlinux2", + "amazonlinux2-swift:6.0", + "swift:5.8-amazonlinux2", + "swift:5.7-amazonlinux2", + "swift:5.6-amazonlinux2", + "registry.example.com/swift:6.0-amazonlinux2", + "my-amazonlinux2-build:latest", + "swift-amazonlinux2:6.1", + "public.ecr.aws/swift:6.0-amazonlinux2", + "test-amazonlinux2-image", + "swift:5.5-amazonlinux2", + "dev-amazonlinux2:v2", + "swift:5.4-amazonlinux2", + "swift:5.3-amazonlinux2", + "base-amazonlinux2:latest", + "swift-runtime-amazonlinux2:6.0", + "ci-amazonlinux2-builder:1.0", + "swift:5.2-amazonlinux2", + "custom-swift-amazonlinux2:dev", + "swift:5.1-amazonlinux2", + "staging-amazonlinux2:v3", + "swift:5.0-amazonlinux2", + "swift:4.2-amazonlinux2", + "build-amazonlinux2:release", + "swift:amazonlinux2", + "my/repo/amazonlinux2:tag", + "test:amazonlinux2-latest", + "swift:6.2-amazonlinux2", + "registry/amazonlinux2-swift:v1", + "local-amazonlinux2:dev", + "swift:nightly-main-amazonlinux2", + "swift:6.0.1-amazonlinux2", + "swift:6.0.2-amazonlinux2", + "swift:6.1.0-amazonlinux2", + "builder-amazonlinux2:prod", + "deploy-amazonlinux2:staging", + "lambda-amazonlinux2:latest", + "swift-lambda-amazonlinux2:6.0", + "amazonlinux2-builder:v4", + "ci/cd-amazonlinux2:latest", + "swift:release-amazonlinux2", + "swift:dev-amazonlinux2", + ] + + static let nonAL2Images: [String] = [ + "swift:5.9-amazonlinux2023", + "swift:6.0-amazonlinux2023", + "swift:6.1-amazonlinux2023", + "swift:latest-amazonlinux2023", + "swift:nightly-amazonlinux2023", + "myregistry/swift:6.0-amazonlinux2023", + "custom-amazonlinux2023:v1", + "ubuntu:22.04", + "debian:bookworm", + "alpine:3.18", + "fedora:39", + "centos:stream9", + "swift:6.0", + "swift:latest", + "ghcr.io/swift:6.0-amazonlinux2023", + "registry.example.com/swift:6.0-amazonlinux2023", + "public.ecr.aws/swift:6.0-amazonlinux2023", + "swift:5.10-amazonlinux2023", + "swift:6.2-amazonlinux2023", + "custom-image:latest", + "my-build-image:v1", + "swift-builder:6.0", + "lambda-base:latest", + "ci-runner:2.0", + "dev-env:latest", + "swift:nightly-main-amazonlinux2023", + "swift:6.0.1-amazonlinux2023", + "swift:6.0.2-amazonlinux2023", + "swift:6.1.0-amazonlinux2023", + "amazonlinux2023-swift:6.0", + "my-amazonlinux2023-image:v1", + "swift-amazonlinux2023:latest", + "ci-amazonlinux2023:prod", + "builder-amazonlinux2023:v2", + "deploy-amazonlinux2023:staging", + "swift:release-amazonlinux2023", + "swift:dev-amazonlinux2023", + "registry/amazonlinux2023-swift:v1", + "local-amazonlinux2023:dev", + "staging-amazonlinux2023:v3", + "base-amazonlinux2023:latest", + "swift-runtime-amazonlinux2023:6.0", + "ci-amazonlinux2023-builder:1.0", + "lambda-amazonlinux2023:latest", + "swift-lambda-amazonlinux2023:6.0", + "amazonlinux2023-builder:v4", + "test-amazonlinux2023-image", + "prod-amazonlinux2023:release", + "nightly-amazonlinux2023:latest", + "snapshot-amazonlinux2023:v5", + ] + + private func baseArgs() -> [String] { + [ + "--package-id", "my-package", + "--package-display-name", "MyPackage", + "--package-directory", "/tmp/project", + "--docker-tool-path", "/usr/local/bin/docker", + "--zip-tool-path", "/usr/bin/zip", + "--output-path", "/tmp/output", + "--products", "MyLambda", + "--configuration", "release", + ] + } + + @Test("AL2 images (not AL2023) set explicitAL2Image to true", arguments: al2Images) + func al2ImageDetected(image: String) throws { + let args = baseArgs() + ["--base-docker-image", image] + let config = try BuilderConfiguration(arguments: args) + #expect( + config.explicitAL2Image == true, + "Image '\(image)' contains 'amazonlinux2' but not 'amazonlinux2023', should set explicitAL2Image=true" + ) + } + + @Test("AL2023 or non-AL2 images set explicitAL2Image to false", arguments: nonAL2Images) + func nonAL2ImageNotDetected(image: String) throws { + let args = baseArgs() + ["--base-docker-image", image] + let config = try BuilderConfiguration(arguments: args) + #expect( + config.explicitAL2Image == false, + "Image '\(image)' should set explicitAL2Image=false" + ) + } + + @Test("Default image (no --base-docker-image) sets explicitAL2Image to false") + func defaultImageNotFlagged() throws { + let args = baseArgs() + let config = try BuilderConfiguration(arguments: args) + #expect(config.explicitAL2Image == false) + } +} + +// MARK: - Property 9: Unsupported cross-compile methods report error with link + +/// **Validates: Requirements 2.14** +/// +/// For swift-static-sdk and custom-sdk, CrossCompileMethod.parse throws error +/// containing the SDK guide URL. +@Suite("Property 9: Unsupported cross-compile methods report error with link") +struct UnsupportedCrossCompileMethodsPropertyTests { + + static let unsupportedMethods: [String] = [ + "swift-static-sdk", + "custom-sdk", + ] + + static let sdkGuideURL = "https://www.swift.org/documentation/articles/static-linux-getting-started.html" + + @Test("Unsupported methods throw error with SDK guide URL", arguments: unsupportedMethods) + func unsupportedMethodThrowsWithLink(method: String) { + do { + _ = try CrossCompileMethod.parse(method) + Issue.record("Expected CrossCompileMethod.parse(\"\(method)\") to throw, but it succeeded") + } catch { + let errorDescription = String(describing: error) + #expect( + errorDescription.contains(Self.sdkGuideURL), + "Error for '\(method)' should contain SDK guide URL '\(Self.sdkGuideURL)', got: \(errorDescription)" + ) + } + } + + @Test("Unsupported methods via BuilderConfiguration throw error with SDK guide URL", arguments: unsupportedMethods) + func unsupportedMethodInBuilderConfigThrowsWithLink(method: String) { + let args: [String] = [ + "--package-id", "my-package", + "--package-display-name", "MyPackage", + "--package-directory", "/tmp/project", + "--docker-tool-path", "/usr/local/bin/docker", + "--zip-tool-path", "/usr/bin/zip", + "--output-path", "/tmp/output", + "--products", "MyLambda", + "--configuration", "release", + "--cross-compile", method, + ] + do { + _ = try BuilderConfiguration(arguments: args) + Issue.record("Expected BuilderConfiguration to throw for --cross-compile \(method)") + } catch { + let errorDescription = String(describing: error) + #expect( + errorDescription.contains(Self.sdkGuideURL), + "Error for '--cross-compile \(method)' should contain SDK guide URL, got: \(errorDescription)" + ) + } + } +} diff --git a/readme.md b/readme.md index b6daabfe6..3ac49ee95 100644 --- a/readme.md +++ b/readme.md @@ -40,7 +40,7 @@ Swift AWS Lambda Runtime was designed to make building Lambda functions in Swift - To build and archive your Lambda function, you need to install [docker](https://docs.docker.com/desktop/install/mac-install/) or Apple [container](https://github.com/apple/container). -- 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). +- 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) (run `aws configure` to set up your credentials in `~/.aws/`). - 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. @@ -58,7 +58,7 @@ For the fastest path, just type: ```bash cd Examples/_MyFirstFunction -./create_and_deploy_function.sh +./create_function.sh ``` Otherwise, continue reading. @@ -117,19 +117,35 @@ The runtime comes with a plugin to generate the code of a simple AWS Lambda func swift package lambda-init --allow-writing-to-package-directory ``` -Your `Sources/main.swift` file should look like this. +Your `Sources/MyLambda/MyLambda.swift` file should look like this. ```swift import AWSLambdaRuntime -// in this example we are receiving and responding with strings +// 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: String, context: LambdaContext) in - return String(event.reversed()) + (event: HelloRequest, context: LambdaContext) in + + HelloResponse( + greetings: "Hello \(event.name). You look \(event.age > 30 ? "younger" : "older") than your age." + ) } -try await runtime.run() +// start the loop +try await runtime.run() ``` 4. Build & archive the package @@ -137,9 +153,7 @@ try await runtime.run() The runtime comes with a plugin to compile on Amazon Linux and create a ZIP archive: ```bash -swift package archive \ - --allow-network-connections docker \ - --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` By default, it runs on `docker` but it also allows you to build with [Apple container](https://github.com/apple/container) (it requires disabling the sandbox): @@ -150,9 +164,8 @@ By default, it runs on `docker` but it also allows you to build with [Apple cont # until https://github.com/swiftlang/swift-package-manager/issues/9763 is fixed swift package --disable-sandbox \ --allow-network-connections docker \ - --base-docker-image swift:amazonlinux2023 \ - archive \ - --container-cli container + lambda-build \ + --cross-compile container ``` If there is no error, the ZIP archive is ready to deploy. @@ -161,59 +174,38 @@ The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPa > [!NOTE] > If you encounter Docker credential store errors during the build, remove the `credsStore` entry from your `~/.docker/config.json` file or disable the plugin sandbox with `--disable-sandbox`. See [issue #609](https://github.com/awslabs/swift-aws-lambda-runtime/issues/609) for details. -> [!NOTE] -> The archive plugin currently defaults to Amazon Linux 2 as the build environment. After June 30, 2026, the default will change to Amazon Linux 2023. To migrate early, add the `--base-docker-image swift:amazonlinux2023` flag to the archive command: -> ```bash -> swift package archive \ -> --allow-network-connections docker \ -> --base-docker-image swift:amazonlinux2023 -> ``` -> When deploying functions built on Amazon Linux 2023, you must use the `provided.al2023` runtime instead of `provided.al2` in the `aws lambda create-function` command. - 5. Deploy to AWS 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. -Here is how to deploy using the `aws` command line. +The fastest way to deploy is using the `lambda-deploy` plugin. It handles IAM role creation, function creation, and code upload automatically. + +> [!IMPORTANT] +> Before deploying, ensure you have the AWS CLI installed and have run `aws configure` to set up your credentials in `~/.aws/`. On EC2, ECS, or EKS, credentials are typically provided automatically by the instance or task role, so running `aws configure` is not required in those environments. ```bash -aws lambda create-function \ ---function-name MyLambda \ ---zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip \ ---runtime provided.al2023 \ ---handler provided \ ---architectures arm64 \ ---role arn:aws:iam:::role/lambda_basic_execution +swift package --allow-network-connections all:443 lambda-deploy ``` -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`. - -Replace `` with your actual AWS account ID (for example: 012345678901). - -> [!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/awslabs/swift-aws-lambda-runtime/blob/8dff649920ab0c66bb039d15ae48d9d5764db71a/Examples/_MyFirstFunction/create_and_deploy_function.sh#L40C1-L40C31) +This creates the Lambda function, provisions the necessary IAM role, and uploads the deployment package. 6. Invoke your Lambda function ```bash aws lambda invoke \ --function-name MyLambda \ ---payload $(echo \"Hello World\" | base64) \ -out.txt && cat out.txt && rm out.txt +--payload $(echo '{"name":"World","age":30}' | base64) \ +/dev/stdout ``` This should print ``` +{"greetings":"Hello World. You look older than your age."} { "StatusCode": 200, "ExecutedVersion": "$LATEST" } -"dlroW olleH" ``` ## Developing your Swift Lambda functions diff --git a/scripts/generate-aws-clients.sh b/scripts/generate-aws-clients.sh new file mode 100755 index 000000000..302d26b86 --- /dev/null +++ b/scripts/generate-aws-clients.sh @@ -0,0 +1,349 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright SwiftAWSLambdaRuntime project authors +## Copyright (c) Amazon.com, Inc. or its affiliates. +## 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 +## +##===----------------------------------------------------------------------===## + +# ============================================================================= +# generate-aws-clients.sh +# +# Maintainer-run script to generate AWS service clients for the Lambda deploy +# plugin. This script is NOT part of the build process. It uses the Soto Code +# Generator to produce lightweight Swift clients for Lambda, IAM, S3, and STS +# with only the operations required by the deployer. +# +# Prerequisites: +# - Swift toolchain installed +# - Git installed +# - Internet access (to clone repos and download models) +# +# Usage: +# ./scripts/generate-aws-clients.sh +# +# The generated files are written to: +# Sources/AWSLambdaPluginHelper/GeneratedClients/ +# ============================================================================= + +set -euo pipefail + +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { error "$@"; exit 1; } + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +OUTPUT_DIR="${PROJECT_ROOT}/Sources/AWSLambdaPluginHelper/GeneratedClients" + +# Soto Code Generator repository and version +SOTO_CODEGEN_REPO="https://github.com/soto-project/soto-codegenerator.git" +SOTO_CODEGEN_BRANCH="main" + +# AWS SDK Smithy models repository +AWS_MODELS_REPO="https://github.com/aws/aws-sdk-go-v2.git" +AWS_MODELS_BRANCH="main" + +# Working directory for generation +WORK_DIR="${PROJECT_ROOT}/.build/codegen-work" + +# Services and their required operations +declare -A SERVICE_OPERATIONS +SERVICE_OPERATIONS=( + ["Lambda"]="CreateFunction,UpdateFunctionCode,DeleteFunction,GetFunction,CreateFunctionUrlConfig,GetFunctionUrlConfig,DeleteFunctionUrlConfig,AddPermission,RemovePermission" + ["IAM"]="CreateRole,DeleteRole,AttachRolePolicy,DetachRolePolicy,GetRole,PutRolePolicy,DeleteRolePolicy" + ["S3"]="CreateBucket,HeadBucket,PutObject,DeleteObject" + ["STS"]="GetCallerIdentity" +) + +# Map service names to their Smithy model directory names in aws-sdk-go-v2 +declare -A SERVICE_MODEL_DIRS +SERVICE_MODEL_DIRS=( + ["Lambda"]="lambda" + ["IAM"]="iam" + ["S3"]="s3" + ["STS"]="sts" +) + +# --------------------------------------------------------------------------- +# Helper functions +# --------------------------------------------------------------------------- + +check_prerequisites() { + log "Checking prerequisites..." + + if ! command -v swift &> /dev/null; then + fatal "Swift toolchain not found. Please install Swift." + fi + + if ! command -v git &> /dev/null; then + fatal "Git not found. Please install git." + fi + + log "Prerequisites satisfied." +} + +setup_work_dir() { + log "Setting up working directory at ${WORK_DIR}..." + rm -rf "${WORK_DIR}" + mkdir -p "${WORK_DIR}" +} + +clone_codegen() { + log "Cloning Soto Code Generator..." + if [ -d "${WORK_DIR}/soto-codegenerator" ]; then + log "Soto Code Generator already cloned, pulling latest..." + git -C "${WORK_DIR}/soto-codegenerator" pull --quiet + else + git clone --quiet --depth 1 --branch "${SOTO_CODEGEN_BRANCH}" \ + "${SOTO_CODEGEN_REPO}" "${WORK_DIR}/soto-codegenerator" + fi + log "Soto Code Generator ready." +} + +download_models() { + log "Downloading AWS service model files..." + + local models_dir="${WORK_DIR}/aws-models" + mkdir -p "${models_dir}" + + # Clone aws-sdk-go-v2 sparsely to get only the service model directories we need + if [ ! -d "${WORK_DIR}/aws-sdk-go-v2" ]; then + git clone --quiet --depth 1 --filter=blob:none --sparse \ + --branch "${AWS_MODELS_BRANCH}" \ + "${AWS_MODELS_REPO}" "${WORK_DIR}/aws-sdk-go-v2" + + pushd "${WORK_DIR}/aws-sdk-go-v2" > /dev/null + local sparse_paths="" + for service in "${!SERVICE_MODEL_DIRS[@]}"; do + sparse_paths="${sparse_paths} codegen/sdk-codegen/aws-models/${SERVICE_MODEL_DIRS[$service]}" + done + # shellcheck disable=SC2086 + git sparse-checkout set ${sparse_paths} + popd > /dev/null + fi + + # Copy model files to our working models directory + for service in "${!SERVICE_MODEL_DIRS[@]}"; do + local model_dir_name="${SERVICE_MODEL_DIRS[$service]}" + local src_dir="${WORK_DIR}/aws-sdk-go-v2/codegen/sdk-codegen/aws-models/${model_dir_name}" + if [ -d "${src_dir}" ]; then + cp -r "${src_dir}" "${models_dir}/" + log " Copied model for ${service} (${model_dir_name})" + else + # Try alternative model locations + local alt_src="${WORK_DIR}/aws-sdk-go-v2/codegen/sdk-codegen/aws-models" + local smithy_file + smithy_file=$(find "${alt_src}" -name "${model_dir_name}.json" -o -name "${model_dir_name}.smithy" 2>/dev/null | head -1) + if [ -n "${smithy_file}" ]; then + mkdir -p "${models_dir}/${model_dir_name}" + cp "${smithy_file}" "${models_dir}/${model_dir_name}/" + log " Copied model file for ${service}" + else + fatal "Could not find Smithy model for ${service} (looked in ${src_dir})" + fi + fi + done + + log "AWS service models ready." +} + +generate_config() { + log "Generating code generator configuration..." + + local config_file="${WORK_DIR}/codegen-config.json" + + # Build the configuration JSON with only the operations we need + cat > "${config_file}" << 'CONFIGEOF' +{ + "services": { + "Lambda": { + "operations": [ + "CreateFunction", + "UpdateFunctionCode", + "DeleteFunction", + "GetFunction", + "CreateFunctionUrlConfig", + "GetFunctionUrlConfig", + "DeleteFunctionUrlConfig", + "AddPermission", + "RemovePermission" + ] + }, + "IAM": { + "operations": [ + "CreateRole", + "DeleteRole", + "AttachRolePolicy", + "DetachRolePolicy", + "GetRole", + "PutRolePolicy", + "DeleteRolePolicy" + ] + }, + "S3": { + "operations": [ + "CreateBucket", + "HeadBucket", + "PutObject", + "DeleteObject" + ] + }, + "STS": { + "operations": [ + "GetCallerIdentity" + ] + } + } +} +CONFIGEOF + + log "Configuration written to ${config_file}" +} + +run_codegen() { + log "Building Soto Code Generator..." + pushd "${WORK_DIR}/soto-codegenerator" > /dev/null + swift build --configuration release 2>&1 | tail -5 + popd > /dev/null + + log "Running code generation for each service..." + + local codegen_bin="${WORK_DIR}/soto-codegenerator/.build/release/soto-codegenerator" + local models_dir="${WORK_DIR}/aws-models" + local generated_dir="${WORK_DIR}/generated" + mkdir -p "${generated_dir}" + + # If the code generator binary doesn't exist, try the default executable name + if [ ! -f "${codegen_bin}" ]; then + codegen_bin=$(find "${WORK_DIR}/soto-codegenerator/.build/release" -type f -perm +111 -name "*codegen*" | head -1) + if [ -z "${codegen_bin}" ]; then + # Fall back to running via swift run + log "Using 'swift run' to invoke the code generator..." + codegen_bin="SWIFT_RUN" + fi + fi + + for service in "${!SERVICE_MODEL_DIRS[@]}"; do + local model_dir_name="${SERVICE_MODEL_DIRS[$service]}" + local model_path="${models_dir}/${model_dir_name}" + local service_output="${generated_dir}/${service}" + mkdir -p "${service_output}" + + log " Generating ${service} client..." + + local operations="${SERVICE_OPERATIONS[$service]}" + + if [ "${codegen_bin}" = "SWIFT_RUN" ]; then + pushd "${WORK_DIR}/soto-codegenerator" > /dev/null + swift run soto-codegenerator \ + --model-path "${model_path}" \ + --output-path "${service_output}" \ + --operations "${operations}" \ + --module "${service}" \ + 2>&1 || log " Warning: Code generation for ${service} returned non-zero (may need manual review)" + popd > /dev/null + else + "${codegen_bin}" \ + --model-path "${model_path}" \ + --output-path "${service_output}" \ + --operations "${operations}" \ + --module "${service}" \ + 2>&1 || log " Warning: Code generation for ${service} returned non-zero (may need manual review)" + fi + done + + log "Code generation complete." +} + +copy_output() { + log "Copying generated clients to ${OUTPUT_DIR}..." + + local generated_dir="${WORK_DIR}/generated" + + # Clean previous generated output + rm -rf "${OUTPUT_DIR}" + mkdir -p "${OUTPUT_DIR}" + + for service in "${!SERVICE_MODEL_DIRS[@]}"; do + local service_dir="${generated_dir}/${service}" + local dest_dir="${OUTPUT_DIR}/${service}" + + if [ -d "${service_dir}" ] && [ "$(ls -A "${service_dir}" 2>/dev/null)" ]; then + mkdir -p "${dest_dir}" + cp -r "${service_dir}/"* "${dest_dir}/" + log " Copied ${service} → ${dest_dir}" + else + log " Warning: No generated files found for ${service} in ${service_dir}" + log " You may need to create the client files manually." + fi + done + + log "Generated clients installed at ${OUTPUT_DIR}" +} + +cleanup() { + log "Cleaning up working directory..." + rm -rf "${WORK_DIR}" + log "Cleanup complete." +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +main() { + log "==========================================" + log "AWS Service Client Generation Script" + log "==========================================" + log "" + log "This script generates lightweight AWS service clients" + log "for the Lambda deploy plugin using the Soto Code Generator." + log "" + log "Services: Lambda, IAM, S3, STS" + log "Output: ${OUTPUT_DIR}" + log "" + + check_prerequisites + setup_work_dir + clone_codegen + download_models + generate_config + run_codegen + copy_output + + # Uncomment the following line to clean up after successful generation: + # cleanup + + log "" + log "==========================================" + log "Generation complete!" + log "==========================================" + log "" + log "Generated files are at:" + log " ${OUTPUT_DIR}" + log "" + log "Next steps:" + log " 1. Review the generated files" + log " 2. Run 'swift build' to verify compilation" + log " 3. Commit the generated files to the repository" + log "" + log "Note: If the code generator did not produce the expected output," + log "you may need to adjust the model paths or write the client files" + log "manually based on the Soto client patterns." + log "" +} + +main "$@" diff --git a/scripts/integration-test.sh b/scripts/integration-test.sh new file mode 100755 index 000000000..b3eda6975 --- /dev/null +++ b/scripts/integration-test.sh @@ -0,0 +1,320 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright SwiftAWSLambdaRuntime project authors +## Copyright (c) Amazon.com, Inc. or its affiliates. +## 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 +## +##===----------------------------------------------------------------------===## + +# ============================================================================= +# integration-test.sh +# +# End-to-end integration test for the Lambda v4 plugin system. +# Exercises the full lifecycle: scaffold → build → deploy → validate → delete. +# +# Prerequisites: +# - AWS credentials configured (via aws configure or environment variables) +# - Docker installed and running +# - Swift toolchain installed +# - curl with --aws-sigv4 support (curl 7.75+) +# +# Usage: +# ./scripts/integration-test.sh +# ============================================================================= + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +FUNCTION_NAME="swift-lambda-e2e-test-$(date +%s)" +CLEANUP_NEEDED=false +WORK_DIR="" + +# --------------------------------------------------------------------------- +# Logging +# --------------------------------------------------------------------------- + +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { error "$@"; exit 1; } + +# --------------------------------------------------------------------------- +# Cleanup (guaranteed via trap) +# --------------------------------------------------------------------------- + +cleanup() { + local exit_code=$? + + if [ "$CLEANUP_NEEDED" = true ]; then + log "Cleaning up AWS resources for function: ${FUNCTION_NAME}..." + ( + cd "${WORK_DIR}" && \ + swift package --allow-network-connections all:443 \ + lambda-deploy --allow-writing-to-package-directory \ + --delete --products "${FUNCTION_NAME}" 2>&1 + ) || log "Warning: cleanup of AWS resources may have been incomplete." + fi + + if [ -n "${WORK_DIR}" ] && [ -d "${WORK_DIR}" ]; then + log "Removing temporary directory: ${WORK_DIR}" + rm -rf "${WORK_DIR}" + fi + + if [ $exit_code -ne 0 ]; then + error "Integration test FAILED (exit code: ${exit_code})" + fi + + exit $exit_code +} + +trap cleanup EXIT + +# --------------------------------------------------------------------------- +# Prerequisites check +# --------------------------------------------------------------------------- + +check_prerequisites() { + log "Checking prerequisites..." + + if ! command -v swift &> /dev/null; then + fatal "Swift toolchain not found. Please install Swift." + fi + + if ! command -v docker &> /dev/null; then + fatal "Docker not found. Please install Docker." + fi + + if ! command -v curl &> /dev/null; then + fatal "curl not found. Please install curl." + fi + + if ! command -v aws &> /dev/null; then + fatal "AWS CLI not found. Please install and configure the AWS CLI." + fi + + # Verify AWS credentials are available + if ! aws sts get-caller-identity &> /dev/null; then + fatal "AWS credentials not configured or invalid. Run 'aws configure' or set environment variables." + fi + + log "All prerequisites satisfied." +} + +# --------------------------------------------------------------------------- +# Step 1: Create temporary project directory and initialize Swift package +# --------------------------------------------------------------------------- + +scaffold_project() { + log "Step 1: Creating temporary project directory..." + + WORK_DIR=$(mktemp -d -t "swift-lambda-e2e-XXXXXX") + log " Working directory: ${WORK_DIR}" + + cd "${WORK_DIR}" + + # Initialize a Swift package with the function name as the executable target + swift package init --type executable --name "${FUNCTION_NAME}" + + # Add the 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 "${FUNCTION_NAME}" --package swift-aws-lambda-runtime + + # Also add AWSLambdaEvents for the URL template + swift package add-dependency https://github.com/swift-server/swift-aws-lambda-events.git --branch main + swift package add-target-dependency AWSLambdaEvents "${FUNCTION_NAME}" --package swift-aws-lambda-events + + log " Swift package initialized." +} + +# --------------------------------------------------------------------------- +# Step 2: Scaffold the Lambda function using lambda-init --with-url +# --------------------------------------------------------------------------- + +scaffold_function() { + log "Step 2: Scaffolding Lambda function with URL template..." + + cd "${WORK_DIR}" + swift package --allow-writing-to-package-directory lambda-init --with-url + + log " Function scaffolded with URL template." +} + +# --------------------------------------------------------------------------- +# Step 3: Build and package the Lambda function +# --------------------------------------------------------------------------- + +build_function() { + log "Step 3: Building and packaging the Lambda function..." + + cd "${WORK_DIR}" + swift package --allow-network-connections docker lambda-build --products "${FUNCTION_NAME}" + + log " Build and packaging complete." +} + +# --------------------------------------------------------------------------- +# Step 4: Deploy the Lambda function with Function URL +# --------------------------------------------------------------------------- + +deploy_function() { + log "Step 4: Deploying Lambda function with Function URL..." + + cd "${WORK_DIR}" + + # Capture deploy output to extract the Function URL + DEPLOY_OUTPUT=$(swift package --allow-network-connections all:443 \ + lambda-deploy --allow-writing-to-package-directory \ + --with-url --products "${FUNCTION_NAME}" 2>&1) || { + error "Deployment failed." + echo "${DEPLOY_OUTPUT}" >&2 + exit 1 + } + + # Mark cleanup as needed now that resources are deployed + CLEANUP_NEEDED=true + + echo "${DEPLOY_OUTPUT}" >&2 + + log " Deployment complete." +} + +# --------------------------------------------------------------------------- +# Step 5: Extract Function URL from deploy output +# --------------------------------------------------------------------------- + +extract_function_url() { + log "Step 5: Extracting Function URL from deploy output..." + + # The deploy plugin outputs the Function URL — extract it + FUNCTION_URL=$(echo "${DEPLOY_OUTPUT}" | grep -oE 'https://[a-z0-9]+\.lambda-url\.[a-z0-9-]+\.on\.aws/?' | head -1) + + if [ -z "${FUNCTION_URL}" ]; then + fatal "Could not extract Function URL from deploy output." + fi + + log " Function URL: ${FUNCTION_URL}" +} + +# --------------------------------------------------------------------------- +# Step 6: Validate the deployed function via curl with AWS SigV4 +# --------------------------------------------------------------------------- + +validate_function() { + log "Step 6: Validating deployed function via Function URL..." + + # Resolve the AWS region for signing + local region + region=$(aws configure get region 2>/dev/null || echo "${AWS_REGION:-${AWS_DEFAULT_REGION:-us-east-1}}") + + # Wait for the function to become active (cold start may take a moment) + log " Waiting for function to become active..." + local max_retries=30 + local retry_count=0 + local response="" + + while [ $retry_count -lt $max_retries ]; do + # Use curl with AWS SigV4 to call the Function URL + response=$(curl --silent --show-error --max-time 30 \ + --aws-sigv4 "aws:amz:${region}:lambda" \ + --user "${AWS_ACCESS_KEY_ID:-$(aws configure get aws_access_key_id)}:${AWS_SECRET_ACCESS_KEY:-$(aws configure get aws_secret_access_key)}" \ + ${AWS_SESSION_TOKEN:+-H "x-amz-security-token: ${AWS_SESSION_TOKEN}"} \ + "${FUNCTION_URL}?name=World" 2>&1) || true + + # Check if we got a valid response (not a 5xx or connection error) + if echo "${response}" | grep -q '"message"'; then + break + fi + + retry_count=$((retry_count + 1)) + if [ $retry_count -lt $max_retries ]; then + log " Attempt ${retry_count}/${max_retries} - waiting 10 seconds..." + sleep 10 + fi + done + + if [ $retry_count -ge $max_retries ]; then + error "Function did not return a valid response after ${max_retries} attempts." + error "Last response: ${response}" + exit 1 + fi + + log " Response received: ${response}" + + RESPONSE_BODY="${response}" +} + +# --------------------------------------------------------------------------- +# Step 7: Verify response matches expected output +# --------------------------------------------------------------------------- + +verify_response() { + log "Step 7: Verifying response body..." + + local expected_message="Hello World" + + if echo "${RESPONSE_BODY}" | grep -q "${expected_message}"; then + log " Response verification PASSED: contains '${expected_message}'" + else + error "Response verification FAILED." + error " Expected response to contain: '${expected_message}'" + error " Actual response: '${RESPONSE_BODY}'" + exit 1 + fi +} + +# --------------------------------------------------------------------------- +# Step 8: Delete the deployed function and associated resources +# --------------------------------------------------------------------------- + +delete_function() { + log "Step 8: Deleting Lambda function and associated resources..." + + cd "${WORK_DIR}" + swift package --allow-network-connections all:443 \ + lambda-deploy --allow-writing-to-package-directory \ + --delete --products "${FUNCTION_NAME}" + + CLEANUP_NEEDED=false + + log " Function and resources deleted." +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +main() { + log "==========================================" + log "Lambda Plugin End-to-End Integration Test" + log "==========================================" + log "" + log "Function name: ${FUNCTION_NAME}" + log "" + + check_prerequisites + scaffold_project + scaffold_function + build_function + deploy_function + extract_function_url + validate_function + verify_response + delete_function + + log "" + log "==========================================" + log "Integration test PASSED" + log "==========================================" +} + +main "$@" From 534cd29b0f3fc89ccd3b05dca04e5c1082af9604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Thu, 4 Jun 2026 13:01:25 +0200 Subject: [PATCH 23/27] remove .kiro/ from license check --- .github/workflows/pull_request.yml | 1 + .licenseignore | 3 +- Examples/_MyFirstFunction/create_function.sh | 2 +- .../AWSLambdaPluginHelper/Extensions.swift | 2 - .../GeneratedClients/IAM/IAMClient.swift | 35 +++++--- .../GeneratedClients/IAM/IAMErrors.swift | 7 +- .../GeneratedClients/IAM/IAMShapes.swift | 7 +- .../Lambda/LambdaClient.swift | 50 +++++++++--- .../Lambda/LambdaErrors.swift | 7 +- .../Lambda/LambdaShapes.swift | 7 +- .../GeneratedClients/S3/S3Client.swift | 22 +++-- .../GeneratedClients/S3/S3Errors.swift | 7 +- .../GeneratedClients/S3/S3Shapes.swift | 7 +- .../lambda-build/Builder.swift | 3 +- .../lambda-deploy/Deployer.swift | 80 ++++++++++++++----- .../Placeholder.swift | 19 ++++- .../PropertyTests.swift | 7 +- api-breakage-allowlist.txt | 33 ++++++++ 18 files changed, 222 insertions(+), 77 deletions(-) create mode 100644 api-breakage-allowlist.txt diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index e98ecc48c..3460e0738 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -16,6 +16,7 @@ jobs: shell_check_enabled: true python_lint_check_enabled: true api_breakage_check_container_image: "swift:6.3-noble" + api_breakage_check_allowlist_path: "api-breakage-allowlist.txt" docs_check_container_image: "swift:6.3-noble" format_check_container_image: "swift:6.3-noble" yamllint_check_enabled: true diff --git a/.licenseignore b/.licenseignore index fd35b5d6f..34fce131e 100644 --- a/.licenseignore +++ b/.licenseignore @@ -36,4 +36,5 @@ Package.resolved **/.npmignore **/*.json **/*.txt -*.toml \ No newline at end of file +*.toml +.kiro/* diff --git a/Examples/_MyFirstFunction/create_function.sh b/Examples/_MyFirstFunction/create_function.sh index 7165f8e8c..bea1173e9 100755 --- a/Examples/_MyFirstFunction/create_function.sh +++ b/Examples/_MyFirstFunction/create_function.sh @@ -60,7 +60,7 @@ sleep 5 echo "🔗 Invoke the Lambda function" aws lambda invoke \ --function-name MyLambda \ - --payload $(echo '{"name":"World","age":30}' | base64) \ + --payload "$(echo '{"name":"World","age":30}' | base64)" \ /tmp/out.json > /dev/null && cat /tmp/out.json echo "" diff --git a/Sources/AWSLambdaPluginHelper/Extensions.swift b/Sources/AWSLambdaPluginHelper/Extensions.swift index 877d9752d..f5c5aa829 100644 --- a/Sources/AWSLambdaPluginHelper/Extensions.swift +++ b/Sources/AWSLambdaPluginHelper/Extensions.swift @@ -36,5 +36,3 @@ extension String { Array(self.utf8) } } - - diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMClient.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMClient.swift index 3712c3997..cf428dbc5 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMClient.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMClient.swift @@ -1,12 +1,13 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the Swift AWS Lambda Runtime open source project +// This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the Swift AWS Lambda Runtime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift AWS Lambda Runtime project authors +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors // // SPDX-License-Identifier: Apache-2.0 // @@ -65,7 +66,10 @@ public struct IAMClient: AWSService { /// - Parameter input: The request parameters. /// - Returns: The newly created role. @discardableResult - public func createRole(_ input: IAMCreateRoleRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> IAMCreateRoleResponse { + public func createRole( + _ input: IAMCreateRoleRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> IAMCreateRoleResponse { try await self.client.execute( operation: "CreateRole", path: "/", @@ -93,7 +97,10 @@ public struct IAMClient: AWSService { /// - Parameter input: The request parameters. /// - Returns: The role details. @discardableResult - public func getRole(_ input: IAMGetRoleRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> IAMGetRoleResponse { + public func getRole( + _ input: IAMGetRoleRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> IAMGetRoleResponse { try await self.client.execute( operation: "GetRole", path: "/", @@ -106,7 +113,10 @@ public struct IAMClient: AWSService { /// Attaches the specified managed policy to the specified IAM role. /// - Parameter input: The request parameters. - public func attachRolePolicy(_ input: IAMAttachRolePolicyRequest, logger: Logger = AWSClient.loggingDisabled) async throws { + public func attachRolePolicy( + _ input: IAMAttachRolePolicyRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws { try await self.client.execute( operation: "AttachRolePolicy", path: "/", @@ -119,7 +129,10 @@ public struct IAMClient: AWSService { /// Removes the specified managed policy from the specified IAM role. /// - Parameter input: The request parameters. - public func detachRolePolicy(_ input: IAMDetachRolePolicyRequest, logger: Logger = AWSClient.loggingDisabled) async throws { + public func detachRolePolicy( + _ input: IAMDetachRolePolicyRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws { try await self.client.execute( operation: "DetachRolePolicy", path: "/", @@ -132,7 +145,8 @@ public struct IAMClient: AWSService { /// Adds or updates an inline policy document that is embedded in the specified IAM role. /// - Parameter input: The request parameters. - public func putRolePolicy(_ input: IAMPutRolePolicyRequest, logger: Logger = AWSClient.loggingDisabled) async throws { + public func putRolePolicy(_ input: IAMPutRolePolicyRequest, logger: Logger = AWSClient.loggingDisabled) async throws + { try await self.client.execute( operation: "PutRolePolicy", path: "/", @@ -145,7 +159,10 @@ public struct IAMClient: AWSService { /// Deletes the specified inline policy from the specified IAM role. /// - Parameter input: The request parameters. - public func deleteRolePolicy(_ input: IAMDeleteRolePolicyRequest, logger: Logger = AWSClient.loggingDisabled) async throws { + public func deleteRolePolicy( + _ input: IAMDeleteRolePolicyRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws { try await self.client.execute( operation: "DeleteRolePolicy", path: "/", diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMErrors.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMErrors.swift index edb9ba906..fcb3d8fe1 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMErrors.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMErrors.swift @@ -1,12 +1,13 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the Swift AWS Lambda Runtime open source project +// This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the Swift AWS Lambda Runtime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift AWS Lambda Runtime project authors +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors // // SPDX-License-Identifier: Apache-2.0 // diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMShapes.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMShapes.swift index 48f1a8393..86681deb3 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMShapes.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMShapes.swift @@ -1,12 +1,13 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the Swift AWS Lambda Runtime open source project +// This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the Swift AWS Lambda Runtime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift AWS Lambda Runtime project authors +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors // // SPDX-License-Identifier: Apache-2.0 // diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaClient.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaClient.swift index e80eaa2f4..58ad910a5 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaClient.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaClient.swift @@ -1,12 +1,13 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the Swift AWS Lambda Runtime open source project +// This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the Swift AWS Lambda Runtime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift AWS Lambda Runtime project authors +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors // // SPDX-License-Identifier: Apache-2.0 // @@ -60,7 +61,10 @@ public struct LambdaClient: AWSService, Sendable { /// - Parameter input: The request parameters. /// - Returns: The function configuration and code location. @discardableResult - public func getFunction(_ input: GetFunctionRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> GetFunctionResponse { + public func getFunction( + _ input: GetFunctionRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> GetFunctionResponse { try await self.client.execute( operation: "GetFunction", path: "/2015-03-31/functions/{FunctionName}", @@ -75,7 +79,10 @@ public struct LambdaClient: AWSService, Sendable { /// - Parameter input: The request parameters. /// - Returns: The function configuration. @discardableResult - public func createFunction(_ input: CreateFunctionRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> CreateFunctionResponse { + public func createFunction( + _ input: CreateFunctionRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> CreateFunctionResponse { try await self.client.execute( operation: "CreateFunction", path: "/2015-03-31/functions", @@ -90,7 +97,10 @@ public struct LambdaClient: AWSService, Sendable { /// - Parameter input: The request parameters. /// - Returns: The updated function configuration. @discardableResult - public func updateFunctionCode(_ input: UpdateFunctionCodeRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> UpdateFunctionCodeResponse { + public func updateFunctionCode( + _ input: UpdateFunctionCodeRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> UpdateFunctionCodeResponse { try await self.client.execute( operation: "UpdateFunctionCode", path: "/2015-03-31/functions/{FunctionName}/code", @@ -103,7 +113,8 @@ public struct LambdaClient: AWSService, Sendable { /// Deletes a Lambda function. /// - Parameter input: The request parameters. - public func deleteFunction(_ input: DeleteFunctionRequest, logger: Logger = AWSClient.loggingDisabled) async throws { + public func deleteFunction(_ input: DeleteFunctionRequest, logger: Logger = AWSClient.loggingDisabled) async throws + { try await self.client.execute( operation: "DeleteFunction", path: "/2015-03-31/functions/{FunctionName}", @@ -118,7 +129,10 @@ public struct LambdaClient: AWSService, Sendable { /// - Parameter input: The request parameters. /// - Returns: The function URL configuration. @discardableResult - public func createFunctionUrlConfig(_ input: CreateFunctionUrlConfigRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> CreateFunctionUrlConfigResponse { + public func createFunctionUrlConfig( + _ input: CreateFunctionUrlConfigRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> CreateFunctionUrlConfigResponse { try await self.client.execute( operation: "CreateFunctionUrlConfig", path: "/2021-10-31/functions/{FunctionName}/url", @@ -131,7 +145,10 @@ public struct LambdaClient: AWSService, Sendable { /// Deletes a Lambda function URL. /// - Parameter input: The request parameters. - public func deleteFunctionUrlConfig(_ input: DeleteFunctionUrlConfigRequest, logger: Logger = AWSClient.loggingDisabled) async throws { + public func deleteFunctionUrlConfig( + _ input: DeleteFunctionUrlConfigRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws { try await self.client.execute( operation: "DeleteFunctionUrlConfig", path: "/2021-10-31/functions/{FunctionName}/url", @@ -146,7 +163,10 @@ public struct LambdaClient: AWSService, Sendable { /// - Parameter input: The request parameters. /// - Returns: The Function URL configuration including the URL endpoint. @discardableResult - public func getFunctionUrlConfig(_ input: GetFunctionUrlConfigRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> GetFunctionUrlConfigResponse { + public func getFunctionUrlConfig( + _ input: GetFunctionUrlConfigRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> GetFunctionUrlConfigResponse { try await self.client.execute( operation: "GetFunctionUrlConfig", path: "/2021-10-31/functions/{FunctionName}/url", @@ -161,7 +181,10 @@ public struct LambdaClient: AWSService, Sendable { /// - Parameter input: The request parameters. /// - Returns: The permission statement added to the function policy. @discardableResult - public func addPermission(_ input: AddPermissionRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> AddPermissionResponse { + public func addPermission( + _ input: AddPermissionRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> AddPermissionResponse { try await self.client.execute( operation: "AddPermission", path: "/2015-03-31/functions/{FunctionName}/policy", @@ -174,7 +197,10 @@ public struct LambdaClient: AWSService, Sendable { /// Revokes function-use permission from an AWS service or another AWS account. /// - Parameter input: The request parameters. - public func removePermission(_ input: RemovePermissionRequest, logger: Logger = AWSClient.loggingDisabled) async throws { + public func removePermission( + _ input: RemovePermissionRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws { try await self.client.execute( operation: "RemovePermission", path: "/2015-03-31/functions/{FunctionName}/policy/{StatementId}", diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaErrors.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaErrors.swift index 397446a58..b0e227ae2 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaErrors.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaErrors.swift @@ -1,12 +1,13 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the Swift AWS Lambda Runtime open source project +// This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the Swift AWS Lambda Runtime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift AWS Lambda Runtime project authors +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors // // SPDX-License-Identifier: Apache-2.0 // diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaShapes.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaShapes.swift index 35f7eacda..f65cf96d1 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaShapes.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaShapes.swift @@ -1,12 +1,13 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the Swift AWS Lambda Runtime open source project +// This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the Swift AWS Lambda Runtime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift AWS Lambda Runtime project authors +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors // // SPDX-License-Identifier: Apache-2.0 // diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Client.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Client.swift index 2a2f30f95..ca6300c46 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Client.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Client.swift @@ -1,12 +1,13 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the Swift AWS Lambda Runtime open source project +// This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the Swift AWS Lambda Runtime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift AWS Lambda Runtime project authors +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors // // SPDX-License-Identifier: Apache-2.0 // @@ -61,7 +62,10 @@ struct S3Client: AWSService { /// - Parameter input: The request parameters. /// - Returns: The response containing the bucket location. @discardableResult - func createBucket(_ input: CreateBucketRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> CreateBucketResponse { + func createBucket( + _ input: CreateBucketRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> CreateBucketResponse { try await self.client.execute( operation: "CreateBucket", path: "/{Bucket}", @@ -98,7 +102,10 @@ struct S3Client: AWSService { /// - Parameter input: The request parameters including the object body data. /// - Returns: The response containing ETag and version information. @discardableResult - func putObject(_ input: PutObjectRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> PutObjectResponse { + func putObject( + _ input: PutObjectRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> PutObjectResponse { try await self.client.execute( operation: "PutObject", path: "/{Bucket}/{Key+}", @@ -116,7 +123,10 @@ struct S3Client: AWSService { /// - Parameter input: The request parameters. /// - Returns: The response containing delete marker and version information. @discardableResult - func deleteObject(_ input: DeleteObjectRequest, logger: Logger = AWSClient.loggingDisabled) async throws -> DeleteObjectResponse { + func deleteObject( + _ input: DeleteObjectRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> DeleteObjectResponse { try await self.client.execute( operation: "DeleteObject", path: "/{Bucket}/{Key+}", diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Errors.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Errors.swift index 678fbde65..e6cce18fc 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Errors.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Errors.swift @@ -1,12 +1,13 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the Swift AWS Lambda Runtime open source project +// This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the Swift AWS Lambda Runtime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift AWS Lambda Runtime project authors +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors // // SPDX-License-Identifier: Apache-2.0 // diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Shapes.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Shapes.swift index 57de5bbae..356eebb7e 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Shapes.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Shapes.swift @@ -1,12 +1,13 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the Swift AWS Lambda Runtime open source project +// This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2024 Apple Inc. and the Swift AWS Lambda Runtime project authors +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift AWS Lambda Runtime project authors +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors // // SPDX-License-Identifier: Apache-2.0 // diff --git a/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift b/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift index 116cd5123..c0e44ecd8 100644 --- a/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift @@ -596,7 +596,8 @@ struct BuilderConfiguration: CustomStringConvertible { // detect when user explicitly provides an AL2 (not AL2023) base image if let explicitImage = baseDockerImageArgument.first { - self.explicitAL2Image = explicitImage.contains("amazonlinux2") + self.explicitAL2Image = + explicitImage.contains("amazonlinux2") && !explicitImage.contains("amazonlinux2023") } else { self.explicitAL2Image = false diff --git a/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift b/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift index 9b978a5a1..b1012e301 100644 --- a/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift @@ -12,15 +12,15 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// +import Logging +import SotoCore + #if canImport(FoundationEssentials) import FoundationEssentials #else import Foundation #endif -import Logging -import SotoCore - @available(LambdaSwift 2.0, *) enum DeploymentAction: Equatable { /// Create a new function (function does not exist yet). @@ -274,9 +274,13 @@ struct Deployer { if verbose { if zipData != nil { let sizeMB = Double(zipData!.count) / (1024 * 1024) - print("[verbose] Creating Lambda function '\(name)' with direct upload (\(String(format: "%.1f", sizeMB)) MB)...") + print( + "[verbose] Creating Lambda function '\(name)' with direct upload (\(String(format: "%.1f", sizeMB)) MB)..." + ) } else { - print("[verbose] Creating Lambda function '\(name)' with S3 reference s3://\(bucket ?? "")/\(key ?? "")...") + print( + "[verbose] Creating Lambda function '\(name)' with S3 reference s3://\(bucket ?? "")/\(key ?? "")..." + ) } } @@ -341,9 +345,13 @@ struct Deployer { if verbose { if zipData != nil { let sizeMB = Double(zipData!.count) / (1024 * 1024) - print("[verbose] Updating function code for '\(name)' with direct upload (\(String(format: "%.1f", sizeMB)) MB)...") + print( + "[verbose] Updating function code for '\(name)' with direct upload (\(String(format: "%.1f", sizeMB)) MB)..." + ) } else { - print("[verbose] Updating function code for '\(name)' with S3 reference s3://\(bucket ?? "")/\(key ?? "")...") + print( + "[verbose] Updating function code for '\(name)' with S3 reference s3://\(bucket ?? "")/\(key ?? "")..." + ) } } @@ -511,11 +519,13 @@ struct Deployer { /// This allows `lambda-deploy` to auto-detect the need for `--with-url`. private func detectFunctionURLUsage() -> Bool { let sourcesDir = URL(fileURLWithPath: "Sources") - guard let enumerator = FileManager.default.enumerator( - at: sourcesDir, - includingPropertiesForKeys: nil, - options: [.skipsHiddenFiles] - ) else { + guard + let enumerator = FileManager.default.enumerator( + at: sourcesDir, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) + else { return false } @@ -658,8 +668,14 @@ struct Deployer { // Default build output path. // Check both the current Builder plugin path and the legacy Packager plugin path. // The legacy AWSLambdaPackager path can be removed when the archive plugin is retired. - let builderPath = URL(fileURLWithPath: ".build/plugins/AWSLambdaBuilder/outputs/AWSLambdaPackager/\(functionName)/\(functionName).zip") - let packagerPath = URL(fileURLWithPath: ".build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/\(functionName)/\(functionName).zip") + let builderPath = URL( + fileURLWithPath: + ".build/plugins/AWSLambdaBuilder/outputs/AWSLambdaPackager/\(functionName)/\(functionName).zip" + ) + let packagerPath = URL( + fileURLWithPath: + ".build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/\(functionName)/\(functionName).zip" + ) if FileManager.default.fileExists(atPath: builderPath.path) { archiveURL = builderPath @@ -680,7 +696,9 @@ struct Deployer { if configuration.verboseLogging { let sizeMB = Double(archiveSize) / (1024 * 1024) print("[verbose] Archive: \(archiveURL.path) (\(String(format: "%.1f", sizeMB)) MB)") - print("[verbose] Upload strategy: \(Self.shouldUploadDirectly(archiveSize: archiveSize) ? "direct" : "S3 staging")") + print( + "[verbose] Upload strategy: \(Self.shouldUploadDirectly(archiveSize: archiveSize) ? "direct" : "S3 staging")" + ) } // Determine upload strategy @@ -692,8 +710,19 @@ struct Deployer { print("Archive exceeds 50 MB, staging to S3...") let bucketName = Self.deploymentBucketName(region: region.rawValue, accountId: accountId) s3Key = "\(functionName)/\(functionName).zip" - try await ensureBucketExists(bucket: bucketName, region: region, using: s3Client, verbose: configuration.verboseLogging) - try await uploadToS3(bucket: bucketName, key: s3Key!, data: zipData, using: s3Client, verbose: configuration.verboseLogging) + try await ensureBucketExists( + bucket: bucketName, + region: region, + using: s3Client, + verbose: configuration.verboseLogging + ) + try await uploadToS3( + bucket: bucketName, + key: s3Key!, + data: zipData, + using: s3Client, + verbose: configuration.verboseLogging + ) s3Bucket = bucketName } @@ -758,7 +787,12 @@ struct Deployer { // Clean up S3 staged object if let bucket = s3Bucket, let key = s3Key { - try await deleteFromS3(bucket: bucket, key: key, using: s3Client, verbose: configuration.verboseLogging) + try await deleteFromS3( + bucket: bucket, + key: key, + using: s3Client, + verbose: configuration.verboseLogging + ) } // Set up Function URL if requested (or auto-detected from source code) @@ -796,7 +830,8 @@ struct Deployer { // Report success reportDeploymentSuccess( functionName: functionName, - functionArn: functionArn ?? "arn:aws:lambda:\(region.rawValue):\(accountId):function:\(functionName)", + functionArn: functionArn + ?? "arn:aws:lambda:\(region.rawValue):\(accountId):function:\(functionName)", region: region.rawValue, functionURL: functionURL ) @@ -883,7 +918,8 @@ struct Deployer { roleName: roleName, assumeRolePolicyDocument: Self.lambdaTrustPolicy, path: "/", - description: "Execution role for Lambda function '\(functionName)' created by swift-aws-lambda-runtime deploy plugin" + description: + "Execution role for Lambda function '\(functionName)' created by swift-aws-lambda-runtime deploy plugin" ) let createRoleResponse: IAMCreateRoleResponse @@ -1081,7 +1117,9 @@ struct Deployer { print("") print("Invoke your function with:") print("") - print(" (eval $(aws configure export-credentials --format env) && curl --aws-sigv4 \"aws:amz:\(region):lambda\" --user \"$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY\" -H \"x-amz-security-token: $AWS_SESSION_TOKEN\" \"\(functionURL)?name=World\" )") + print( + " (eval $(aws configure export-credentials --format env) && curl --aws-sigv4 \"aws:amz:\(region):lambda\" --user \"$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY\" -H \"x-amz-security-token: $AWS_SESSION_TOKEN\" \"\(functionURL)?name=World\" )" + ) } else { print("") print("Invoke your function with:") diff --git a/Tests/AWSLambdaPluginHelperTests/Placeholder.swift b/Tests/AWSLambdaPluginHelperTests/Placeholder.swift index 4954eb87a..e808efbbb 100644 --- a/Tests/AWSLambdaPluginHelperTests/Placeholder.swift +++ b/Tests/AWSLambdaPluginHelperTests/Placeholder.swift @@ -1,3 +1,16 @@ -// Placeholder file to keep the test target valid. -// Tests for the vendored crypto/signer code were removed when the vendored code -// was replaced by soto-core (task 5). New unit and property tests will be added in task 11. +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// 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 +// +//===----------------------------------------------------------------------===// + +// Placeholder file to keep the test target valid when no other test files are present. diff --git a/Tests/AWSLambdaPluginHelperTests/PropertyTests.swift b/Tests/AWSLambdaPluginHelperTests/PropertyTests.swift index f1e922f56..227168092 100644 --- a/Tests/AWSLambdaPluginHelperTests/PropertyTests.swift +++ b/Tests/AWSLambdaPluginHelperTests/PropertyTests.swift @@ -13,14 +13,15 @@ // //===----------------------------------------------------------------------===// +import Testing + +@testable import AWSLambdaPluginHelper + #if canImport(FoundationEssentials) import FoundationEssentials #else import Foundation #endif -import Testing - -@testable import AWSLambdaPluginHelper // MARK: - Property 2: Deprecated option alias equivalence diff --git a/api-breakage-allowlist.txt b/api-breakage-allowlist.txt new file mode 100644 index 000000000..2d7c49567 --- /dev/null +++ b/api-breakage-allowlist.txt @@ -0,0 +1,33 @@ +API breakage: struct LambdaJSONEventDecoder has been removed +API breakage: struct LambdaJSONOutputEncoder has been removed +API breakage: struct InvocationMetadata has been removed +API breakage: protocol LambdaRuntimeClientResponseStreamWriter has been removed +API breakage: protocol LambdaRuntimeClientProtocol has been removed +API breakage: struct Invocation has been removed +API breakage: enum Lambda has been removed +API breakage: struct LambdaClock has been removed +API breakage: struct ClientApplication has been removed +API breakage: struct ClientContext has been removed +API breakage: struct LambdaContext has been removed +API breakage: struct StreamingLambdaStatusAndHeadersResponse has been removed +API breakage: struct LambdaRuntimeError has been removed +API breakage: struct JSONLogHandler has been removed +API breakage: struct LoggingConfiguration has been removed +API breakage: class LambdaManagedRuntime has been removed +API breakage: struct ClosureHandlerSendable has been removed +API breakage: protocol StreamingLambdaHandler has been removed +API breakage: protocol LambdaResponseStreamWriter has been removed +API breakage: protocol LambdaHandler has been removed +API breakage: protocol LambdaWithBackgroundProcessingHandler has been removed +API breakage: protocol LambdaResponseWriter has been removed +API breakage: struct StreamingClosureHandler has been removed +API breakage: struct ClosureHandler has been removed +API breakage: protocol LambdaEventDecoder has been removed +API breakage: protocol LambdaOutputEncoder has been removed +API breakage: struct VoidEncoder has been removed +API breakage: struct LambdaHandlerAdapter has been removed +API breakage: struct LambdaCodableAdapter has been removed +API breakage: struct LambdaCodableResponseWriter has been removed +API breakage: class LambdaRuntime has been removed +API breakage: typealias _Lambda_SendableMetatype has been removed +API breakage: enum Version has been removed From a80721af8e4a10d3ae0c2420f621591ae746b294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Thu, 4 Jun 2026 19:19:59 +0200 Subject: [PATCH 24/27] fix integration test --- scripts/integration-test.sh | 95 +++++++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 15 deletions(-) diff --git a/scripts/integration-test.sh b/scripts/integration-test.sh index b3eda6975..a94311d22 100755 --- a/scripts/integration-test.sh +++ b/scripts/integration-test.sh @@ -37,8 +37,10 @@ set -euo pipefail # --------------------------------------------------------------------------- FUNCTION_NAME="swift-lambda-e2e-test-$(date +%s)" +AWS_REGION="us-east-1" CLEANUP_NEEDED=false WORK_DIR="" +FIXED_WORK_DIR="" # --------------------------------------------------------------------------- # Logging @@ -61,11 +63,11 @@ cleanup() { cd "${WORK_DIR}" && \ swift package --allow-network-connections all:443 \ lambda-deploy --allow-writing-to-package-directory \ - --delete --products "${FUNCTION_NAME}" 2>&1 + --region "${AWS_REGION}" --delete --products "${FUNCTION_NAME}" 2>&1 ) || log "Warning: cleanup of AWS resources may have been incomplete." fi - if [ -n "${WORK_DIR}" ] && [ -d "${WORK_DIR}" ]; then + if [ -n "${WORK_DIR}" ] && [ -d "${WORK_DIR}" ] && [ -z "${FIXED_WORK_DIR}" ]; then log "Removing temporary directory: ${WORK_DIR}" rm -rf "${WORK_DIR}" fi @@ -117,16 +119,32 @@ check_prerequisites() { scaffold_project() { log "Step 1: Creating temporary project directory..." - WORK_DIR=$(mktemp -d -t "swift-lambda-e2e-XXXXXX") - log " Working directory: ${WORK_DIR}" + if [ -n "${FIXED_WORK_DIR}" ]; then + WORK_DIR="${FIXED_WORK_DIR}" + mkdir -p "${WORK_DIR}" + log " Using fixed working directory: ${WORK_DIR}" + + # If Package.swift already exists, skip scaffolding + if [ -f "${WORK_DIR}/Package.swift" ]; then + log " Package.swift already exists, skipping scaffold." + cd "${WORK_DIR}" + return + fi + else + WORK_DIR=$(mktemp -d -t "swift-lambda-e2e-XXXXXX") + log " Working directory: ${WORK_DIR}" + fi cd "${WORK_DIR}" # Initialize a Swift package with the function name as the executable target swift package init --type executable --name "${FUNCTION_NAME}" + # Add macOS 15 platform requirement (needed by AWSLambdaRuntime) + sed -i '' 's/name: "'"${FUNCTION_NAME}"'",/name: "'"${FUNCTION_NAME}"'",\n platforms: [.macOS(.v15)],/' Package.swift + # Add the lambda runtime dependency - 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-runtime.git --branch sebsto/new-plugins swift package add-target-dependency AWSLambdaRuntime "${FUNCTION_NAME}" --package swift-aws-lambda-runtime # Also add AWSLambdaEvents for the URL template @@ -144,6 +162,13 @@ scaffold_function() { log "Step 2: Scaffolding Lambda function with URL template..." cd "${WORK_DIR}" + + # Skip if already scaffolded (for --work-dir reuse) + if [ -n "${FIXED_WORK_DIR}" ] && grep -q "LambdaRuntime" Sources/main.swift 2>/dev/null; then + log " Function already scaffolded, skipping." + return + fi + swift package --allow-writing-to-package-directory lambda-init --with-url log " Function scaffolded with URL template." @@ -157,6 +182,13 @@ build_function() { log "Step 3: Building and packaging the Lambda function..." cd "${WORK_DIR}" + + # Skip build if archive already exists (for --work-dir reuse) + if [ -n "${FIXED_WORK_DIR}" ] && [ -d ".build/plugins/AWSLambdaBuilder/outputs" ]; then + log " Build artifacts found, skipping build." + return + fi + swift package --allow-network-connections docker lambda-build --products "${FUNCTION_NAME}" log " Build and packaging complete." @@ -174,7 +206,7 @@ deploy_function() { # Capture deploy output to extract the Function URL DEPLOY_OUTPUT=$(swift package --allow-network-connections all:443 \ lambda-deploy --allow-writing-to-package-directory \ - --with-url --products "${FUNCTION_NAME}" 2>&1) || { + --region "${AWS_REGION}" --with-url --products "${FUNCTION_NAME}" 2>&1) || { error "Deployment failed." echo "${DEPLOY_OUTPUT}" >&2 exit 1 @@ -212,9 +244,25 @@ extract_function_url() { validate_function() { log "Step 6: Validating deployed function via Function URL..." - # Resolve the AWS region for signing - local region - region=$(aws configure get region 2>/dev/null || echo "${AWS_REGION:-${AWS_DEFAULT_REGION:-us-east-1}}") + # Use the hardcoded region for SigV4 signing + local region="${AWS_REGION}" + log " Region for SigV4 signing: ${region}" + + # Resolve AWS credentials for curl (supports SSO, assumed roles, config files, etc.) + log " Resolving AWS credentials for curl..." + eval "$(aws configure export-credentials --format env-no-export 2>/dev/null)" || \ + fatal "Could not resolve AWS credentials. Ensure 'aws configure export-credentials' works." + + local access_key_id="${AWS_ACCESS_KEY_ID:-}" + local secret_access_key="${AWS_SECRET_ACCESS_KEY:-}" + local session_token="${AWS_SESSION_TOKEN:-}" + + if [ -z "${access_key_id}" ] || [ -z "${secret_access_key}" ]; then + fatal "Could not resolve AWS credentials for curl signing." + fi + + log " AWS_ACCESS_KEY_ID: ${access_key_id:0:8}..." + log " AWS_SESSION_TOKEN: ${session_token:+present}" # Wait for the function to become active (cold start may take a moment) log " Waiting for function to become active..." @@ -224,14 +272,14 @@ validate_function() { while [ $retry_count -lt $max_retries ]; do # Use curl with AWS SigV4 to call the Function URL - response=$(curl --silent --show-error --max-time 30 \ + response=$(curl --silent --show-error --max-time 60 \ --aws-sigv4 "aws:amz:${region}:lambda" \ - --user "${AWS_ACCESS_KEY_ID:-$(aws configure get aws_access_key_id)}:${AWS_SECRET_ACCESS_KEY:-$(aws configure get aws_secret_access_key)}" \ - ${AWS_SESSION_TOKEN:+-H "x-amz-security-token: ${AWS_SESSION_TOKEN}"} \ + --user "${access_key_id}:${secret_access_key}" \ + ${session_token:+-H "x-amz-security-token: ${session_token}"} \ "${FUNCTION_URL}?name=World" 2>&1) || true - # Check if we got a valid response (not a 5xx or connection error) - if echo "${response}" | grep -q '"message"'; then + # Check if we got the expected successful response + if echo "${response}" | grep -q '"Hello'; then break fi @@ -282,7 +330,7 @@ delete_function() { cd "${WORK_DIR}" swift package --allow-network-connections all:443 \ lambda-deploy --allow-writing-to-package-directory \ - --delete --products "${FUNCTION_NAME}" + --region "${AWS_REGION}" --delete --products "${FUNCTION_NAME}" CLEANUP_NEEDED=false @@ -294,11 +342,28 @@ delete_function() { # --------------------------------------------------------------------------- main() { + # Parse arguments + while [ $# -gt 0 ]; do + case "$1" in + --work-dir) + FIXED_WORK_DIR="$2" + shift 2 + ;; + *) + fatal "Unknown argument: $1. Usage: $0 [--work-dir ]" + ;; + esac + done + log "==========================================" log "Lambda Plugin End-to-End Integration Test" log "==========================================" log "" log "Function name: ${FUNCTION_NAME}" + log "Region: ${AWS_REGION}" + if [ -n "${FIXED_WORK_DIR}" ]; then + log "Fixed work dir: ${FIXED_WORK_DIR} (temp dir will NOT be deleted)" + fi log "" check_prerequisites From 65800ffbcd7412a804764075ba9eae590c21b9c0 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Fri, 5 Jun 2026 09:56:00 +0200 Subject: [PATCH 25/27] add @available(LambdaSwift 2.0, *) annotations for soto-core compatibility soto-core removed its platforms: declaration and now uses @available annotations on its types. Add matching availability annotations to generated clients, test functions, and update the generation script to include them automatically on future runs. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/pull_request.yml | 1 - Package.swift | 9 +++-- .../GeneratedClients/IAM/IAMClient.swift | 1 + .../GeneratedClients/IAM/IAMErrors.swift | 1 + .../GeneratedClients/IAM/IAMShapes.swift | 10 ++++++ .../Lambda/LambdaClient.swift | 1 + .../Lambda/LambdaErrors.swift | 1 + .../Lambda/LambdaShapes.swift | 22 +++++++++++++ .../GeneratedClients/S3/S3Client.swift | 1 + .../GeneratedClients/S3/S3Errors.swift | 1 + .../GeneratedClients/S3/S3Shapes.swift | 8 +++++ .../GeneratedClients/STS/STSClient.swift | 1 + .../GeneratedClients/STS/STSErrors.swift | 1 + .../GeneratedClients/STS/STSShapes.swift | 2 ++ .../BuilderConfigurationTests.swift | 19 +++++++++++ .../DeployerConfigurationTests.swift | 25 ++++++++++++++ .../DeployerS3Tests.swift | 16 +++++++++ .../PropertyTests.swift | 26 +++++++++++---- api-breakage-allowlist.txt | 33 ------------------- scripts/generate-aws-clients.sh | 15 +++++++++ 20 files changed, 149 insertions(+), 45 deletions(-) delete mode 100644 api-breakage-allowlist.txt diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 3460e0738..e98ecc48c 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -16,7 +16,6 @@ jobs: shell_check_enabled: true python_lint_check_enabled: true api_breakage_check_container_image: "swift:6.3-noble" - api_breakage_check_allowlist_path: "api-breakage-allowlist.txt" docs_check_container_image: "swift:6.3-noble" format_check_container_image: "swift:6.3-noble" yamllint_check_enabled: true diff --git a/Package.swift b/Package.swift index 899b33ac2..8f92d10c9 100644 --- a/Package.swift +++ b/Package.swift @@ -11,10 +11,6 @@ let defaultSwiftSettings: [SwiftSetting] = let package = Package( name: "swift-aws-lambda-runtime", - // Required because soto-core declares platforms: [.macOS(.v10_15)] in its Package.swift. - // SwiftPM rejects executable targets whose platform minimum is below their dependencies'. - // Without this declaration the package defaults to macOS 10.13, causing a planning error. - platforms: [.macOS(.v15)], products: [ .library(name: "AWSLambdaRuntime", targets: ["AWSLambdaRuntime"]), @@ -58,7 +54,10 @@ let package = Package( .package(url: "https://github.com/apple/swift-log.git", from: "1.12.0"), .package(url: "https://github.com/apple/swift-collections.git", from: "1.5.0"), .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.11.0"), - .package(url: "https://github.com/soto-project/soto-core.git", from: "7.13.0"), + // .package(url: "https://github.com/soto-project/soto-core.git", from: "7.13.0"), + .package(url: "https://github.com/sebsto/soto-core.git", branch: "remove-platforms-use-availability-macro"), + // .package(name: "soto-core", path: "../../soto-core") + ], targets: [ .target( diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMClient.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMClient.swift index cf428dbc5..c4a788357 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMClient.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMClient.swift @@ -25,6 +25,7 @@ import SotoCore /// /// Operations use POST with `Action=&Version=2010-05-08` /// URL-encoded form body. Responses are XML. +@available(LambdaSwift 2.0, *) public struct IAMClient: AWSService { /// The underlying AWS client used for making requests. public let client: AWSClient diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMErrors.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMErrors.swift index fcb3d8fe1..2937b979d 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMErrors.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMErrors.swift @@ -18,6 +18,7 @@ import SotoCore /// Error type for AWS IAM service operations. +@available(LambdaSwift 2.0, *) public struct IAMErrorType: AWSErrorType { enum Code: String { case entityAlreadyExistsException = "EntityAlreadyExists" diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMShapes.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMShapes.swift index 86681deb3..3184c1c3f 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMShapes.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMShapes.swift @@ -20,6 +20,7 @@ import SotoCore // MARK: - Common Shapes /// Represents an IAM role. +@available(LambdaSwift 2.0, *) public struct IAMRole: AWSDecodableShape, Sendable { /// The friendly name that identifies the role. public let roleName: String? @@ -50,6 +51,7 @@ public struct IAMRole: AWSDecodableShape, Sendable { // MARK: - CreateRole /// Request for the CreateRole operation. +@available(LambdaSwift 2.0, *) public struct IAMCreateRoleRequest: AWSEncodableShape, Sendable { /// The name of the role to create. public let roleName: String @@ -81,6 +83,7 @@ public struct IAMCreateRoleRequest: AWSEncodableShape, Sendable { } /// Response for the CreateRole operation. +@available(LambdaSwift 2.0, *) public struct IAMCreateRoleResponse: AWSDecodableShape, Sendable { /// The role that was created. public let role: IAMRole? @@ -93,6 +96,7 @@ public struct IAMCreateRoleResponse: AWSDecodableShape, Sendable { // MARK: - DeleteRole /// Request for the DeleteRole operation. +@available(LambdaSwift 2.0, *) public struct IAMDeleteRoleRequest: AWSEncodableShape, Sendable { /// The name of the role to delete. public let roleName: String @@ -109,6 +113,7 @@ public struct IAMDeleteRoleRequest: AWSEncodableShape, Sendable { // MARK: - GetRole /// Request for the GetRole operation. +@available(LambdaSwift 2.0, *) public struct IAMGetRoleRequest: AWSEncodableShape, Sendable { /// The name of the IAM role to get information about. public let roleName: String @@ -123,6 +128,7 @@ public struct IAMGetRoleRequest: AWSEncodableShape, Sendable { } /// Response for the GetRole operation. +@available(LambdaSwift 2.0, *) public struct IAMGetRoleResponse: AWSDecodableShape, Sendable { /// The role details. public let role: IAMRole? @@ -135,6 +141,7 @@ public struct IAMGetRoleResponse: AWSDecodableShape, Sendable { // MARK: - AttachRolePolicy /// Request for the AttachRolePolicy operation. +@available(LambdaSwift 2.0, *) public struct IAMAttachRolePolicyRequest: AWSEncodableShape, Sendable { /// The name of the IAM role to attach the policy to. public let roleName: String @@ -155,6 +162,7 @@ public struct IAMAttachRolePolicyRequest: AWSEncodableShape, Sendable { // MARK: - DetachRolePolicy /// Request for the DetachRolePolicy operation. +@available(LambdaSwift 2.0, *) public struct IAMDetachRolePolicyRequest: AWSEncodableShape, Sendable { /// The name of the IAM role to detach the policy from. public let roleName: String @@ -175,6 +183,7 @@ public struct IAMDetachRolePolicyRequest: AWSEncodableShape, Sendable { // MARK: - PutRolePolicy /// Request for the PutRolePolicy operation. +@available(LambdaSwift 2.0, *) public struct IAMPutRolePolicyRequest: AWSEncodableShape, Sendable { /// The name of the role to associate the policy with. public let roleName: String @@ -199,6 +208,7 @@ public struct IAMPutRolePolicyRequest: AWSEncodableShape, Sendable { // MARK: - DeleteRolePolicy /// Request for the DeleteRolePolicy operation. +@available(LambdaSwift 2.0, *) public struct IAMDeleteRolePolicyRequest: AWSEncodableShape, Sendable { /// The name of the role the policy is associated with. public let roleName: String diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaClient.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaClient.swift index 58ad910a5..bc1195af4 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaClient.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaClient.swift @@ -22,6 +22,7 @@ import SotoCore /// AWS Lambda service client /// /// Provides operations for managing AWS Lambda functions. +@available(LambdaSwift 2.0, *) public struct LambdaClient: AWSService, Sendable { /// The underlying AWS client used for making requests. public let client: AWSClient diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaErrors.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaErrors.swift index b0e227ae2..6393b98a2 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaErrors.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaErrors.swift @@ -18,6 +18,7 @@ import SotoCore /// Error type for AWS Lambda service +@available(LambdaSwift 2.0, *) public struct LambdaErrorType: AWSErrorType { enum Code: String { case invalidParameterValueException = "InvalidParameterValueException" diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaShapes.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaShapes.swift index f65cf96d1..a1ed0f66d 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaShapes.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaShapes.swift @@ -20,24 +20,28 @@ // MARK: - Enums /// Lambda function architecture +@available(LambdaSwift 2.0, *) public enum LambdaArchitecture: String, Codable, Sendable { case x86_64 = "x86_64" case arm64 = "arm64" } /// Lambda function runtime +@available(LambdaSwift 2.0, *) public enum LambdaRuntime: String, Codable, Sendable { case providedAl2023 = "provided.al2023" case providedAl2 = "provided.al2" } /// Lambda function packaging type +@available(LambdaSwift 2.0, *) public enum LambdaPackageType: String, Codable, Sendable { case zip = "Zip" case image = "Image" } /// Function URL auth type +@available(LambdaSwift 2.0, *) public enum FunctionUrlAuthType: String, Codable, Sendable { case awsIam = "AWS_IAM" case none = "NONE" @@ -46,6 +50,7 @@ public enum FunctionUrlAuthType: String, Codable, Sendable { // MARK: - GetFunction /// Request for GetFunction operation +@available(LambdaSwift 2.0, *) public struct GetFunctionRequest: AWSEncodableShape, Sendable { /// The name of the Lambda function. public let functionName: String @@ -64,6 +69,7 @@ public struct GetFunctionRequest: AWSEncodableShape, Sendable { } /// Response for GetFunction operation +@available(LambdaSwift 2.0, *) public struct GetFunctionResponse: AWSDecodableShape, Sendable { /// The configuration of the function. public let configuration: FunctionConfiguration? @@ -77,6 +83,7 @@ public struct GetFunctionResponse: AWSDecodableShape, Sendable { } /// Function configuration details +@available(LambdaSwift 2.0, *) public struct FunctionConfiguration: Codable, Sendable { /// The name of the function. public let functionName: String? @@ -129,6 +136,7 @@ public struct FunctionConfiguration: Codable, Sendable { } /// Function code location details +@available(LambdaSwift 2.0, *) public struct FunctionCodeLocation: Codable, Sendable { /// The service that hosts the deployment package. public let repositoryType: String? @@ -144,6 +152,7 @@ public struct FunctionCodeLocation: Codable, Sendable { // MARK: - CreateFunction /// Function code for CreateFunction +@available(LambdaSwift 2.0, *) public struct FunctionCode: Codable, Sendable { /// The base64-encoded contents of the deployment package. public let zipFile: String? @@ -166,6 +175,7 @@ public struct FunctionCode: Codable, Sendable { } /// Request for CreateFunction operation +@available(LambdaSwift 2.0, *) public struct CreateFunctionRequest: AWSEncodableShape, Sendable { /// The name of the Lambda function. public let functionName: String @@ -227,6 +237,7 @@ public struct CreateFunctionRequest: AWSEncodableShape, Sendable { } /// Response for CreateFunction operation +@available(LambdaSwift 2.0, *) public struct CreateFunctionResponse: AWSDecodableShape, Sendable { /// The name of the function. public let functionName: String? @@ -266,6 +277,7 @@ public struct CreateFunctionResponse: AWSDecodableShape, Sendable { // MARK: - UpdateFunctionCode /// Request for UpdateFunctionCode operation +@available(LambdaSwift 2.0, *) public struct UpdateFunctionCodeRequest: AWSEncodableShape, Sendable { /// The name of the Lambda function. public let functionName: String @@ -311,6 +323,7 @@ public struct UpdateFunctionCodeRequest: AWSEncodableShape, Sendable { } /// Response for UpdateFunctionCode operation +@available(LambdaSwift 2.0, *) public struct UpdateFunctionCodeResponse: AWSDecodableShape, Sendable { /// The name of the function. public let functionName: String? @@ -347,6 +360,7 @@ public struct UpdateFunctionCodeResponse: AWSDecodableShape, Sendable { // MARK: - DeleteFunction /// Request for DeleteFunction operation +@available(LambdaSwift 2.0, *) public struct DeleteFunctionRequest: AWSEncodableShape, Sendable { /// The name of the Lambda function. public let functionName: String @@ -367,6 +381,7 @@ public struct DeleteFunctionRequest: AWSEncodableShape, Sendable { // MARK: - CreateFunctionUrlConfig /// Request for CreateFunctionUrlConfig operation +@available(LambdaSwift 2.0, *) public struct CreateFunctionUrlConfigRequest: AWSEncodableShape, Sendable { /// The name of the Lambda function. public let functionName: String @@ -391,6 +406,7 @@ public struct CreateFunctionUrlConfigRequest: AWSEncodableShape, Sendable { } /// Response for CreateFunctionUrlConfig operation +@available(LambdaSwift 2.0, *) public struct CreateFunctionUrlConfigResponse: AWSDecodableShape, Sendable { /// The HTTP URL endpoint for the function. public let functionUrl: String? @@ -412,6 +428,7 @@ public struct CreateFunctionUrlConfigResponse: AWSDecodableShape, Sendable { // MARK: - DeleteFunctionUrlConfig /// Request for DeleteFunctionUrlConfig operation +@available(LambdaSwift 2.0, *) public struct DeleteFunctionUrlConfigRequest: AWSEncodableShape, Sendable { /// The name of the Lambda function. public let functionName: String @@ -432,6 +449,7 @@ public struct DeleteFunctionUrlConfigRequest: AWSEncodableShape, Sendable { // MARK: - GetFunctionUrlConfig /// Request for GetFunctionUrlConfig operation +@available(LambdaSwift 2.0, *) public struct GetFunctionUrlConfigRequest: AWSEncodableShape, Sendable { /// The name of the Lambda function. public let functionName: String @@ -450,6 +468,7 @@ public struct GetFunctionUrlConfigRequest: AWSEncodableShape, Sendable { } /// Response for GetFunctionUrlConfig operation +@available(LambdaSwift 2.0, *) public struct GetFunctionUrlConfigResponse: AWSDecodableShape, Sendable { /// The HTTP URL endpoint for the function. public let functionUrl: String? @@ -465,6 +484,7 @@ public struct GetFunctionUrlConfigResponse: AWSDecodableShape, Sendable { // MARK: - AddPermission /// Request for AddPermission operation +@available(LambdaSwift 2.0, *) public struct AddPermissionRequest: AWSEncodableShape, Sendable { /// The name of the Lambda function. public let functionName: String @@ -541,6 +561,7 @@ public struct AddPermissionRequest: AWSEncodableShape, Sendable { } /// Response for AddPermission operation +@available(LambdaSwift 2.0, *) public struct AddPermissionResponse: AWSDecodableShape, Sendable { /// The permission statement that's added to the function policy. public let statement: String? @@ -553,6 +574,7 @@ public struct AddPermissionResponse: AWSDecodableShape, Sendable { // MARK: - RemovePermission /// Request for RemovePermission operation +@available(LambdaSwift 2.0, *) public struct RemovePermissionRequest: AWSEncodableShape, Sendable { /// The name of the Lambda function. public let functionName: String diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Client.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Client.swift index ca6300c46..f22b4c250 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Client.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Client.swift @@ -22,6 +22,7 @@ import SotoCore /// /// S3 uses the REST-XML protocol with path-style addressing. /// Endpoint: `https://s3..amazonaws.com` +@available(LambdaSwift 2.0, *) struct S3Client: AWSService { let client: AWSClient let config: AWSServiceConfig diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Errors.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Errors.swift index e6cce18fc..833727709 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Errors.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Errors.swift @@ -18,6 +18,7 @@ import SotoCore /// Error type for AWS S3 service +@available(LambdaSwift 2.0, *) struct S3ErrorType: AWSErrorType { enum Code: String { case bucketAlreadyExists = "BucketAlreadyExists" diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Shapes.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Shapes.swift index 356eebb7e..0d06b7417 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Shapes.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Shapes.swift @@ -20,6 +20,7 @@ // MARK: - CreateBucket /// Configuration for the bucket location constraint. +@available(LambdaSwift 2.0, *) struct CreateBucketConfiguration: AWSEncodableShape, Sendable { static let _xmlRootNodeName: String? = "CreateBucketConfiguration" static let _xmlNamespace: String? = "http://s3.amazonaws.com/doc/2006-03-01/" @@ -38,6 +39,7 @@ struct CreateBucketConfiguration: AWSEncodableShape, Sendable { } /// Request for CreateBucket operation. +@available(LambdaSwift 2.0, *) struct CreateBucketRequest: AWSEncodableShape, Sendable { /// The name of the bucket to create. let bucket: String @@ -63,6 +65,7 @@ struct CreateBucketRequest: AWSEncodableShape, Sendable { } /// Response for CreateBucket operation. +@available(LambdaSwift 2.0, *) struct CreateBucketResponse: AWSDecodableShape, Sendable { /// The URI that identifies the bucket. let location: String? @@ -76,6 +79,7 @@ struct CreateBucketResponse: AWSDecodableShape, Sendable { // MARK: - HeadBucket /// Request for HeadBucket operation. +@available(LambdaSwift 2.0, *) struct HeadBucketRequest: AWSEncodableShape, Sendable { /// The bucket name. let bucket: String @@ -96,6 +100,7 @@ struct HeadBucketRequest: AWSEncodableShape, Sendable { // MARK: - PutObject /// Request for PutObject operation. +@available(LambdaSwift 2.0, *) struct PutObjectRequest: AWSEncodableShape, Sendable { static let _options: AWSShapeOptions = [.allowStreaming] @@ -124,6 +129,7 @@ struct PutObjectRequest: AWSEncodableShape, Sendable { } /// Response for PutObject operation. +@available(LambdaSwift 2.0, *) struct PutObjectResponse: AWSDecodableShape, Sendable { /// Entity tag for the uploaded object. let eTag: String? @@ -140,6 +146,7 @@ struct PutObjectResponse: AWSDecodableShape, Sendable { // MARK: - DeleteObject /// Request for DeleteObject operation. +@available(LambdaSwift 2.0, *) struct DeleteObjectRequest: AWSEncodableShape, Sendable { /// The bucket name. let bucket: String @@ -168,6 +175,7 @@ struct DeleteObjectRequest: AWSEncodableShape, Sendable { } /// Response for DeleteObject operation. +@available(LambdaSwift 2.0, *) struct DeleteObjectResponse: AWSDecodableShape, Sendable { /// Indicates whether the specified object version that was permanently deleted /// was a delete marker. diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSClient.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSClient.swift index 5bda287e5..86455cbaf 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSClient.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSClient.swift @@ -26,6 +26,7 @@ import SotoCore /// /// Operations use POST with `Action=&Version=2011-06-15` /// URL-encoded form body. Responses are XML. +@available(LambdaSwift 2.0, *) struct STSClient: AWSService { let client: AWSClient let config: AWSServiceConfig diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSErrors.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSErrors.swift index 4e66c2692..7d2bffd22 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSErrors.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSErrors.swift @@ -20,6 +20,7 @@ import SotoCore /// Error type for STS service operations. +@available(LambdaSwift 2.0, *) struct STSErrorType: AWSErrorType { enum Code: String { case expiredTokenException = "ExpiredTokenException" diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSShapes.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSShapes.swift index 877e72b43..01a5a2cee 100644 --- a/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSShapes.swift +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSShapes.swift @@ -23,6 +23,7 @@ import SotoCore /// Request shape for the GetCallerIdentity operation. /// This operation requires no input parameters. +@available(LambdaSwift 2.0, *) struct STSGetCallerIdentityRequest: AWSEncodableShape { init() {} } @@ -30,6 +31,7 @@ struct STSGetCallerIdentityRequest: AWSEncodableShape { // MARK: - Output Shapes /// Response shape for the GetCallerIdentity operation. +@available(LambdaSwift 2.0, *) struct STSGetCallerIdentityResponse: AWSDecodableShape { /// The unique identifier of the calling entity. /// The exact value depends on the type of entity that is making the call. diff --git a/Tests/AWSLambdaPluginHelperTests/BuilderConfigurationTests.swift b/Tests/AWSLambdaPluginHelperTests/BuilderConfigurationTests.swift index 7ec33a767..9e54887f5 100644 --- a/Tests/AWSLambdaPluginHelperTests/BuilderConfigurationTests.swift +++ b/Tests/AWSLambdaPluginHelperTests/BuilderConfigurationTests.swift @@ -42,6 +42,7 @@ struct BuilderConfigurationTests { // MARK: - Cross-compile parsing (Requirement 2.7) + @available(LambdaSwift 2.0, *) @Test("--cross-compile with valid value 'docker'") func crossCompileDocker() throws { let args = defaultArgs() + ["--cross-compile", "docker"] @@ -49,6 +50,7 @@ struct BuilderConfigurationTests { #expect(config.crossCompileMethod == .docker) } + @available(LambdaSwift 2.0, *) @Test("--cross-compile with valid value 'container'") func crossCompileContainer() throws { let args = defaultArgs() + ["--cross-compile", "container"] @@ -56,6 +58,7 @@ struct BuilderConfigurationTests { #expect(config.crossCompileMethod == .container) } + @available(LambdaSwift 2.0, *) @Test("--cross-compile with 'swift-static-sdk' throws unsupported error") func crossCompileSwiftStaticSdk() throws { let args = defaultArgs() + ["--cross-compile", "swift-static-sdk"] @@ -64,6 +67,7 @@ struct BuilderConfigurationTests { } } + @available(LambdaSwift 2.0, *) @Test("--cross-compile with 'custom-sdk' throws unsupported error") func crossCompileCustomSdk() throws { let args = defaultArgs() + ["--cross-compile", "custom-sdk"] @@ -72,6 +76,7 @@ struct BuilderConfigurationTests { } } + @available(LambdaSwift 2.0, *) @Test("--cross-compile with invalid value throws error") func crossCompileInvalidValue() throws { let args = defaultArgs() + ["--cross-compile", "invalid-method"] @@ -80,6 +85,7 @@ struct BuilderConfigurationTests { } } + @available(LambdaSwift 2.0, *) @Test("--cross-compile defaults to docker when omitted") func crossCompileDefaultsToDocker() throws { let args = defaultArgs() @@ -89,6 +95,7 @@ struct BuilderConfigurationTests { // MARK: - No-strip flag (Requirements 2.5, 2.6) + @available(LambdaSwift 2.0, *) @Test("--no-strip flag is detected when present") func noStripFlagPresent() throws { let args = defaultArgs() + ["--no-strip"] @@ -96,6 +103,7 @@ struct BuilderConfigurationTests { #expect(config.noStrip == true) } + @available(LambdaSwift 2.0, *) @Test("--no-strip flag defaults to false when omitted") func noStripFlagAbsent() throws { let args = defaultArgs() @@ -105,6 +113,7 @@ struct BuilderConfigurationTests { // MARK: - Output directory deprecated alias (Requirement 7.5) + @available(LambdaSwift 2.0, *) @Test("--output-directory deprecated alias maps to outputDirectory") func outputDirectoryAlias() throws { let args: [String] = [ @@ -121,6 +130,7 @@ struct BuilderConfigurationTests { #expect(config.outputDirectory.path().hasSuffix("custom/output/path")) } + @available(LambdaSwift 2.0, *) @Test("--output-path takes precedence when both are provided") func outputPathTakesPrecedence() throws { let args: [String] = [ @@ -140,6 +150,7 @@ struct BuilderConfigurationTests { // MARK: - Mutual exclusion of --swift-version and --base-docker-image (Requirement 2.17) + @available(LambdaSwift 2.0, *) @Test("--swift-version and --base-docker-image together throws error") func mutualExclusionSwiftVersionAndBaseImage() throws { let args = defaultArgs() + ["--swift-version", "6.0", "--base-docker-image", "swift:6.0-amazonlinux2023"] @@ -148,6 +159,7 @@ struct BuilderConfigurationTests { } } + @available(LambdaSwift 2.0, *) @Test("--swift-version alone is accepted") func swiftVersionAlone() throws { let args = defaultArgs() + ["--swift-version", "6.0"] @@ -155,6 +167,7 @@ struct BuilderConfigurationTests { #expect(config.baseDockerImage == "swift:6.0-amazonlinux2023") } + @available(LambdaSwift 2.0, *) @Test("--base-docker-image alone is accepted") func baseDockerImageAlone() throws { let args = defaultArgs() + ["--base-docker-image", "swift:5.10-amazonlinux2023"] @@ -164,6 +177,7 @@ struct BuilderConfigurationTests { // MARK: - Default base image is amazonlinux2023 (Requirement 6.1) + @available(LambdaSwift 2.0, *) @Test("Default base image contains amazonlinux2023") func defaultBaseImageIsAL2023() throws { let args = defaultArgs() @@ -171,6 +185,7 @@ struct BuilderConfigurationTests { #expect(config.baseDockerImage.contains("amazonlinux2023")) } + @available(LambdaSwift 2.0, *) @Test("Default base image format without swift-version is swift:amazonlinux2023") func defaultBaseImageFormatNoVersion() throws { let args = defaultArgs() @@ -178,6 +193,7 @@ struct BuilderConfigurationTests { #expect(config.baseDockerImage == "swift:amazonlinux2023") } + @available(LambdaSwift 2.0, *) @Test("Base image with --swift-version includes version prefix") func baseImageWithSwiftVersion() throws { let args = defaultArgs() + ["--swift-version", "6.1"] @@ -187,6 +203,7 @@ struct BuilderConfigurationTests { // MARK: - Explicit AL2 image detection + @available(LambdaSwift 2.0, *) @Test("Explicit AL2 image is detected") func explicitAL2ImageDetected() throws { let args = defaultArgs() + ["--base-docker-image", "swift:5.10-amazonlinux2"] @@ -194,6 +211,7 @@ struct BuilderConfigurationTests { #expect(config.explicitAL2Image == true) } + @available(LambdaSwift 2.0, *) @Test("AL2023 image is not flagged as explicit AL2") func al2023ImageNotFlaggedAsAL2() throws { let args = defaultArgs() + ["--base-docker-image", "swift:6.0-amazonlinux2023"] @@ -201,6 +219,7 @@ struct BuilderConfigurationTests { #expect(config.explicitAL2Image == false) } + @available(LambdaSwift 2.0, *) @Test("Default image (no --base-docker-image) is not flagged as explicit AL2") func defaultImageNotFlaggedAsAL2() throws { let args = defaultArgs() diff --git a/Tests/AWSLambdaPluginHelperTests/DeployerConfigurationTests.swift b/Tests/AWSLambdaPluginHelperTests/DeployerConfigurationTests.swift index 61d84b6cf..f97946781 100644 --- a/Tests/AWSLambdaPluginHelperTests/DeployerConfigurationTests.swift +++ b/Tests/AWSLambdaPluginHelperTests/DeployerConfigurationTests.swift @@ -22,18 +22,21 @@ struct DeployerConfigurationTests { // MARK: - Architecture parsing (Requirement 3.14) + @available(LambdaSwift 2.0, *) @Test("Valid architecture x64 is parsed correctly") func architectureX64() throws { let config = try DeployerConfiguration(arguments: ["--architecture", "x64"]) #expect(config.architecture == .x64) } + @available(LambdaSwift 2.0, *) @Test("Valid architecture arm64 is parsed correctly") func architectureArm64() throws { let config = try DeployerConfiguration(arguments: ["--architecture", "arm64"]) #expect(config.architecture == .arm64) } + @available(LambdaSwift 2.0, *) @Test("Invalid architecture throws error") func invalidArchitectureThrows() throws { #expect(throws: DeployerErrors.self) { @@ -41,6 +44,7 @@ struct DeployerConfigurationTests { } } + @available(LambdaSwift 2.0, *) @Test("Invalid architecture value produces descriptive error") func invalidArchitectureMessage() throws { do { @@ -55,6 +59,7 @@ struct DeployerConfigurationTests { // MARK: - Default architecture matches host (Requirement 3.13) + @available(LambdaSwift 2.0, *) @Test("Default architecture matches host when --architecture is omitted") func defaultArchitectureMatchesHost() throws { let config = try DeployerConfiguration(arguments: []) @@ -69,18 +74,21 @@ struct DeployerConfigurationTests { // MARK: - Region parsing (Requirement 3.25) + @available(LambdaSwift 2.0, *) @Test("Region is parsed from arguments") func regionParsing() throws { let config = try DeployerConfiguration(arguments: ["--region", "eu-west-1"]) #expect(config.region == "eu-west-1") } + @available(LambdaSwift 2.0, *) @Test("Region is nil when not specified") func regionDefaultNil() throws { let config = try DeployerConfiguration(arguments: []) #expect(config.region == nil) } + @available(LambdaSwift 2.0, *) @Test("Region with equals syntax is parsed") func regionEqualsSyntax() throws { let config = try DeployerConfiguration(arguments: ["--region=us-west-2"]) @@ -89,6 +97,7 @@ struct DeployerConfigurationTests { // MARK: - IAM role parsing + @available(LambdaSwift 2.0, *) @Test("IAM role is parsed from arguments") func iamRoleParsing() throws { let roleArn = "arn:aws:iam::123456789012:role/my-role" @@ -96,6 +105,7 @@ struct DeployerConfigurationTests { #expect(config.iamRole == roleArn) } + @available(LambdaSwift 2.0, *) @Test("IAM role is nil when not specified") func iamRoleDefaultNil() throws { let config = try DeployerConfiguration(arguments: []) @@ -104,6 +114,7 @@ struct DeployerConfigurationTests { // MARK: - Input directory parsing + @available(LambdaSwift 2.0, *) @Test("Input directory is parsed from arguments") func inputDirectoryParsing() throws { let config = try DeployerConfiguration(arguments: ["--input-directory", "/tmp/build/output"]) @@ -111,6 +122,7 @@ struct DeployerConfigurationTests { #expect(config.inputDirectory?.path().contains("/tmp/build/output") == true) } + @available(LambdaSwift 2.0, *) @Test("Input directory is nil when not specified") func inputDirectoryDefaultNil() throws { let config = try DeployerConfiguration(arguments: []) @@ -119,12 +131,14 @@ struct DeployerConfigurationTests { // MARK: - With URL flag parsing + @available(LambdaSwift 2.0, *) @Test("--with-url flag is detected") func withURLFlag() throws { let config = try DeployerConfiguration(arguments: ["--with-url"]) #expect(config.withURL == true) } + @available(LambdaSwift 2.0, *) @Test("--with-url defaults to false") func withURLDefaultFalse() throws { let config = try DeployerConfiguration(arguments: []) @@ -133,12 +147,14 @@ struct DeployerConfigurationTests { // MARK: - Delete flag parsing + @available(LambdaSwift 2.0, *) @Test("--delete flag is detected") func deleteFlag() throws { let config = try DeployerConfiguration(arguments: ["--delete"]) #expect(config.delete == true) } + @available(LambdaSwift 2.0, *) @Test("--delete defaults to false") func deleteDefaultFalse() throws { let config = try DeployerConfiguration(arguments: []) @@ -147,12 +163,14 @@ struct DeployerConfigurationTests { // MARK: - Help flag (Requirement 3.25) + @available(LambdaSwift 2.0, *) @Test("--help flag is detected") func helpFlag() throws { let config = try DeployerConfiguration(arguments: ["--help"]) #expect(config.help == true) } + @available(LambdaSwift 2.0, *) @Test("--help defaults to false") func helpDefaultFalse() throws { let config = try DeployerConfiguration(arguments: []) @@ -161,12 +179,14 @@ struct DeployerConfigurationTests { // MARK: - Verbose flag + @available(LambdaSwift 2.0, *) @Test("--verbose flag is detected") func verboseFlag() throws { let config = try DeployerConfiguration(arguments: ["--verbose"]) #expect(config.verboseLogging == true) } + @available(LambdaSwift 2.0, *) @Test("--verbose defaults to false") func verboseDefaultFalse() throws { let config = try DeployerConfiguration(arguments: []) @@ -175,18 +195,21 @@ struct DeployerConfigurationTests { // MARK: - Products parsing + @available(LambdaSwift 2.0, *) @Test("Products are parsed from arguments") func productsParsing() throws { let config = try DeployerConfiguration(arguments: ["--products", "MyLambda"]) #expect(config.products == ["MyLambda"]) } + @available(LambdaSwift 2.0, *) @Test("Multiple comma-separated products are parsed") func multipleProductsParsing() throws { let config = try DeployerConfiguration(arguments: ["--products", "FuncA,FuncB,FuncC"]) #expect(config.products == ["FuncA", "FuncB", "FuncC"]) } + @available(LambdaSwift 2.0, *) @Test("Products default to empty array") func productsDefaultEmpty() throws { let config = try DeployerConfiguration(arguments: []) @@ -195,6 +218,7 @@ struct DeployerConfigurationTests { // MARK: - Combined arguments + @available(LambdaSwift 2.0, *) @Test("Multiple options parsed together") func combinedArguments() throws { let config = try DeployerConfiguration(arguments: [ @@ -215,6 +239,7 @@ struct DeployerConfigurationTests { #expect(config.products == ["MyFunc"]) } + @available(LambdaSwift 2.0, *) @Test("Delete with region and products") func deleteWithOptions() throws { let config = try DeployerConfiguration(arguments: [ diff --git a/Tests/AWSLambdaPluginHelperTests/DeployerS3Tests.swift b/Tests/AWSLambdaPluginHelperTests/DeployerS3Tests.swift index f18c32483..13a63a9d3 100644 --- a/Tests/AWSLambdaPluginHelperTests/DeployerS3Tests.swift +++ b/Tests/AWSLambdaPluginHelperTests/DeployerS3Tests.swift @@ -22,36 +22,42 @@ import Testing @Suite("Deployment bucket name construction") struct DeploymentBucketNameTests { + @available(LambdaSwift 2.0, *) @Test("Bucket name has correct format: swift-aws-lambda-runtime--") func bucketNameFormat() { let name = Deployer.deploymentBucketName(region: "us-east-1", accountId: "123456789012") #expect(name == "swift-aws-lambda-runtime-us-east-1-123456789012") } + @available(LambdaSwift 2.0, *) @Test("Bucket name with eu-west-1 region") func bucketNameEuWest1() { let name = Deployer.deploymentBucketName(region: "eu-west-1", accountId: "987654321098") #expect(name == "swift-aws-lambda-runtime-eu-west-1-987654321098") } + @available(LambdaSwift 2.0, *) @Test("Bucket name with ap-southeast-2 region") func bucketNameApSoutheast2() { let name = Deployer.deploymentBucketName(region: "ap-southeast-2", accountId: "111222333444") #expect(name == "swift-aws-lambda-runtime-ap-southeast-2-111222333444") } + @available(LambdaSwift 2.0, *) @Test("Bucket name with us-west-2 region and different account") func bucketNameUsWest2() { let name = Deployer.deploymentBucketName(region: "us-west-2", accountId: "000000000000") #expect(name == "swift-aws-lambda-runtime-us-west-2-000000000000") } + @available(LambdaSwift 2.0, *) @Test("Bucket name is always lowercase") func bucketNameIsLowercase() { let name = Deployer.deploymentBucketName(region: "us-east-1", accountId: "123456789012") #expect(name == name.lowercased()) } + @available(LambdaSwift 2.0, *) @Test( "Bucket name length is between 3 and 63 characters (valid S3 name)", arguments: [ @@ -68,6 +74,7 @@ struct DeploymentBucketNameTests { #expect(name.count <= 63, "Bucket name must be at most 63 characters") } + @available(LambdaSwift 2.0, *) @Test( "Bucket name contains only valid S3 characters (lowercase, digits, hyphens)", arguments: [ @@ -89,52 +96,61 @@ struct DeploymentBucketNameTests { @Suite("Archive size threshold and upload strategy") struct ArchiveSizeThresholdTests { + @available(LambdaSwift 2.0, *) @Test("directUploadLimit is exactly 50 MB") func directUploadLimitValue() { let expectedLimit: Int64 = 50 * 1024 * 1024 #expect(Deployer.directUploadLimit == expectedLimit) } + @available(LambdaSwift 2.0, *) @Test("Archive of exactly 50 MB should upload directly") func exactly50MBUploadsDirectly() { let fiftyMB: Int64 = 50 * 1024 * 1024 #expect(Deployer.shouldUploadDirectly(archiveSize: fiftyMB) == true) } + @available(LambdaSwift 2.0, *) @Test("Archive of 50 MB + 1 byte should use S3 staging") func fiftyMBPlusOneUsesS3() { let fiftyMBPlusOne: Int64 = 50 * 1024 * 1024 + 1 #expect(Deployer.shouldUploadDirectly(archiveSize: fiftyMBPlusOne) == false) } + @available(LambdaSwift 2.0, *) @Test("Archive of 0 bytes should upload directly") func zeroBytesUploadsDirectly() { #expect(Deployer.shouldUploadDirectly(archiveSize: 0) == true) } + @available(LambdaSwift 2.0, *) @Test("Archive of 1 byte should upload directly") func oneByteUploadsDirectly() { #expect(Deployer.shouldUploadDirectly(archiveSize: 1) == true) } + @available(LambdaSwift 2.0, *) @Test("Archive of 49 MB should upload directly") func fortyNineMBUploadsDirectly() { let fortyNineMB: Int64 = 49 * 1024 * 1024 #expect(Deployer.shouldUploadDirectly(archiveSize: fortyNineMB) == true) } + @available(LambdaSwift 2.0, *) @Test("Archive of 51 MB should use S3 staging") func fiftyOneMBUsesS3() { let fiftyOneMB: Int64 = 51 * 1024 * 1024 #expect(Deployer.shouldUploadDirectly(archiveSize: fiftyOneMB) == false) } + @available(LambdaSwift 2.0, *) @Test("Archive of 100 MB should use S3 staging") func hundredMBUsesS3() { let hundredMB: Int64 = 100 * 1024 * 1024 #expect(Deployer.shouldUploadDirectly(archiveSize: hundredMB) == false) } + @available(LambdaSwift 2.0, *) @Test("Archive of 250 MB (Lambda max) should use S3 staging") func lambdaMaxSizeUsesS3() { let lambdaMax: Int64 = 250 * 1024 * 1024 diff --git a/Tests/AWSLambdaPluginHelperTests/PropertyTests.swift b/Tests/AWSLambdaPluginHelperTests/PropertyTests.swift index 227168092..93bd0c41e 100644 --- a/Tests/AWSLambdaPluginHelperTests/PropertyTests.swift +++ b/Tests/AWSLambdaPluginHelperTests/PropertyTests.swift @@ -152,6 +152,7 @@ struct DeprecatedAliasEquivalencePropertyTests { ] } + @available(LambdaSwift 2.0, *) @Test("--output-directory produces same outputDirectory as --output-path", arguments: samplePaths) func deprecatedAliasEquivalence(path: String) throws { let argsWithOutputPath = baseArgs() + ["--output-path", path] @@ -177,13 +178,17 @@ struct DeprecatedAliasEquivalencePropertyTests { @Suite("Property 3: Cross-compile method parsing round-trip") struct CrossCompileMethodRoundTripPropertyTests { - static let allCases: [CrossCompileMethod] = [ - .docker, - .container, - .swiftStaticSdk, - .customSdk, - ] + @available(LambdaSwift 2.0, *) + static var allCases: [CrossCompileMethod] { + [ + .docker, + .container, + .swiftStaticSdk, + .customSdk, + ] + } + @available(LambdaSwift 2.0, *) @Test("rawValue → init(rawValue:) round-trips for all CrossCompileMethod cases", arguments: allCases) func rawValueRoundTrip(method: CrossCompileMethod) { let rawValue = method.rawValue @@ -245,6 +250,7 @@ struct MutualExclusionPropertyTests { ] } + @available(LambdaSwift 2.0, *) @Test( "Both --swift-version and --base-docker-image throws error", arguments: combinations @@ -298,6 +304,7 @@ struct DeploymentBucketNamePropertyTests { return Array(result.prefix(100)) }() + @available(LambdaSwift 2.0, *) @Test( "Bucket name matches expected format and is a valid S3 name", arguments: combinations @@ -385,6 +392,7 @@ struct ArchiveSizeUploadStrategyPropertyTests { return Array(Set(sizes).sorted().prefix(100)) }() + @available(LambdaSwift 2.0, *) @Test("Sizes at or below 50 MB should upload directly", arguments: sizesAtOrBelowLimit) func sizeAtOrBelowLimitUploadsDirect(size: Int64) { #expect( @@ -393,6 +401,7 @@ struct ArchiveSizeUploadStrategyPropertyTests { ) } + @available(LambdaSwift 2.0, *) @Test("Sizes above 50 MB should use S3 staging", arguments: sizesAboveLimit) func sizeAboveLimitUsesS3(size: Int64) { #expect( @@ -530,6 +539,7 @@ struct AL2WarningLogicPropertyTests { ] } + @available(LambdaSwift 2.0, *) @Test("AL2 images (not AL2023) set explicitAL2Image to true", arguments: al2Images) func al2ImageDetected(image: String) throws { let args = baseArgs() + ["--base-docker-image", image] @@ -540,6 +550,7 @@ struct AL2WarningLogicPropertyTests { ) } + @available(LambdaSwift 2.0, *) @Test("AL2023 or non-AL2 images set explicitAL2Image to false", arguments: nonAL2Images) func nonAL2ImageNotDetected(image: String) throws { let args = baseArgs() + ["--base-docker-image", image] @@ -550,6 +561,7 @@ struct AL2WarningLogicPropertyTests { ) } + @available(LambdaSwift 2.0, *) @Test("Default image (no --base-docker-image) sets explicitAL2Image to false") func defaultImageNotFlagged() throws { let args = baseArgs() @@ -574,6 +586,7 @@ struct UnsupportedCrossCompileMethodsPropertyTests { static let sdkGuideURL = "https://www.swift.org/documentation/articles/static-linux-getting-started.html" + @available(LambdaSwift 2.0, *) @Test("Unsupported methods throw error with SDK guide URL", arguments: unsupportedMethods) func unsupportedMethodThrowsWithLink(method: String) { do { @@ -588,6 +601,7 @@ struct UnsupportedCrossCompileMethodsPropertyTests { } } + @available(LambdaSwift 2.0, *) @Test("Unsupported methods via BuilderConfiguration throw error with SDK guide URL", arguments: unsupportedMethods) func unsupportedMethodInBuilderConfigThrowsWithLink(method: String) { let args: [String] = [ diff --git a/api-breakage-allowlist.txt b/api-breakage-allowlist.txt deleted file mode 100644 index 2d7c49567..000000000 --- a/api-breakage-allowlist.txt +++ /dev/null @@ -1,33 +0,0 @@ -API breakage: struct LambdaJSONEventDecoder has been removed -API breakage: struct LambdaJSONOutputEncoder has been removed -API breakage: struct InvocationMetadata has been removed -API breakage: protocol LambdaRuntimeClientResponseStreamWriter has been removed -API breakage: protocol LambdaRuntimeClientProtocol has been removed -API breakage: struct Invocation has been removed -API breakage: enum Lambda has been removed -API breakage: struct LambdaClock has been removed -API breakage: struct ClientApplication has been removed -API breakage: struct ClientContext has been removed -API breakage: struct LambdaContext has been removed -API breakage: struct StreamingLambdaStatusAndHeadersResponse has been removed -API breakage: struct LambdaRuntimeError has been removed -API breakage: struct JSONLogHandler has been removed -API breakage: struct LoggingConfiguration has been removed -API breakage: class LambdaManagedRuntime has been removed -API breakage: struct ClosureHandlerSendable has been removed -API breakage: protocol StreamingLambdaHandler has been removed -API breakage: protocol LambdaResponseStreamWriter has been removed -API breakage: protocol LambdaHandler has been removed -API breakage: protocol LambdaWithBackgroundProcessingHandler has been removed -API breakage: protocol LambdaResponseWriter has been removed -API breakage: struct StreamingClosureHandler has been removed -API breakage: struct ClosureHandler has been removed -API breakage: protocol LambdaEventDecoder has been removed -API breakage: protocol LambdaOutputEncoder has been removed -API breakage: struct VoidEncoder has been removed -API breakage: struct LambdaHandlerAdapter has been removed -API breakage: struct LambdaCodableAdapter has been removed -API breakage: struct LambdaCodableResponseWriter has been removed -API breakage: class LambdaRuntime has been removed -API breakage: typealias _Lambda_SendableMetatype has been removed -API breakage: enum Version has been removed diff --git a/scripts/generate-aws-clients.sh b/scripts/generate-aws-clients.sh index 302d26b86..c2e44b4a5 100755 --- a/scripts/generate-aws-clients.sh +++ b/scripts/generate-aws-clients.sh @@ -294,6 +294,20 @@ copy_output() { log "Generated clients installed at ${OUTPUT_DIR}" } +add_availability_annotations() { + log "Adding @available(LambdaSwift 2.0, *) annotations..." + + # Add @available(LambdaSwift 2.0, *) before every top-level struct/enum declaration + # in the generated files. This is required because soto-core uses availability + # annotations on its types (AWSClient, AWSServiceConfig, etc.) and this package + # does not declare a platforms: minimum. + find "${OUTPUT_DIR}" -name "*.swift" -print0 | while IFS= read -r -d '' file; do + perl -i -pe 's/^((?:public )?(?:struct|enum) \w+)/\@available(LambdaSwift 2.0, *)\n$1/' "$file" + done + + log "Availability annotations added." +} + cleanup() { log "Cleaning up working directory..." rm -rf "${WORK_DIR}" @@ -323,6 +337,7 @@ main() { generate_config run_codegen copy_output + add_availability_annotations # Uncomment the following line to clean up after successful generation: # cleanup From 637a4d94e76f1fc63ad107cd9aed12fd81fed0bf Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Fri, 5 Jun 2026 10:16:23 +0200 Subject: [PATCH 26/27] add --profile option to lambda-deploy for named AWS profile selection Uses SotoCore's .configFile(profile:) credential provider when --profile is specified, keeping the default credential chain otherwise. Co-Authored-By: Claude Opus 4.6 --- .../lambda-deploy/Deployer.swift | 28 ++++++++++++++++--- .../DeployerConfigurationTests.swift | 16 +++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift b/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift index b1012e301..2161c8191 100644 --- a/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift @@ -559,17 +559,28 @@ struct Deployer { // Check for AWS configuration files and emit non-blocking warning if absent self.checkAWSConfigurationFiles(verbose: configuration.verboseLogging) - // Initialize AWSClient with the default credential provider chain. - // Use a verbose logger when --verbose is set so SotoCore reports which - // credential provider is attempted and why it fails. + // Initialize AWSClient with the appropriate credential provider. + // When --profile is specified, use .configFile(profile:) to load + // credentials from the named profile in ~/.aws/credentials and ~/.aws/config. + // Otherwise, use the default credential provider chain. let clientLogger: Logger = { var logger = Logger(label: "AWSLambdaDeployer") logger.logLevel = configuration.verboseLogging ? .debug : .info return logger }() + let credentialProvider: CredentialProviderFactory + if let profile = configuration.profile { + if configuration.verboseLogging { + print("[verbose] Using AWS profile: \(profile)") + } + credentialProvider = .configFile(profile: profile) + } else { + credentialProvider = .default + } + let awsClient = AWSClient( - credentialProvider: .default, + credentialProvider: credentialProvider, logger: clientLogger ) @@ -1140,6 +1151,7 @@ struct Deployer { [--with-url] [--delete] [--region ] + [--profile ] [--iam-role ] [--input-directory ] [--architecture ] @@ -1154,6 +1166,8 @@ struct Deployer { Function URL (if any). --region The AWS region to deploy to. (default: resolved from AWS configuration) + --profile The named AWS profile to use for credentials and region. + (default: default credential provider chain) --iam-role The ARN of an existing IAM role for the Lambda function. (default: create a new role) --input-directory The path to the directory containing the deployment @@ -1176,6 +1190,7 @@ struct DeployerConfiguration: CustomStringConvertible { let withURL: Bool let delete: Bool let region: String? + let profile: String? let iamRole: String? let inputDirectory: URL? let architecture: Architecture @@ -1202,6 +1217,7 @@ struct DeployerConfiguration: CustomStringConvertible { let withURLArgument = argumentExtractor.extractFlag(named: "with-url") > 0 let deleteArgument = argumentExtractor.extractFlag(named: "delete") > 0 let regionArgument = argumentExtractor.extractOption(named: "region") + let profileArgument = argumentExtractor.extractOption(named: "profile") let iamRoleArgument = argumentExtractor.extractOption(named: "iam-role") let inputDirectoryArgument = argumentExtractor.extractOption(named: "input-directory") let architectureArgument = argumentExtractor.extractOption(named: "architecture") @@ -1222,6 +1238,9 @@ struct DeployerConfiguration: CustomStringConvertible { // AWS region (nil means Soto resolves it) self.region = regionArgument.first + // AWS profile from ~/.aws/config (nil means default credential chain) + self.profile = profileArgument.first + // IAM role ARN (nil means create a new role) self.iamRole = iamRoleArgument.first @@ -1253,6 +1272,7 @@ struct DeployerConfiguration: CustomStringConvertible { withURL: \(self.withURL) delete: \(self.delete) region: \(self.region ?? "") + profile: \(self.profile ?? "") iamRole: \(self.iamRole ?? "") inputDirectory: \(self.inputDirectory?.path() ?? "") architecture: \(self.architecture.rawValue) diff --git a/Tests/AWSLambdaPluginHelperTests/DeployerConfigurationTests.swift b/Tests/AWSLambdaPluginHelperTests/DeployerConfigurationTests.swift index f97946781..9ff2710ef 100644 --- a/Tests/AWSLambdaPluginHelperTests/DeployerConfigurationTests.swift +++ b/Tests/AWSLambdaPluginHelperTests/DeployerConfigurationTests.swift @@ -216,6 +216,22 @@ struct DeployerConfigurationTests { #expect(config.products.isEmpty) } + // MARK: - Profile parsing + + @available(LambdaSwift 2.0, *) + @Test("Profile is parsed from arguments") + func profileParsing() throws { + let config = try DeployerConfiguration(arguments: ["--profile", "staging"]) + #expect(config.profile == "staging") + } + + @available(LambdaSwift 2.0, *) + @Test("Profile is nil when not specified") + func profileDefaultNil() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.profile == nil) + } + // MARK: - Combined arguments @available(LambdaSwift 2.0, *) From 134ee8e837c52c1a7f9f0da12f2ac5371113156e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 5 Jun 2026 13:00:39 +0200 Subject: [PATCH 27/27] check sso and login when --profile is selected --- .../lambda-deploy/Deployer.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift b/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift index 2161c8191..6bf086b3e 100644 --- a/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift @@ -560,8 +560,10 @@ struct Deployer { self.checkAWSConfigurationFiles(verbose: configuration.verboseLogging) // Initialize AWSClient with the appropriate credential provider. - // When --profile is specified, use .configFile(profile:) to load - // credentials from the named profile in ~/.aws/credentials and ~/.aws/config. + // When --profile is specified, build a selector chain that passes the + // profile name to each provider that understands profiles (configFile, + // sso, login) — mirroring the structure of .default so profiles using + // login_session or sso_session resolve, not just static / assume-role. // Otherwise, use the default credential provider chain. let clientLogger: Logger = { var logger = Logger(label: "AWSLambdaDeployer") @@ -574,7 +576,11 @@ struct Deployer { if configuration.verboseLogging { print("[verbose] Using AWS profile: \(profile)") } - credentialProvider = .configFile(profile: profile) + credentialProvider = .selector( + .configFile(profile: profile), + .sso(profileName: profile), + .login(profileName: profile) + ) } else { credentialProvider = .default }