1af1db7ffc
- AWSService: SigV4 request signing, XML parser, credential management - AWSS3Service: bucket/object CRUD, website hosting, deploy pipeline - AWSRoute53Service: hosted zones, DNS records, S3/CloudFront aliases - AWSCloudFrontService: distributions, cache invalidation, ACM certs - AWSLambdaService: functions, invoke, zip deploy - AWSTools: 15 MCP agent tools for all AWS operations - CredentialStore: AWS credential convenience properties - AWSAndWebsiteTests: 72 tests covering models, XML parsing, errors, credentials, templates, deploy providers (165 total tests passing)
371 lines
12 KiB
Swift
371 lines
12 KiB
Swift
// AWSLambdaService.swift
|
|
// CxIDE — AWS Lambda serverless function management
|
|
//
|
|
// Create, deploy, invoke, and manage Lambda functions.
|
|
// Supports Node.js and Python runtimes with zip deployment.
|
|
|
|
import Foundation
|
|
|
|
// MARK: - Lambda Service
|
|
|
|
final class AWSLambdaService: @unchecked Sendable {
|
|
static let shared = AWSLambdaService()
|
|
|
|
private let aws: AWSService
|
|
|
|
init(aws: AWSService = .shared) {
|
|
self.aws = aws
|
|
}
|
|
|
|
// MARK: - Functions
|
|
|
|
/// List Lambda functions
|
|
func listFunctions(region: String? = nil) async throws -> [LambdaFunction] {
|
|
let (data, _) = try await aws.request(
|
|
service: "lambda",
|
|
region: region,
|
|
method: "GET",
|
|
path: "/2015-03-31/functions"
|
|
)
|
|
|
|
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
let functions = json["Functions"] as? [[String: Any]] else {
|
|
return []
|
|
}
|
|
|
|
return functions.compactMap { f -> LambdaFunction? in
|
|
guard let name = f["FunctionName"] as? String,
|
|
let arn = f["FunctionArn"] as? String else { return nil }
|
|
return LambdaFunction(
|
|
name: name,
|
|
arn: arn,
|
|
runtime: f["Runtime"] as? String ?? "unknown",
|
|
handler: f["Handler"] as? String ?? "",
|
|
memorySize: f["MemorySize"] as? Int ?? 128,
|
|
timeout: f["Timeout"] as? Int ?? 3,
|
|
lastModified: f["LastModified"] as? String,
|
|
codeSize: f["CodeSize"] as? Int ?? 0,
|
|
description: f["Description"] as? String
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Get function details
|
|
func getFunction(name: String, region: String? = nil) async throws -> LambdaFunction {
|
|
let (data, _) = try await aws.request(
|
|
service: "lambda",
|
|
region: region,
|
|
method: "GET",
|
|
path: "/2015-03-31/functions/\(name)"
|
|
)
|
|
|
|
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
let config = json["Configuration"] as? [String: Any],
|
|
let funcName = config["FunctionName"] as? String,
|
|
let arn = config["FunctionArn"] as? String else {
|
|
throw AWSError.functionNotFound(name)
|
|
}
|
|
|
|
return LambdaFunction(
|
|
name: funcName,
|
|
arn: arn,
|
|
runtime: config["Runtime"] as? String ?? "unknown",
|
|
handler: config["Handler"] as? String ?? "",
|
|
memorySize: config["MemorySize"] as? Int ?? 128,
|
|
timeout: config["Timeout"] as? Int ?? 3,
|
|
lastModified: config["LastModified"] as? String,
|
|
codeSize: config["CodeSize"] as? Int ?? 0,
|
|
description: config["Description"] as? String
|
|
)
|
|
}
|
|
|
|
/// Create a new Lambda function from a zip file
|
|
func createFunction(
|
|
name: String,
|
|
runtime: LambdaRuntime,
|
|
handler: String,
|
|
roleArn: String,
|
|
zipData: Data,
|
|
description: String = "",
|
|
memorySize: Int = 128,
|
|
timeout: Int = 30,
|
|
environment: [String: String] = [:],
|
|
region: String? = nil
|
|
) async throws -> LambdaFunction {
|
|
var requestBody: [String: Any] = [
|
|
"FunctionName": name,
|
|
"Runtime": runtime.rawValue,
|
|
"Handler": handler,
|
|
"Role": roleArn,
|
|
"Code": ["ZipFile": zipData.base64EncodedString()],
|
|
"Description": description,
|
|
"MemorySize": memorySize,
|
|
"Timeout": timeout,
|
|
]
|
|
|
|
if !environment.isEmpty {
|
|
requestBody["Environment"] = ["Variables": environment]
|
|
}
|
|
|
|
let bodyData = try JSONSerialization.data(withJSONObject: requestBody)
|
|
|
|
let (data, _) = try await aws.request(
|
|
service: "lambda",
|
|
region: region,
|
|
method: "POST",
|
|
path: "/2015-03-31/functions",
|
|
body: bodyData,
|
|
contentType: "application/json"
|
|
)
|
|
|
|
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
let funcName = json["FunctionName"] as? String,
|
|
let arn = json["FunctionArn"] as? String else {
|
|
throw AWSError.invalidXML("Failed to parse CreateFunction response")
|
|
}
|
|
|
|
return LambdaFunction(
|
|
name: funcName, arn: arn,
|
|
runtime: runtime.rawValue, handler: handler,
|
|
memorySize: memorySize, timeout: timeout,
|
|
lastModified: nil, codeSize: zipData.count,
|
|
description: description
|
|
)
|
|
}
|
|
|
|
/// Update function code with a new zip
|
|
func updateFunctionCode(name: String, zipData: Data, region: String? = nil) async throws {
|
|
let requestBody: [String: Any] = [
|
|
"ZipFile": zipData.base64EncodedString(),
|
|
]
|
|
let bodyData = try JSONSerialization.data(withJSONObject: requestBody)
|
|
|
|
_ = try await aws.request(
|
|
service: "lambda",
|
|
region: region,
|
|
method: "PUT",
|
|
path: "/2015-03-31/functions/\(name)/code",
|
|
body: bodyData,
|
|
contentType: "application/json"
|
|
)
|
|
}
|
|
|
|
/// Update function configuration
|
|
func updateFunctionConfig(
|
|
name: String,
|
|
memorySize: Int? = nil,
|
|
timeout: Int? = nil,
|
|
environment: [String: String]? = nil,
|
|
description: String? = nil,
|
|
region: String? = nil
|
|
) async throws {
|
|
var requestBody: [String: Any] = [:]
|
|
if let mem = memorySize { requestBody["MemorySize"] = mem }
|
|
if let t = timeout { requestBody["Timeout"] = t }
|
|
if let env = environment { requestBody["Environment"] = ["Variables": env] }
|
|
if let desc = description { requestBody["Description"] = desc }
|
|
|
|
let bodyData = try JSONSerialization.data(withJSONObject: requestBody)
|
|
|
|
_ = try await aws.request(
|
|
service: "lambda",
|
|
region: region,
|
|
method: "PUT",
|
|
path: "/2015-03-31/functions/\(name)/configuration",
|
|
body: bodyData,
|
|
contentType: "application/json"
|
|
)
|
|
}
|
|
|
|
/// Delete a Lambda function
|
|
func deleteFunction(name: String, region: String? = nil) async throws {
|
|
_ = try await aws.request(
|
|
service: "lambda",
|
|
region: region,
|
|
method: "DELETE",
|
|
path: "/2015-03-31/functions/\(name)"
|
|
)
|
|
}
|
|
|
|
/// Invoke a Lambda function
|
|
func invoke(
|
|
name: String,
|
|
payload: [String: Any]? = nil,
|
|
async invocationType: Bool = false,
|
|
region: String? = nil
|
|
) async throws -> LambdaInvokeResult {
|
|
let bodyData: Data?
|
|
if let payload = payload {
|
|
bodyData = try JSONSerialization.data(withJSONObject: payload)
|
|
} else {
|
|
bodyData = nil
|
|
}
|
|
|
|
var headers: [String: String] = [:]
|
|
if invocationType {
|
|
headers["X-Amz-Invocation-Type"] = "Event"
|
|
}
|
|
|
|
let (data, statusCode) = try await aws.request(
|
|
service: "lambda",
|
|
region: region,
|
|
method: "POST",
|
|
path: "/2015-03-31/functions/\(name)/invocations",
|
|
headers: headers,
|
|
body: bodyData,
|
|
contentType: "application/json"
|
|
)
|
|
|
|
let responsePayload = String(data: data, encoding: .utf8) ?? ""
|
|
let parsedPayload = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
|
|
return LambdaInvokeResult(
|
|
statusCode: statusCode,
|
|
payload: responsePayload,
|
|
parsedPayload: parsedPayload
|
|
)
|
|
}
|
|
|
|
// MARK: - Function URL (simplified API Gateway)
|
|
|
|
/// Create a function URL for direct HTTP access
|
|
func createFunctionURL(name: String, authType: String = "NONE", region: String? = nil) async throws -> String {
|
|
let requestBody: [String: Any] = [
|
|
"AuthType": authType,
|
|
"Cors": [
|
|
"AllowOrigins": ["*"],
|
|
"AllowMethods": ["*"],
|
|
"AllowHeaders": ["*"],
|
|
] as [String: Any],
|
|
]
|
|
let bodyData = try JSONSerialization.data(withJSONObject: requestBody)
|
|
|
|
let (data, _) = try await aws.request(
|
|
service: "lambda",
|
|
region: region,
|
|
method: "POST",
|
|
path: "/2021-10-31/functions/\(name)/url",
|
|
body: bodyData,
|
|
contentType: "application/json"
|
|
)
|
|
|
|
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
let url = json["FunctionUrl"] as? String else {
|
|
throw AWSError.invalidXML("Failed to parse CreateFunctionUrlConfig response")
|
|
}
|
|
return url
|
|
}
|
|
|
|
// MARK: - Deploy from Directory
|
|
|
|
/// Create a zip from a directory and deploy as a Lambda function
|
|
func deployFromDirectory(
|
|
directory: URL,
|
|
functionName: String,
|
|
runtime: LambdaRuntime,
|
|
handler: String,
|
|
roleArn: String,
|
|
region: String? = nil
|
|
) async throws -> LambdaFunction {
|
|
// Create a zip file from the directory
|
|
let zipData = try createZip(from: directory)
|
|
|
|
// Try to update existing function first
|
|
do {
|
|
_ = try await getFunction(name: functionName, region: region)
|
|
try await updateFunctionCode(name: functionName, zipData: zipData, region: region)
|
|
return try await getFunction(name: functionName, region: region)
|
|
} catch {
|
|
// Function doesn't exist, create it
|
|
return try await createFunction(
|
|
name: functionName,
|
|
runtime: runtime,
|
|
handler: handler,
|
|
roleArn: roleArn,
|
|
zipData: zipData,
|
|
region: region
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Create a zip archive from a directory using the zip CLI
|
|
private func createZip(from directory: URL) throws -> Data {
|
|
let tempZip = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent(UUID().uuidString + ".zip")
|
|
|
|
let process = Process()
|
|
process.executableURL = URL(fileURLWithPath: "/usr/bin/zip")
|
|
process.arguments = ["-r", "-9", tempZip.path, "."]
|
|
process.currentDirectoryURL = directory
|
|
|
|
let pipe = Pipe()
|
|
process.standardOutput = pipe
|
|
process.standardError = pipe
|
|
|
|
try process.run()
|
|
process.waitUntilExit()
|
|
|
|
guard process.terminationStatus == 0 else {
|
|
throw AWSError.deploymentFailed("Failed to create zip archive")
|
|
}
|
|
|
|
let data = try Data(contentsOf: tempZip)
|
|
try? FileManager.default.removeItem(at: tempZip)
|
|
return data
|
|
}
|
|
}
|
|
|
|
// MARK: - Lambda Models
|
|
|
|
enum LambdaRuntime: String, CaseIterable, Sendable {
|
|
case nodejs20 = "nodejs20.x"
|
|
case nodejs18 = "nodejs18.x"
|
|
case python312 = "python3.12"
|
|
case python311 = "python3.11"
|
|
case python310 = "python3.10"
|
|
case java21 = "java21"
|
|
case java17 = "java17"
|
|
case dotnet8 = "dotnet8"
|
|
case ruby33 = "ruby3.3"
|
|
case provided2023 = "provided.al2023"
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .nodejs20: return "Node.js 20.x"
|
|
case .nodejs18: return "Node.js 18.x"
|
|
case .python312: return "Python 3.12"
|
|
case .python311: return "Python 3.11"
|
|
case .python310: return "Python 3.10"
|
|
case .java21: return "Java 21"
|
|
case .java17: return "Java 17"
|
|
case .dotnet8: return ".NET 8"
|
|
case .ruby33: return "Ruby 3.3"
|
|
case .provided2023: return "Custom (AL2023)"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct LambdaFunction: Sendable {
|
|
let name: String
|
|
let arn: String
|
|
let runtime: String
|
|
let handler: String
|
|
let memorySize: Int
|
|
let timeout: Int
|
|
let lastModified: String?
|
|
let codeSize: Int
|
|
let description: String?
|
|
|
|
var formattedCodeSize: String {
|
|
if codeSize < 1024 { return "\(codeSize) B" }
|
|
else if codeSize < 1024 * 1024 { return String(format: "%.1f KB", Double(codeSize) / 1024) }
|
|
else { return String(format: "%.1f MB", Double(codeSize) / 1024 / 1024) }
|
|
}
|
|
}
|
|
|
|
struct LambdaInvokeResult: Sendable {
|
|
let statusCode: Int
|
|
let payload: String
|
|
let parsedPayload: [String: Any]?
|
|
}
|