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