add ability for consumers to get deployment information

This commit is contained in:
Emil Lerch 2026-02-03 16:52:12 -08:00
parent ed9c7ced6c
commit 140f9e9c55
Signed by untrusted user: lobo
GPG key ID: A7B62D657EF764F8
3 changed files with 164 additions and 10 deletions

View file

@ -107,11 +107,13 @@ fn configureBuildInternal(b: *std.Build, exe: *std.Build.Step.Compile) !void {
.target = b.graph.host, .target = b.graph.host,
.optimize = .ReleaseSafe, .optimize = .ReleaseSafe,
}); });
try @import("lambdabuild.zig").configureBuild(b, lambda_build_dep, exe, .{}); // Ignore return value for internal builds
_ = try @import("lambdabuild.zig").configureBuild(b, lambda_build_dep, exe, .{});
} }
/// Re-export LambdaConfig for consumers /// Re-export types for consumers
pub const LambdaConfig = @import("lambdabuild.zig").Config; pub const LambdaConfig = @import("lambdabuild.zig").Config;
pub const LambdaBuildInfo = @import("lambdabuild.zig").BuildInfo;
/// Configure Lambda build steps for a Zig project. /// Configure Lambda build steps for a Zig project.
/// ///
@ -119,6 +121,11 @@ pub const LambdaConfig = @import("lambdabuild.zig").Config;
/// Lambda functions to AWS. The `lambda_zig_dep` parameter must be the /// Lambda functions to AWS. The `lambda_zig_dep` parameter must be the
/// dependency object obtained from `b.dependency("lambda_zig", ...)`. /// dependency object obtained from `b.dependency("lambda_zig", ...)`.
/// ///
/// Returns a `LambdaBuildInfo` struct containing:
/// - References to all build steps (package, iam, deploy, invoke)
/// - A `deploy_output` LazyPath to a JSON file with deployment info
/// - The function name used
///
/// ## Build Steps /// ## Build Steps
/// ///
/// The following build steps are added: /// The following build steps are added:
@ -144,6 +151,24 @@ pub const LambdaConfig = @import("lambdabuild.zig").Config;
/// - `-Dallow-principal=[string]`: AWS service principal to grant invoke permission /// - `-Dallow-principal=[string]`: AWS service principal to grant invoke permission
/// (e.g., "alexa-appkit.amazon.com" for Alexa Skills Kit) /// (e.g., "alexa-appkit.amazon.com" for Alexa Skills Kit)
/// ///
/// ## Deploy Output
///
/// The `deploy_output` field in the returned struct is a LazyPath to a JSON file
/// containing deployment information (available after deploy completes):
///
/// ```json
/// {
/// "arn": "arn:aws:lambda:us-east-1:123456789012:function:my-function",
/// "function_name": "my-function",
/// "partition": "aws",
/// "region": "us-east-1",
/// "account_id": "123456789012",
/// "role_arn": "arn:aws:iam::123456789012:role/lambda_basic_execution",
/// "architecture": "arm64",
/// "environment_keys": ["MY_VAR"]
/// }
/// ```
///
/// ## Example /// ## Example
/// ///
/// ```zig /// ```zig
@ -161,13 +186,15 @@ pub const LambdaConfig = @import("lambdabuild.zig").Config;
/// const exe = b.addExecutable(.{ ... }); /// const exe = b.addExecutable(.{ ... });
/// b.installArtifact(exe); /// b.installArtifact(exe);
/// ///
/// // Use default config (function name defaults to "zig-fn") /// // Configure Lambda build and get deployment info
/// try lambda_zig.configureBuild(b, lambda_zig_dep, exe, .{}); /// const lambda = try lambda_zig.configureBuild(b, lambda_zig_dep, exe, .{
///
/// // Or specify project-level defaults
/// try lambda_zig.configureBuild(b, lambda_zig_dep, exe, .{
/// .default_function_name = "my-function", /// .default_function_name = "my-function",
/// }); /// });
///
/// // Use lambda.deploy_output in other steps that need the ARN
/// const my_step = b.addRunArtifact(my_tool);
/// my_step.addFileArg(lambda.deploy_output);
/// my_step.step.dependOn(lambda.deploy_step); // Ensure deploy runs first
/// } /// }
/// ``` /// ```
pub fn configureBuild( pub fn configureBuild(
@ -175,11 +202,11 @@ pub fn configureBuild(
lambda_zig_dep: *std.Build.Dependency, lambda_zig_dep: *std.Build.Dependency,
exe: *std.Build.Step.Compile, exe: *std.Build.Step.Compile,
config: LambdaConfig, config: LambdaConfig,
) !void { ) !LambdaBuildInfo {
// Get lambda_build from the lambda_zig dependency's Build context // Get lambda_build from the lambda_zig dependency's Build context
const lambda_build_dep = lambda_zig_dep.builder.dependency("lambda_build", .{ const lambda_build_dep = lambda_zig_dep.builder.dependency("lambda_build", .{
.target = b.graph.host, .target = b.graph.host,
.optimize = .ReleaseSafe, .optimize = .ReleaseSafe,
}); });
try @import("lambdabuild.zig").configureBuild(b, lambda_build_dep, exe, config); return @import("lambdabuild.zig").configureBuild(b, lambda_build_dep, exe, config);
} }

View file

@ -18,6 +18,32 @@ pub const Config = struct {
default_role_name: []const u8 = "lambda_basic_execution", default_role_name: []const u8 = "lambda_basic_execution",
}; };
/// Information about the configured Lambda build steps.
///
/// Returned by `configureBuild` to allow consumers to depend on steps
/// and access deployment outputs.
pub const BuildInfo = struct {
/// Package step - creates the deployment zip
package_step: *std.Build.Step,
/// IAM step - creates/verifies the IAM role
iam_step: *std.Build.Step,
/// Deploy step - deploys the function to AWS Lambda
deploy_step: *std.Build.Step,
/// Invoke step - invokes the deployed function
invoke_step: *std.Build.Step,
/// LazyPath to JSON file with deployment info.
/// Contains: arn, function_name, region, account_id, role_arn, architecture, environment_keys
/// Available after deploy_step completes.
deploy_output: std.Build.LazyPath,
/// The function name used for deployment
function_name: []const u8,
};
/// Configure Lambda build steps for a Zig project. /// Configure Lambda build steps for a Zig project.
/// ///
/// Adds the following build steps: /// Adds the following build steps:
@ -28,12 +54,15 @@ pub const Config = struct {
/// ///
/// The `config` parameter allows setting project-level defaults that can /// The `config` parameter allows setting project-level defaults that can
/// still be overridden via command-line options. /// still be overridden via command-line options.
///
/// Returns a `BuildInfo` struct containing references to all steps and
/// a `deploy_output` LazyPath to the deployment info JSON file.
pub fn configureBuild( pub fn configureBuild(
b: *std.Build, b: *std.Build,
lambda_build_dep: *std.Build.Dependency, lambda_build_dep: *std.Build.Dependency,
exe: *std.Build.Step.Compile, exe: *std.Build.Step.Compile,
config: Config, config: Config,
) !void { ) !BuildInfo {
// Get the lambda-build CLI artifact from the dependency // Get the lambda-build CLI artifact from the dependency
const cli = lambda_build_dep.artifact("lambda-build"); const cli = lambda_build_dep.artifact("lambda-build");
@ -117,6 +146,9 @@ pub fn configureBuild(
}); });
if (env_file) |ef| deploy_cmd.addArgs(&.{ "--env-file", ef }); if (env_file) |ef| deploy_cmd.addArgs(&.{ "--env-file", ef });
if (allow_principal) |ap| deploy_cmd.addArgs(&.{ "--allow-principal", ap }); if (allow_principal) |ap| deploy_cmd.addArgs(&.{ "--allow-principal", ap });
// Add deploy output file for deployment info JSON
deploy_cmd.addArg("--deploy-output");
const deploy_output = deploy_cmd.addOutputFileArg("deploy-output.json");
deploy_cmd.step.dependOn(&package_cmd.step); deploy_cmd.step.dependOn(&package_cmd.step);
const deploy_step = b.step("awslambda_deploy", "Deploy the Lambda function"); const deploy_step = b.step("awslambda_deploy", "Deploy the Lambda function");
@ -138,4 +170,13 @@ pub fn configureBuild(
const run_step = b.step("awslambda_run", "Invoke the deployed Lambda function"); const run_step = b.step("awslambda_run", "Invoke the deployed Lambda function");
run_step.dependOn(&invoke_cmd.step); run_step.dependOn(&invoke_cmd.step);
return .{
.package_step = package_step,
.iam_step = iam_step,
.deploy_step = deploy_step,
.invoke_step = run_step,
.deploy_output = deploy_output,
.function_name = function_name,
};
} }

View file

@ -18,6 +18,7 @@ pub fn run(args: []const []const u8, options: RunOptions) !void {
var role_name: []const u8 = "lambda_basic_execution"; var role_name: []const u8 = "lambda_basic_execution";
var arch: ?[]const u8 = null; var arch: ?[]const u8 = null;
var allow_principal: ?[]const u8 = null; var allow_principal: ?[]const u8 = null;
var deploy_output: ?[]const u8 = null;
// Environment variables storage // Environment variables storage
var env_vars = std.StringHashMap([]const u8).init(options.allocator); var env_vars = std.StringHashMap([]const u8).init(options.allocator);
@ -65,6 +66,10 @@ pub fn run(args: []const []const u8, options: RunOptions) !void {
i += 1; i += 1;
if (i >= args.len) return error.MissingAllowPrincipal; if (i >= args.len) return error.MissingAllowPrincipal;
allow_principal = args[i]; allow_principal = args[i];
} else if (std.mem.eql(u8, arg, "--deploy-output")) {
i += 1;
if (i >= args.len) return error.MissingDeployOutput;
deploy_output = args[i];
} else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { } else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
printHelp(options.stdout); printHelp(options.stdout);
try options.stdout.flush(); try options.stdout.flush();
@ -98,6 +103,7 @@ pub fn run(args: []const []const u8, options: RunOptions) !void {
.arch = arch, .arch = arch,
.env_vars = if (env_vars.count() > 0) &env_vars else null, .env_vars = if (env_vars.count() > 0) &env_vars else null,
.allow_principal = allow_principal, .allow_principal = allow_principal,
.deploy_output = deploy_output,
}, options); }, options);
} }
@ -187,6 +193,7 @@ fn printHelp(writer: anytype) void {
\\ --env-file <path> Load environment variables from file (KEY=VALUE format) \\ --env-file <path> Load environment variables from file (KEY=VALUE format)
\\ --allow-principal <p> Grant invoke permission to AWS service principal \\ --allow-principal <p> Grant invoke permission to AWS service principal
\\ (e.g., alexa-appkit.amazon.com) \\ (e.g., alexa-appkit.amazon.com)
\\ --deploy-output <path> Write deployment info (ARN, region, etc.) to JSON file
\\ --help, -h Show this help message \\ --help, -h Show this help message
\\ \\
\\Environment File Format: \\Environment File Format:
@ -212,6 +219,7 @@ const DeployOptions = struct {
arch: ?[]const u8, arch: ?[]const u8,
env_vars: ?*const std.StringHashMap([]const u8), env_vars: ?*const std.StringHashMap([]const u8),
allow_principal: ?[]const u8, allow_principal: ?[]const u8,
deploy_output: ?[]const u8,
}; };
fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void { fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
@ -275,6 +283,10 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
var create_options = options.aws_options; var create_options = options.aws_options;
create_options.diagnostics = &create_diagnostics; create_options.diagnostics = &create_diagnostics;
// Track the function ARN from whichever path succeeds
var function_arn: ?[]const u8 = null;
defer if (function_arn) |arn| options.allocator.free(arn);
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,
.architectures = architectures, .architectures = architectures,
@ -302,6 +314,7 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
try options.stdout.print("Updated function: {s}\n", .{deploy_opts.function_name}); try options.stdout.print("Updated function: {s}\n", .{deploy_opts.function_name});
if (update_result.response.function_arn) |arn| { if (update_result.response.function_arn) |arn| {
try options.stdout.print("ARN: {s}\n", .{arn}); try options.stdout.print("ARN: {s}\n", .{arn});
function_arn = try options.allocator.dupe(u8, arn);
} }
try options.stdout.flush(); try options.stdout.flush();
@ -318,6 +331,11 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
try addPermission(deploy_opts.function_name, principal, options); try addPermission(deploy_opts.function_name, principal, options);
} }
// Write deploy output if requested
if (deploy_opts.deploy_output) |output_path| {
try writeDeployOutput(output_path, function_arn.?, role_arn, lambda_arch, deploy_opts.env_vars);
}
return; return;
} }
@ -329,6 +347,7 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
try options.stdout.print("Created function: {s}\n", .{deploy_opts.function_name}); try options.stdout.print("Created function: {s}\n", .{deploy_opts.function_name});
if (create_result.response.function_arn) |arn| { if (create_result.response.function_arn) |arn| {
try options.stdout.print("ARN: {s}\n", .{arn}); try options.stdout.print("ARN: {s}\n", .{arn});
function_arn = try options.allocator.dupe(u8, arn);
} }
try options.stdout.flush(); try options.stdout.flush();
@ -339,6 +358,11 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
if (deploy_opts.allow_principal) |principal| { if (deploy_opts.allow_principal) |principal| {
try addPermission(deploy_opts.function_name, principal, options); try addPermission(deploy_opts.function_name, principal, options);
} }
// Write deploy output if requested
if (deploy_opts.deploy_output) |output_path| {
try writeDeployOutput(output_path, function_arn.?, role_arn, lambda_arch, deploy_opts.env_vars);
}
} }
/// Build environment variables in the format expected by AWS Lambda API /// Build environment variables in the format expected by AWS Lambda API
@ -487,3 +511,65 @@ fn addPermission(
try options.stdout.print("Added invoke permission for: {s}\n", .{principal}); try options.stdout.print("Added invoke permission for: {s}\n", .{principal});
try options.stdout.flush(); try options.stdout.flush();
} }
/// Write deployment information to a JSON file
fn writeDeployOutput(
output_path: []const u8,
function_arn: []const u8,
role_arn: []const u8,
architecture: []const u8,
env_vars: ?*const std.StringHashMap([]const u8),
) !void {
// Parse ARN to extract components
// ARN format: arn:{partition}:lambda:{region}:{account_id}:function:{name}
var arn_parts = std.mem.splitScalar(u8, function_arn, ':');
_ = arn_parts.next(); // arn
const partition = arn_parts.next() orelse return error.InvalidArn;
_ = arn_parts.next(); // lambda
const region = arn_parts.next() orelse return error.InvalidArn;
const account_id = arn_parts.next() orelse return error.InvalidArn;
_ = arn_parts.next(); // function
const function_name = arn_parts.next() orelse return error.InvalidArn;
const file = try std.fs.cwd().createFile(output_path, .{});
defer file.close();
var write_buffer: [4096]u8 = undefined;
var buffered = file.writer(&write_buffer);
const writer = &buffered.interface;
try writer.print(
\\{{
\\ "arn": "{s}",
\\ "function_name": "{s}",
\\ "partition": "{s}",
\\ "region": "{s}",
\\ "account_id": "{s}",
\\ "role_arn": "{s}",
\\ "architecture": "{s}",
\\ "environment_keys": [
, .{ function_arn, function_name, partition, region, account_id, role_arn, architecture });
// Write environment variable keys
if (env_vars) |vars| {
var it = vars.keyIterator();
var first = true;
while (it.next()) |key| {
if (!first) {
try writer.writeAll(",");
}
try writer.print("\n \"{s}\"", .{key.*});
first = false;
}
}
try writer.writeAll(
\\
\\ ]
\\}
\\
);
try writer.flush();
std.log.info("Wrote deployment info to: {s}", .{output_path});
}