tell sdk that the profile is a command line flag/change precedence

This commit is contained in:
Emil Lerch 2026-02-02 17:38:51 -08:00
parent 446b726cf9
commit b3f5cb8203
Signed by untrusted user: lobo
GPG key ID: A7B62D657EF764F8
5 changed files with 74 additions and 92 deletions

View file

@ -4,8 +4,8 @@
.fingerprint = 0x6e61de08e7e51114, .fingerprint = 0x6e61de08e7e51114,
.dependencies = .{ .dependencies = .{
.aws = .{ .aws = .{
.url = "git+https://git.lerch.org/lobo/aws-sdk-for-zig#686b18d1f4329e80cf6d9b916eaa0c231333edb9", .url = "git+https://git.lerch.org/lobo/aws-sdk-for-zig#4df27142d0efa560bd13f14cef8298ee9bceafc8",
.hash = "aws-0.0.1-SbsFcAc3CgCdWfayHWFazNfJBxkzLyU2wOJSj7h4W17-", .hash = "aws-0.0.1-SbsFcP05CgDKqHcuxwvc-FZ5ITDVGqeGL9uUU7eE38nb",
}, },
}, },
.paths = .{ .paths = .{

View file

@ -231,19 +231,8 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
const base64_data = try std.fmt.allocPrint(options.allocator, "{b64}", .{zip_data}); const base64_data = try std.fmt.allocPrint(options.allocator, "{b64}", .{zip_data});
defer options.allocator.free(base64_data); defer options.allocator.free(base64_data);
var client = aws.Client.init(options.allocator, .{});
defer client.deinit();
const services = aws.Services(.{.lambda}){}; 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 // 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")) const lambda_arch: []const u8 = if (std.mem.eql(u8, arch_str, "aarch64") or std.mem.eql(u8, arch_str, "arm64"))
"arm64" "arm64"
@ -273,12 +262,9 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
.allocator = options.allocator, .allocator = options.allocator,
}; };
const create_options = aws.Options{ // Use the shared aws_options but add diagnostics for create call
.client = client, var create_options = options.aws_options;
.region = region, create_options.diagnostics = &create_diagnostics;
.diagnostics = &create_diagnostics,
.credential_options = .{ .profile = .{ .profile_name = options.profile } },
};
const create_result = aws.Request(services.lambda.create_function).call(.{ const create_result = aws.Request(services.lambda.create_function).call(.{
.function_name = deploy_opts.function_name, .function_name = deploy_opts.function_name,
@ -301,7 +287,7 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
.function_name = deploy_opts.function_name, .function_name = deploy_opts.function_name,
.architectures = architectures, .architectures = architectures,
.zip_file = base64_data, .zip_file = base64_data,
}, aws_options); }, options.aws_options);
defer update_result.deinit(); defer update_result.deinit();
try options.stdout.print("Updated function: {s}\n", .{deploy_opts.function_name}); 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(); try options.stdout.flush();
// Wait for function to be ready before updating configuration // 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 // Update environment variables if provided
if (env_variables) |vars| { if (env_variables) |vars| {
try updateFunctionConfiguration(deploy_opts.function_name, vars, aws_options, options); try updateFunctionConfiguration(deploy_opts.function_name, vars, options);
} }
return; return;
@ -333,7 +319,7 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
try options.stdout.flush(); try options.stdout.flush();
// Wait for function to be ready before returning // 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 /// Build environment variables in the format expected by AWS Lambda API
@ -364,7 +350,6 @@ fn buildEnvVariables(
fn updateFunctionConfiguration( fn updateFunctionConfiguration(
function_name: []const u8, function_name: []const u8,
env_variables: []EnvVar, env_variables: []EnvVar,
aws_options: aws.Options,
options: RunOptions, options: RunOptions,
) !void { ) !void {
const services = aws.Services(.{.lambda}){}; const services = aws.Services(.{.lambda}){};
@ -374,24 +359,24 @@ fn updateFunctionConfiguration(
const update_config_result = try aws.Request(services.lambda.update_function_configuration).call(.{ const update_config_result = try aws.Request(services.lambda.update_function_configuration).call(.{
.function_name = function_name, .function_name = function_name,
.environment = .{ .variables = env_variables }, .environment = .{ .variables = env_variables },
}, aws_options); }, options.aws_options);
defer update_config_result.deinit(); defer update_config_result.deinit();
try options.stdout.print("Updated environment variables\n", .{}); try options.stdout.print("Updated environment variables\n", .{});
try options.stdout.flush(); try options.stdout.flush();
// Wait for configuration update to complete // 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}){}; const services = aws.Services(.{.lambda}){};
var retries: usize = 30; // Up to ~6 seconds total var retries: usize = 30; // Up to ~6 seconds total
while (retries > 0) : (retries -= 1) { while (retries > 0) : (retries -= 1) {
const result = aws.Request(services.lambda.get_function).call(.{ const result = aws.Request(services.lambda.get_function).call(.{
.function_name = function_name, .function_name = function_name,
}, aws_options) catch |err| { }, options.aws_options) catch |err| {
// Function should exist at this point, but retry on transient errors // Function should exist at this point, but retry on transient errors
std.log.warn("GetFunction failed during wait: {}", .{err}); std.log.warn("GetFunction failed during wait: {}", .{err});
std.Thread.sleep(200 * std.time.ns_per_ms); std.Thread.sleep(200 * std.time.ns_per_ms);

View file

@ -58,10 +58,6 @@ fn printHelp(writer: *std.Io.Writer) void {
/// Get or create an IAM role for Lambda execution /// Get or create an IAM role for Lambda execution
/// Returns the role ARN /// Returns the role ARN
pub fn getOrCreateRole(role_name: []const u8, options: RunOptions) ![]const u8 { 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}){}; const services = aws.Services(.{.iam}){};
var diagnostics = aws.Diagnostics{ var diagnostics = aws.Diagnostics{
@ -70,11 +66,9 @@ pub fn getOrCreateRole(role_name: []const u8, options: RunOptions) ![]const u8 {
.allocator = options.allocator, .allocator = options.allocator,
}; };
const aws_options = aws.Options{ // Use the shared aws_options but add diagnostics for this call
.client = client, var aws_options = options.aws_options;
.diagnostics = &diagnostics, aws_options.diagnostics = &diagnostics;
.credential_options = .{ .profile = .{ .profile_name = options.profile } },
};
const get_result = aws.Request(services.iam.get_role).call(.{ const get_result = aws.Request(services.iam.get_role).call(.{
.role_name = role_name, .role_name = role_name,
@ -82,7 +76,7 @@ pub fn getOrCreateRole(role_name: []const u8, options: RunOptions) ![]const u8 {
defer diagnostics.deinit(); defer diagnostics.deinit();
if (diagnostics.http_code == 404) { if (diagnostics.http_code == 404) {
// Role doesn't exist, create it // 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 }); std.log.err("IAM GetRole failed: {} (HTTP {})", .{ err, diagnostics.http_code });
return error.IamGetRoleFailed; 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); 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 services = aws.Services(.{.iam}){};
const aws_options = aws.Options{
.client = client,
.credential_options = .{ .profile = .{ .profile_name = profile } },
};
const assume_role_policy = const assume_role_policy =
\\{ \\{
\\ "Version": "2012-10-17", \\ "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(.{ const create_result = try aws.Request(services.iam.create_role).call(.{
.role_name = role_name, .role_name = role_name,
.assume_role_policy_document = assume_role_policy, .assume_role_policy_document = assume_role_policy,
}, aws_options); }, options.aws_options);
defer create_result.deinit(); 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 // Attach the Lambda execution policy
std.log.info("Attaching AWSLambdaExecute 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(.{ const attach_result = try aws.Request(services.iam.attach_role_policy).call(.{
.policy_arn = "arn:aws:iam::aws:policy/AWSLambdaExecute", .policy_arn = "arn:aws:iam::aws:policy/AWSLambdaExecute",
.role_name = role_name, .role_name = role_name,
}, aws_options); }, options.aws_options);
defer attach_result.deinit(); defer attach_result.deinit();
// IAM role creation can take a moment to propagate // IAM role creation can take a moment to propagate

View file

@ -57,19 +57,7 @@ fn printHelp(writer: *std.Io.Writer) void {
} }
fn invokeFunction(function_name: []const u8, payload: []const u8, options: RunOptions) !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 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}); 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, .payload = payload,
.log_type = "Tail", .log_type = "Tail",
.invocation_type = "RequestResponse", .invocation_type = "RequestResponse",
}, aws_options); }, options.aws_options);
defer result.deinit(); defer result.deinit();
// Print response payload // Print response payload

View file

@ -11,6 +11,7 @@
//! invoke Invoke the deployed function //! invoke Invoke the deployed function
const std = @import("std"); const std = @import("std");
const aws = @import("aws");
const package = @import("package.zig"); const package = @import("package.zig");
const iam_cmd = @import("iam.zig"); const iam_cmd = @import("iam.zig");
const deploy_cmd = @import("deploy.zig"); const deploy_cmd = @import("deploy.zig");
@ -21,8 +22,8 @@ pub const RunOptions = struct {
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
stdout: *std.Io.Writer, stdout: *std.Io.Writer,
stderr: *std.Io.Writer, stderr: *std.Io.Writer,
region: ?[]const u8 = null, region: []const u8,
profile: ?[]const u8 = null, aws_options: aws.Options,
}; };
pub fn main() !u8 { pub fn main() !u8 {
@ -35,46 +36,42 @@ pub fn main() !u8 {
var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer); var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer);
var options = RunOptions{ run(allocator, &stdout_writer.interface, &stderr_writer.interface) catch |err| {
.allocator = allocator, stderr_writer.interface.print("Error: {}\n", .{err}) catch {};
.stdout = &stdout_writer.interface, stderr_writer.interface.flush() catch {};
.stderr = &stderr_writer.interface,
};
run(&options) catch |err| {
options.stderr.print("Error: {}\n", .{err}) catch {};
options.stderr.flush() catch {};
return 1; return 1;
}; };
try options.stderr.flush(); try stderr_writer.interface.flush();
try options.stdout.flush(); try stdout_writer.interface.flush();
return 0; return 0;
} }
fn run(options: *RunOptions) !void { fn run(allocator: std.mem.Allocator, stdout: *std.Io.Writer, stderr: *std.Io.Writer) !void {
const args = try std.process.argsAlloc(options.allocator); const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(options.allocator, args); defer std.process.argsFree(allocator, args);
if (args.len < 2) { if (args.len < 2) {
printUsage(options.stderr); printUsage(stderr);
try options.stderr.flush(); try stderr.flush();
return error.MissingCommand; return error.MissingCommand;
} }
// Parse global options and find command // Parse global options and find command
var cmd_start: usize = 1; var cmd_start: usize = 1;
var region: []const u8 = "us-east-1";
var profile: ?[]const u8 = null;
while (cmd_start < args.len) { while (cmd_start < args.len) {
const arg = args[cmd_start]; const arg = args[cmd_start];
if (std.mem.eql(u8, arg, "--region")) { if (std.mem.eql(u8, arg, "--region")) {
cmd_start += 1; cmd_start += 1;
if (cmd_start >= args.len) return error.MissingRegionValue; if (cmd_start >= args.len) return error.MissingRegionValue;
options.region = args[cmd_start]; region = args[cmd_start];
cmd_start += 1; cmd_start += 1;
} else if (std.mem.eql(u8, arg, "--profile")) { } else if (std.mem.eql(u8, arg, "--profile")) {
cmd_start += 1; cmd_start += 1;
if (cmd_start >= args.len) return error.MissingProfileValue; if (cmd_start >= args.len) return error.MissingProfileValue;
options.profile = args[cmd_start]; profile = args[cmd_start];
cmd_start += 1; cmd_start += 1;
} else if (std.mem.startsWith(u8, arg, "--")) { } else if (std.mem.startsWith(u8, arg, "--")) {
// Unknown global option - might be command-specific, let command handle it // 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) { if (cmd_start >= args.len) {
printUsage(options.stderr); printUsage(stderr);
try options.stderr.flush(); try stderr.flush();
return error.MissingCommand; 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 command = args[cmd_start];
const cmd_args = args[cmd_start + 1 ..]; const cmd_args = args[cmd_start + 1 ..];
if (std.mem.eql(u8, command, "package")) { 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")) { } 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")) { } 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")) { } 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")) { } else if (std.mem.eql(u8, command, "--help") or std.mem.eql(u8, command, "-h")) {
printUsage(options.stdout); printUsage(stdout);
try options.stdout.flush(); try stdout.flush();
} else { } else {
options.stderr.print("Unknown command: {s}\n\n", .{command}) catch {}; stderr.print("Unknown command: {s}\n\n", .{command}) catch {};
printUsage(options.stderr); printUsage(stderr);
try options.stderr.flush(); try stderr.flush();
return error.UnknownCommand; return error.UnknownCommand;
} }
} }
@ -120,7 +140,7 @@ fn printUsage(writer: *std.Io.Writer) void {
\\Lambda deployment CLI tool \\Lambda deployment CLI tool
\\ \\
\\Global Options: \\Global Options:
\\ --region <region> AWS region (default: from AWS config) \\ --region <region> AWS region (default: us-east-1)
\\ --profile <profile> AWS profile to use \\ --profile <profile> AWS profile to use
\\ \\
\\Commands: \\Commands: