Files
CxIDE/Services/AWSLambdaService.swift
T
cx-git-agent 1af1db7ffc Add comprehensive AWS integration (S3, Route53, CloudFront, Lambda, ACM) with 72 new tests
- 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)
2026-04-21 19:40:51 -05:00

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]?
}