diff --git a/tools/build/build.zig.zon b/tools/build/build.zig.zon index 81cfe89..219c529 100644 --- a/tools/build/build.zig.zon +++ b/tools/build/build.zig.zon @@ -4,8 +4,8 @@ .fingerprint = 0x6e61de08e7e51114, .dependencies = .{ .aws = .{ - .url = "git+https://git.lerch.org/lobo/aws-sdk-for-zig#686b18d1f4329e80cf6d9b916eaa0c231333edb9", - .hash = "aws-0.0.1-SbsFcAc3CgCdWfayHWFazNfJBxkzLyU2wOJSj7h4W17-", + .url = "git+https://git.lerch.org/lobo/aws-sdk-for-zig#4df27142d0efa560bd13f14cef8298ee9bceafc8", + .hash = "aws-0.0.1-SbsFcP05CgDKqHcuxwvc-FZ5ITDVGqeGL9uUU7eE38nb", }, }, .paths = .{ diff --git a/tools/build/src/deploy.zig b/tools/build/src/deploy.zig index 43be393..2079d4f 100644 --- a/tools/build/src/deploy.zig +++ b/tools/build/src/deploy.zig @@ -231,19 +231,8 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void { const base64_data = try std.fmt.allocPrint(options.allocator, "{b64}", .{zip_data}); defer options.allocator.free(base64_data); - var client = aws.Client.init(options.allocator, .{}); - defer client.deinit(); - const services = aws.Services(.{.lambda}){}; - const region = options.region orelse "us-east-1"; - - const aws_options = aws.Options{ - .client = client, - .region = region, - .credential_options = .{ .profile = .{ .profile_name = options.profile } }, - }; - // Convert arch string to Lambda format const lambda_arch: []const u8 = if (std.mem.eql(u8, arch_str, "aarch64") or std.mem.eql(u8, arch_str, "arm64")) "arm64" @@ -273,12 +262,9 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void { .allocator = options.allocator, }; - const create_options = aws.Options{ - .client = client, - .region = region, - .diagnostics = &create_diagnostics, - .credential_options = .{ .profile = .{ .profile_name = options.profile } }, - }; + // Use the shared aws_options but add diagnostics for create call + var create_options = options.aws_options; + create_options.diagnostics = &create_diagnostics; const create_result = aws.Request(services.lambda.create_function).call(.{ .function_name = deploy_opts.function_name, @@ -301,7 +287,7 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void { .function_name = deploy_opts.function_name, .architectures = architectures, .zip_file = base64_data, - }, aws_options); + }, options.aws_options); defer update_result.deinit(); try options.stdout.print("Updated function: {s}\n", .{deploy_opts.function_name}); @@ -311,11 +297,11 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void { try options.stdout.flush(); // Wait for function to be ready before updating configuration - try waitForFunctionReady(deploy_opts.function_name, aws_options); + try waitForFunctionReady(deploy_opts.function_name, options); // Update environment variables if provided if (env_variables) |vars| { - try updateFunctionConfiguration(deploy_opts.function_name, vars, aws_options, options); + try updateFunctionConfiguration(deploy_opts.function_name, vars, options); } return; @@ -333,7 +319,7 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void { try options.stdout.flush(); // Wait for function to be ready before returning - try waitForFunctionReady(deploy_opts.function_name, aws_options); + try waitForFunctionReady(deploy_opts.function_name, options); } /// Build environment variables in the format expected by AWS Lambda API @@ -364,7 +350,6 @@ fn buildEnvVariables( fn updateFunctionConfiguration( function_name: []const u8, env_variables: []EnvVar, - aws_options: aws.Options, options: RunOptions, ) !void { const services = aws.Services(.{.lambda}){}; @@ -374,24 +359,24 @@ fn updateFunctionConfiguration( const update_config_result = try aws.Request(services.lambda.update_function_configuration).call(.{ .function_name = function_name, .environment = .{ .variables = env_variables }, - }, aws_options); + }, options.aws_options); defer update_config_result.deinit(); try options.stdout.print("Updated environment variables\n", .{}); try options.stdout.flush(); // Wait for configuration update to complete - try waitForFunctionReady(function_name, aws_options); + try waitForFunctionReady(function_name, options); } -fn waitForFunctionReady(function_name: []const u8, aws_options: aws.Options) !void { +fn waitForFunctionReady(function_name: []const u8, options: RunOptions) !void { const services = aws.Services(.{.lambda}){}; var retries: usize = 30; // Up to ~6 seconds total while (retries > 0) : (retries -= 1) { const result = aws.Request(services.lambda.get_function).call(.{ .function_name = function_name, - }, aws_options) catch |err| { + }, options.aws_options) catch |err| { // Function should exist at this point, but retry on transient errors std.log.warn("GetFunction failed during wait: {}", .{err}); std.Thread.sleep(200 * std.time.ns_per_ms); diff --git a/tools/build/src/iam.zig b/tools/build/src/iam.zig index 2f2c69f..b9724a3 100644 --- a/tools/build/src/iam.zig +++ b/tools/build/src/iam.zig @@ -58,10 +58,6 @@ fn printHelp(writer: *std.Io.Writer) void { /// Get or create an IAM role for Lambda execution /// Returns the role ARN pub fn getOrCreateRole(role_name: []const u8, options: RunOptions) ![]const u8 { - var client = aws.Client.init(options.allocator, .{}); - defer client.deinit(); - - // Try to get existing role const services = aws.Services(.{.iam}){}; var diagnostics = aws.Diagnostics{ @@ -70,11 +66,9 @@ pub fn getOrCreateRole(role_name: []const u8, options: RunOptions) ![]const u8 { .allocator = options.allocator, }; - const aws_options = aws.Options{ - .client = client, - .diagnostics = &diagnostics, - .credential_options = .{ .profile = .{ .profile_name = options.profile } }, - }; + // Use the shared aws_options but add diagnostics for this call + var aws_options = options.aws_options; + aws_options.diagnostics = &diagnostics; const get_result = aws.Request(services.iam.get_role).call(.{ .role_name = role_name, @@ -82,7 +76,7 @@ pub fn getOrCreateRole(role_name: []const u8, options: RunOptions) ![]const u8 { defer diagnostics.deinit(); if (diagnostics.http_code == 404) { // Role doesn't exist, create it - return try createRole(options.allocator, role_name, client, options.profile); + return try createRole(role_name, options); } std.log.err("IAM GetRole failed: {} (HTTP {})", .{ err, diagnostics.http_code }); return error.IamGetRoleFailed; @@ -93,14 +87,9 @@ pub fn getOrCreateRole(role_name: []const u8, options: RunOptions) ![]const u8 { return try options.allocator.dupe(u8, get_result.response.role.arn); } -fn createRole(allocator: std.mem.Allocator, role_name: []const u8, client: aws.Client, profile: ?[]const u8) ![]const u8 { +fn createRole(role_name: []const u8, options: RunOptions) ![]const u8 { const services = aws.Services(.{.iam}){}; - const aws_options = aws.Options{ - .client = client, - .credential_options = .{ .profile = .{ .profile_name = profile } }, - }; - const assume_role_policy = \\{ \\ "Version": "2012-10-17", @@ -122,10 +111,10 @@ fn createRole(allocator: std.mem.Allocator, role_name: []const u8, client: aws.C const create_result = try aws.Request(services.iam.create_role).call(.{ .role_name = role_name, .assume_role_policy_document = assume_role_policy, - }, aws_options); + }, options.aws_options); defer create_result.deinit(); - const arn = try allocator.dupe(u8, create_result.response.role.arn); + const arn = try options.allocator.dupe(u8, create_result.response.role.arn); // Attach the Lambda execution policy std.log.info("Attaching AWSLambdaExecute policy", .{}); @@ -133,7 +122,7 @@ fn createRole(allocator: std.mem.Allocator, role_name: []const u8, client: aws.C const attach_result = try aws.Request(services.iam.attach_role_policy).call(.{ .policy_arn = "arn:aws:iam::aws:policy/AWSLambdaExecute", .role_name = role_name, - }, aws_options); + }, options.aws_options); defer attach_result.deinit(); // IAM role creation can take a moment to propagate diff --git a/tools/build/src/invoke.zig b/tools/build/src/invoke.zig index 84ecf2f..d841239 100644 --- a/tools/build/src/invoke.zig +++ b/tools/build/src/invoke.zig @@ -57,19 +57,7 @@ fn printHelp(writer: *std.Io.Writer) void { } fn invokeFunction(function_name: []const u8, payload: []const u8, options: RunOptions) !void { - // Note: Profile is expected to be set via AWS_PROFILE env var before invoking this tool - // (e.g., via aws-vault exec) - - var client = aws.Client.init(options.allocator, .{}); - defer client.deinit(); - const services = aws.Services(.{.lambda}){}; - const region = options.region orelse "us-east-1"; - - const aws_options = aws.Options{ - .client = client, - .region = region, - }; std.log.info("Invoking function: {s}", .{function_name}); @@ -78,7 +66,7 @@ fn invokeFunction(function_name: []const u8, payload: []const u8, options: RunOp .payload = payload, .log_type = "Tail", .invocation_type = "RequestResponse", - }, aws_options); + }, options.aws_options); defer result.deinit(); // Print response payload diff --git a/tools/build/src/main.zig b/tools/build/src/main.zig index 247cd2d..f15cdde 100644 --- a/tools/build/src/main.zig +++ b/tools/build/src/main.zig @@ -11,6 +11,7 @@ //! invoke Invoke the deployed function const std = @import("std"); +const aws = @import("aws"); const package = @import("package.zig"); const iam_cmd = @import("iam.zig"); const deploy_cmd = @import("deploy.zig"); @@ -21,8 +22,8 @@ pub const RunOptions = struct { allocator: std.mem.Allocator, stdout: *std.Io.Writer, stderr: *std.Io.Writer, - region: ?[]const u8 = null, - profile: ?[]const u8 = null, + region: []const u8, + aws_options: aws.Options, }; pub fn main() !u8 { @@ -35,46 +36,42 @@ pub fn main() !u8 { var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer); - var options = RunOptions{ - .allocator = allocator, - .stdout = &stdout_writer.interface, - .stderr = &stderr_writer.interface, - }; - - run(&options) catch |err| { - options.stderr.print("Error: {}\n", .{err}) catch {}; - options.stderr.flush() catch {}; + run(allocator, &stdout_writer.interface, &stderr_writer.interface) catch |err| { + stderr_writer.interface.print("Error: {}\n", .{err}) catch {}; + stderr_writer.interface.flush() catch {}; return 1; }; - try options.stderr.flush(); - try options.stdout.flush(); + try stderr_writer.interface.flush(); + try stdout_writer.interface.flush(); return 0; } -fn run(options: *RunOptions) !void { - const args = try std.process.argsAlloc(options.allocator); - defer std.process.argsFree(options.allocator, args); +fn run(allocator: std.mem.Allocator, stdout: *std.Io.Writer, stderr: *std.Io.Writer) !void { + const args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); if (args.len < 2) { - printUsage(options.stderr); - try options.stderr.flush(); + printUsage(stderr); + try stderr.flush(); return error.MissingCommand; } // Parse global options and find command var cmd_start: usize = 1; + var region: []const u8 = "us-east-1"; + var profile: ?[]const u8 = null; while (cmd_start < args.len) { const arg = args[cmd_start]; if (std.mem.eql(u8, arg, "--region")) { cmd_start += 1; if (cmd_start >= args.len) return error.MissingRegionValue; - options.region = args[cmd_start]; + region = args[cmd_start]; cmd_start += 1; } else if (std.mem.eql(u8, arg, "--profile")) { cmd_start += 1; if (cmd_start >= args.len) return error.MissingProfileValue; - options.profile = args[cmd_start]; + profile = args[cmd_start]; cmd_start += 1; } else if (std.mem.startsWith(u8, arg, "--")) { // Unknown global option - might be command-specific, let command handle it @@ -86,29 +83,52 @@ fn run(options: *RunOptions) !void { } if (cmd_start >= args.len) { - printUsage(options.stderr); - try options.stderr.flush(); + printUsage(stderr); + try stderr.flush(); return error.MissingCommand; } + // Create AWS client and options once, used by all commands + var client = aws.Client.init(allocator, .{}); + defer client.deinit(); + + const aws_options = aws.Options{ + .client = client, + .region = region, + .credential_options = .{ + .profile = .{ + .profile_name = profile, + .prefer_profile_from_file = profile != null, + }, + }, + }; + + const options = RunOptions{ + .allocator = allocator, + .stdout = stdout, + .stderr = stderr, + .region = region, + .aws_options = aws_options, + }; + const command = args[cmd_start]; const cmd_args = args[cmd_start + 1 ..]; if (std.mem.eql(u8, command, "package")) { - try package.run(cmd_args, options.*); + try package.run(cmd_args, options); } else if (std.mem.eql(u8, command, "iam")) { - try iam_cmd.run(cmd_args, options.*); + try iam_cmd.run(cmd_args, options); } else if (std.mem.eql(u8, command, "deploy")) { - try deploy_cmd.run(cmd_args, options.*); + try deploy_cmd.run(cmd_args, options); } else if (std.mem.eql(u8, command, "invoke")) { - try invoke_cmd.run(cmd_args, options.*); + try invoke_cmd.run(cmd_args, options); } else if (std.mem.eql(u8, command, "--help") or std.mem.eql(u8, command, "-h")) { - printUsage(options.stdout); - try options.stdout.flush(); + printUsage(stdout); + try stdout.flush(); } else { - options.stderr.print("Unknown command: {s}\n\n", .{command}) catch {}; - printUsage(options.stderr); - try options.stderr.flush(); + stderr.print("Unknown command: {s}\n\n", .{command}) catch {}; + printUsage(stderr); + try stderr.flush(); return error.UnknownCommand; } } @@ -120,7 +140,7 @@ fn printUsage(writer: *std.Io.Writer) void { \\Lambda deployment CLI tool \\ \\Global Options: - \\ --region AWS region (default: from AWS config) + \\ --region AWS region (default: us-east-1) \\ --profile AWS profile to use \\ \\Commands: