WIP deploy plugin

This commit is contained in:
Sébastien Stormacq
2024-11-07 10:00:17 +01:00
parent 2b5405a87d
commit cc08a9a5c2
5 changed files with 565 additions and 0 deletions
+79
View File
@@ -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)
}
"""
}
}
+156
View File
@@ -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<Value>: @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
}
}
}
@@ -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"]
}
}
@@ -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<String> = [
"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<SHA256>.authenticationCode(for: Data(signingData.date.utf8), using: SymmetricKey(data: Array("AWS4\(credentials.secretAccessKey)".utf8)))
let kRegion = HMAC<SHA256>.authenticationCode(for: Data(region.utf8), using: SymmetricKey(data: kDate))
let kService = HMAC<SHA256>.authenticationCode(for: Data(name.utf8), using: SymmetricKey(data: kRegion))
let kSigning = HMAC<SHA256>.authenticationCode(for: Data("aws4_request".utf8), using: SymmetricKey(data: kService))
let kSignature = HMAC<SHA256>.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: "")
}
}
@@ -0,0 +1 @@
print("Deployer")