Compare commits
No commits in common. "09e9a32241d94d982dd7e3f4300d879177829842" and "fb84eb8d867eee2e84640c3a58f3e0b61f46d5ee" have entirely different histories.
09e9a32241
...
fb84eb8d86
7 changed files with 140 additions and 809 deletions
177
README.md
177
README.md
|
|
@ -17,8 +17,9 @@ Build options:
|
|||
* **payload**: JSON payload for function invocation (used with awslambda_run)
|
||||
* **region**: AWS region for deployment and invocation
|
||||
* **profile**: AWS profile to use for credentials
|
||||
* **role-name**: IAM role name for the function (default: lambda_basic_execution)
|
||||
* **env-file**: Path to environment variables file for the Lambda function
|
||||
* **config-file**: Path to lambda.json configuration file (overrides build.zig settings)
|
||||
* **allow-principal**: AWS service principal to grant invoke permission (e.g., alexa-appkit.amazon.com)
|
||||
|
||||
The Lambda function can be compiled for x86_64 or aarch64. The build system
|
||||
automatically configures the Lambda architecture based on the target.
|
||||
|
|
@ -26,155 +27,6 @@ automatically configures the Lambda architecture based on the target.
|
|||
A sample project using this runtime can be found at
|
||||
https://git.lerch.org/lobo/lambda-zig-sample
|
||||
|
||||
Lambda Configuration
|
||||
--------------------
|
||||
|
||||
Lambda functions can be configured via a `lambda.json` file or inline in `build.zig`.
|
||||
The configuration controls IAM roles, function settings, and deployment options.
|
||||
|
||||
### Configuration File (lambda.json)
|
||||
|
||||
By default, the build system looks for an optional `lambda.json` file in your project root.
|
||||
If found, it will use these settings for deployment.
|
||||
|
||||
```json
|
||||
{
|
||||
"role_name": "my_lambda_role",
|
||||
"timeout": 30,
|
||||
"memory_size": 512,
|
||||
"description": "My Lambda function",
|
||||
"allow_principal": "alexa-appkit.amazon.com",
|
||||
"tags": [
|
||||
{ "key": "Environment", "value": "production" },
|
||||
{ "key": "Project", "value": "my-project" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Available Configuration Options
|
||||
|
||||
Many of these configuration options are from the Lambda [CreateFunction](https://docs.aws.amazon.com/lambda/latest/api/API_CreateFunction.html#API_CreateFunction_RequestBody)
|
||||
API call and more details are available there.
|
||||
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|----------------------|----------|----------------------------|---------------------------------------------|
|
||||
| `role_name` | string | `"lambda_basic_execution"` | IAM role name for the function |
|
||||
| `timeout` | integer | AWS default (3) | Execution timeout in seconds (1-900) |
|
||||
| `memory_size` | integer | AWS default (128) | Memory allocation in MB (128-10240) |
|
||||
| `description` | string | null | Human-readable function description |
|
||||
| `allow_principal` | string | null | AWS service principal for invoke permission |
|
||||
| `kmskey_arn` | string | null | KMS key ARN for environment encryption |
|
||||
| `layers` | string[] | null | Lambda layer ARNs to attach |
|
||||
| `tags` | Tag[] | null | Resource tags (array of `{key, value}`) |
|
||||
| `vpc_config` | object | null | VPC configuration (see below) |
|
||||
| `dead_letter_config` | object | null | Dead letter queue configuration |
|
||||
| `tracing_config` | object | null | X-Ray tracing configuration |
|
||||
| `ephemeral_storage` | object | AWS default (512) | Ephemeral storage configuration |
|
||||
| `logging_config` | object | null | CloudWatch logging configuration |
|
||||
|
||||
### VPC Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"vpc_config": {
|
||||
"subnet_ids": ["subnet-12345", "subnet-67890"],
|
||||
"security_group_ids": ["sg-12345"],
|
||||
"ipv6_allowed_for_dual_stack": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tracing Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"tracing_config": {
|
||||
"mode": "Active"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Mode must be `"Active"` or `"PassThrough"`.
|
||||
|
||||
### Logging Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"logging_config": {
|
||||
"log_format": "JSON",
|
||||
"application_log_level": "INFO",
|
||||
"system_log_level": "WARN",
|
||||
"log_group": "/aws/lambda/my-function"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Log format must be `"JSON"` or `"Text"`.
|
||||
|
||||
### Ephemeral Storage
|
||||
|
||||
```json
|
||||
{
|
||||
"ephemeral_storage": {
|
||||
"size": 512
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Size must be between 512-10240 MB.
|
||||
|
||||
### Dead Letter Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"dead_letter_config": {
|
||||
"target_arn": "arn:aws:sqs:us-east-1:123456789:my-dlq"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Build Integration Options
|
||||
|
||||
You can also configure Lambda settings directly in `build.zig`:
|
||||
|
||||
```zig
|
||||
// Use a specific config file (required - fails if missing)
|
||||
_ = try lambda.configureBuild(b, dep, exe, .{
|
||||
.lambda_config = .{ .file = .{
|
||||
.path = b.path("deploy/lambda.json"),
|
||||
.required = true,
|
||||
}},
|
||||
});
|
||||
|
||||
// Use inline configuration
|
||||
_ = try lambda.configureBuild(b, dep, exe, .{
|
||||
.lambda_config = .{ .config = .{
|
||||
.role_name = "my_role",
|
||||
.timeout = 30,
|
||||
.memory_size = 512,
|
||||
.description = "My function",
|
||||
}},
|
||||
});
|
||||
|
||||
// Disable config file lookup entirely
|
||||
_ = try lambda.configureBuild(b, dep, exe, .{
|
||||
.lambda_config = .none,
|
||||
});
|
||||
```
|
||||
|
||||
### Overriding Config at Build Time
|
||||
|
||||
The `-Dconfig-file` build option overrides the `build.zig` configuration:
|
||||
|
||||
```sh
|
||||
# Use a different config file for staging
|
||||
zig build awslambda_deploy -Dconfig-file=lambda-staging.json
|
||||
|
||||
# Use production config
|
||||
zig build awslambda_deploy -Dconfig-file=deploy/lambda-prod.json
|
||||
```
|
||||
|
||||
Environment Variables
|
||||
---------------------
|
||||
|
||||
|
|
@ -229,21 +81,24 @@ Lambda functions can be configured to allow invocation by AWS service principals
|
|||
This is required for services like Alexa Skills Kit, API Gateway, or S3 to trigger
|
||||
your Lambda function.
|
||||
|
||||
### Using lambda.json (Recommended)
|
||||
### Using the build system
|
||||
|
||||
Add `allow_principal` to your configuration file:
|
||||
Pass the `-Dallow-principal` option to grant invoke permission to a service:
|
||||
|
||||
```json
|
||||
{
|
||||
"allow_principal": "alexa-appkit.amazon.com"
|
||||
}
|
||||
```sh
|
||||
# Allow Alexa Skills Kit to invoke the function
|
||||
zig build awslambda_deploy -Dfunction-name=my-skill -Dallow-principal=alexa-appkit.amazon.com
|
||||
|
||||
# Allow API Gateway to invoke the function
|
||||
zig build awslambda_deploy -Dfunction-name=my-api -Dallow-principal=apigateway.amazonaws.com
|
||||
```
|
||||
|
||||
Common service principals:
|
||||
- `alexa-appkit.amazon.com` - Alexa Skills Kit
|
||||
- `apigateway.amazonaws.com` - API Gateway
|
||||
- `s3.amazonaws.com` - S3 event notifications
|
||||
- `events.amazonaws.com` - EventBridge/CloudWatch Events
|
||||
### Using the CLI directly
|
||||
|
||||
```sh
|
||||
./lambda-build deploy --function-name my-fn --zip-file function.zip \
|
||||
--allow-principal alexa-appkit.amazon.com
|
||||
```
|
||||
|
||||
The permission is idempotent - if it already exists, the deployment will continue
|
||||
successfully.
|
||||
|
|
|
|||
119
build.zig
119
build.zig
|
|
@ -110,23 +110,9 @@ fn configureBuildInternal(b: *std.Build, exe: *std.Build.Step.Compile) !void {
|
|||
_ = try @import("lambdabuild.zig").configureBuild(b, lambda_build_dep, exe, .{});
|
||||
}
|
||||
|
||||
// Re-export types for consumers
|
||||
const lambdabuild = @import("lambdabuild.zig");
|
||||
|
||||
/// Options for Lambda build integration.
|
||||
pub const Options = lambdabuild.Options;
|
||||
|
||||
/// Source for Lambda build configuration (none, file, or inline config).
|
||||
pub const LambdaConfigSource = lambdabuild.LambdaConfigSource;
|
||||
|
||||
/// A config file path with explicit required/optional semantics.
|
||||
pub const ConfigFile = lambdabuild.ConfigFile;
|
||||
|
||||
/// Lambda build configuration struct (role_name, timeout, memory_size, VPC, etc.).
|
||||
pub const LambdaBuildConfig = lambdabuild.LambdaBuildConfig;
|
||||
|
||||
/// Information about the configured Lambda build steps.
|
||||
pub const BuildInfo = lambdabuild.BuildInfo;
|
||||
/// Re-export types for consumers
|
||||
pub const LambdaConfig = @import("lambdabuild.zig").Config;
|
||||
pub const LambdaBuildInfo = @import("lambdabuild.zig").BuildInfo;
|
||||
|
||||
/// Configure Lambda build steps for a Zig project.
|
||||
///
|
||||
|
|
@ -150,42 +136,19 @@ pub const BuildInfo = lambdabuild.BuildInfo;
|
|||
///
|
||||
/// ## Build Options
|
||||
///
|
||||
/// The following command-line options are available:
|
||||
/// The following options are added to the build (command-line options override
|
||||
/// config defaults):
|
||||
///
|
||||
/// - `-Dfunction-name=[string]`: Name of the Lambda function
|
||||
/// (default: exe.name, or as provided by config parameter)
|
||||
/// (default: "zig-fn", or as provided by config parameter)
|
||||
/// - `-Dregion=[string]`: AWS region for deployment and invocation
|
||||
/// - `-Dprofile=[string]`: AWS profile to use for credentials
|
||||
/// - `-Drole-name=[string]`: IAM role name
|
||||
/// (default: "lambda_basic_execution", or as provided by config parameter)
|
||||
/// - `-Dpayload=[string]`: JSON payload for invocation (default: "{}")
|
||||
/// - `-Denv-file=[string]`: Path to environment variables file (KEY=VALUE format)
|
||||
/// - `-Dconfig-file=[string]`: Path to Lambda build config JSON file (overrides function_config)
|
||||
///
|
||||
/// ## Configuration File
|
||||
///
|
||||
/// Function settings (timeout, memory, VPC, etc.) and deployment settings
|
||||
/// (role_name, allow_principal) are configured via a JSON file or inline config.
|
||||
///
|
||||
/// By default, looks for `lambda.json` in the project root. If not found,
|
||||
/// uses sensible defaults (role_name = "lambda_basic_execution").
|
||||
///
|
||||
/// ### Example lambda.json
|
||||
///
|
||||
/// ```json
|
||||
/// {
|
||||
/// "role_name": "my_lambda_role",
|
||||
/// "timeout": 30,
|
||||
/// "memory_size": 512,
|
||||
/// "description": "My function description",
|
||||
/// "allow_principal": "alexa-appkit.amazon.com",
|
||||
/// "tags": [
|
||||
/// { "key": "Environment", "value": "production" }
|
||||
/// ],
|
||||
/// "logging_config": {
|
||||
/// "log_format": "JSON",
|
||||
/// "application_log_level": "INFO"
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// - `-Dallow-principal=[string]`: AWS service principal to grant invoke permission
|
||||
/// (e.g., "alexa-appkit.amazon.com" for Alexa Skills Kit)
|
||||
///
|
||||
/// ## Deploy Output
|
||||
///
|
||||
|
|
@ -207,8 +170,6 @@ pub const BuildInfo = lambdabuild.BuildInfo;
|
|||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ### Basic Usage (uses lambda.json if present)
|
||||
///
|
||||
/// ```zig
|
||||
/// const lambda_zig = @import("lambda_zig");
|
||||
///
|
||||
|
|
@ -224,73 +185,27 @@ pub const BuildInfo = lambdabuild.BuildInfo;
|
|||
/// const exe = b.addExecutable(.{ ... });
|
||||
/// b.installArtifact(exe);
|
||||
///
|
||||
/// _ = try lambda_zig.configureBuild(b, lambda_zig_dep, exe, .{});
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ### Inline Configuration
|
||||
///
|
||||
/// ```zig
|
||||
/// _ = try lambda_zig.configureBuild(b, lambda_zig_dep, exe, .{
|
||||
/// .lambda_config = .{ .config = .{
|
||||
/// .role_name = "my_custom_role",
|
||||
/// .timeout = 30,
|
||||
/// .memory_size = 512,
|
||||
/// .allow_principal = "alexa-appkit.amazon.com",
|
||||
/// }},
|
||||
/// // Configure Lambda build and get deployment info
|
||||
/// const lambda = try lambda_zig.configureBuild(b, lambda_zig_dep, exe, .{
|
||||
/// .default_function_name = "my-function",
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// ### Custom Config File Path (required by default)
|
||||
///
|
||||
/// ```zig
|
||||
/// _ = try lambda_zig.configureBuild(b, lambda_zig_dep, exe, .{
|
||||
/// .lambda_config = .{ .file = .{ .path = b.path("deploy/production.json") } },
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// ### Optional Config File (silent defaults if missing)
|
||||
///
|
||||
/// ```zig
|
||||
/// _ = try lambda_zig.configureBuild(b, lambda_zig_dep, exe, .{
|
||||
/// .lambda_config = .{ .file = .{
|
||||
/// .path = b.path("lambda.json"),
|
||||
/// .required = false,
|
||||
/// } },
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// ### Dynamically Generated Config
|
||||
///
|
||||
/// ```zig
|
||||
/// const wf = b.addWriteFiles();
|
||||
/// const config_json = wf.add("lambda-config.json", generated_content);
|
||||
///
|
||||
/// _ = try lambda_zig.configureBuild(b, lambda_zig_dep, exe, .{
|
||||
/// .lambda_config = .{ .file = .{ .path = config_json } },
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// ### Using Deploy Output
|
||||
///
|
||||
/// ```zig
|
||||
/// const lambda = try lambda_zig.configureBuild(b, lambda_zig_dep, exe, .{});
|
||||
///
|
||||
/// // 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(
|
||||
b: *std.Build,
|
||||
lambda_zig_dep: *std.Build.Dependency,
|
||||
exe: *std.Build.Step.Compile,
|
||||
options: Options,
|
||||
) !BuildInfo {
|
||||
config: LambdaConfig,
|
||||
) !LambdaBuildInfo {
|
||||
// Get lambda_build from the lambda_zig dependency's Build context
|
||||
const lambda_build_dep = lambda_zig_dep.builder.dependency("lambda_build", .{
|
||||
.target = b.graph.host,
|
||||
.optimize = .ReleaseSafe,
|
||||
});
|
||||
return lambdabuild.configureBuild(b, lambda_build_dep, exe, options);
|
||||
return @import("lambdabuild.zig").configureBuild(b, lambda_build_dep, exe, config);
|
||||
}
|
||||
|
|
|
|||
138
lambdabuild.zig
138
lambdabuild.zig
|
|
@ -5,53 +5,25 @@
|
|||
|
||||
const std = @import("std");
|
||||
|
||||
pub const LambdaBuildConfig = @import("tools/build/src/LambdaBuildConfig.zig");
|
||||
|
||||
/// A config file path with explicit required/optional semantics.
|
||||
pub const ConfigFile = struct {
|
||||
path: std.Build.LazyPath,
|
||||
/// If true (default), error when file is missing. If false, silently use defaults.
|
||||
required: bool = true,
|
||||
};
|
||||
|
||||
/// Source for Lambda build configuration.
|
||||
///
|
||||
/// Determines how Lambda function settings (timeout, memory, VPC, etc.)
|
||||
/// and deployment settings (role_name, allow_principal) are provided.
|
||||
pub const LambdaConfigSource = union(enum) {
|
||||
/// No configuration file. Uses hardcoded defaults.
|
||||
none,
|
||||
|
||||
/// Path to a JSON config file with explicit required/optional semantics.
|
||||
file: ConfigFile,
|
||||
|
||||
/// Inline configuration. Will be serialized to JSON and
|
||||
/// written to a generated file.
|
||||
config: LambdaBuildConfig,
|
||||
};
|
||||
|
||||
/// Options for Lambda build integration.
|
||||
/// Configuration options for Lambda build integration.
|
||||
///
|
||||
/// These provide project-level defaults that can still be overridden
|
||||
/// via command-line options (e.g., `-Dfunction-name=...`).
|
||||
pub const Options = struct {
|
||||
pub const Config = struct {
|
||||
/// Default function name if not specified via -Dfunction-name.
|
||||
/// If null, falls back to the executable name (exe.name).
|
||||
default_function_name: ?[]const u8 = null,
|
||||
|
||||
/// Default IAM role name if not specified via -Drole-name.
|
||||
default_role_name: []const u8 = "lambda_basic_execution",
|
||||
|
||||
/// Default environment file if not specified via -Denv-file.
|
||||
/// If the file doesn't exist, it's silently skipped.
|
||||
default_env_file: ?[]const u8 = ".env",
|
||||
|
||||
/// Lambda build configuration source.
|
||||
/// Defaults to looking for "lambda.json" (optional - uses defaults if missing).
|
||||
///
|
||||
/// Examples:
|
||||
/// - `.none`: No config file, use defaults
|
||||
/// - `.{ .file = .{ .path = b.path("lambda.json") } }`: Required config file
|
||||
/// - `.{ .file = .{ .path = b.path("lambda.json"), .required = false } }`: Optional config file
|
||||
/// - `.{ .config = .{ ... } }`: Inline configuration
|
||||
lambda_config: LambdaConfigSource = .{ .file = .{ .path = .{ .cwd_relative = "lambda.json" }, .required = false } },
|
||||
/// Default AWS service principal to grant invoke permission.
|
||||
/// For Alexa skills, use "alexa-appkit.amazon.com".
|
||||
default_allow_principal: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
/// Information about the configured Lambda build steps.
|
||||
|
|
@ -88,39 +60,8 @@ pub const BuildInfo = struct {
|
|||
/// - awslambda_deploy: Deploy the function to AWS
|
||||
/// - awslambda_run: Invoke the deployed function
|
||||
///
|
||||
/// ## Configuration
|
||||
///
|
||||
/// Function settings (timeout, memory, VPC, etc.) and deployment settings
|
||||
/// (role_name, allow_principal) are configured via a JSON file or inline config.
|
||||
///
|
||||
/// By default, looks for `lambda.json` in the project root. If not found,
|
||||
/// uses sensible defaults (role_name = "lambda_basic_execution").
|
||||
///
|
||||
/// ### Example lambda.json
|
||||
///
|
||||
/// ```json
|
||||
/// {
|
||||
/// "role_name": "my_lambda_role",
|
||||
/// "timeout": 30,
|
||||
/// "memory_size": 512,
|
||||
/// "allow_principal": "alexa-appkit.amazon.com",
|
||||
/// "tags": [
|
||||
/// { "key": "Environment", "value": "production" }
|
||||
/// ]
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ### Inline Configuration
|
||||
///
|
||||
/// ```zig
|
||||
/// lambda.configureBuild(b, dep, exe, .{
|
||||
/// .lambda_config = .{ .config = .{
|
||||
/// .role_name = "my_role",
|
||||
/// .timeout = 30,
|
||||
/// .memory_size = 512,
|
||||
/// }},
|
||||
/// });
|
||||
/// ```
|
||||
/// The `config` parameter allows setting project-level defaults that can
|
||||
/// 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.
|
||||
|
|
@ -128,15 +69,20 @@ pub fn configureBuild(
|
|||
b: *std.Build,
|
||||
lambda_build_dep: *std.Build.Dependency,
|
||||
exe: *std.Build.Step.Compile,
|
||||
options: Options,
|
||||
config: Config,
|
||||
) !BuildInfo {
|
||||
// Get the lambda-build CLI artifact from the dependency
|
||||
const cli = lambda_build_dep.artifact("lambda-build");
|
||||
|
||||
// Get configuration options (command-line overrides config defaults)
|
||||
const function_name = b.option([]const u8, "function-name", "Function name for Lambda") orelse options.default_function_name orelse exe.name;
|
||||
const function_name = b.option([]const u8, "function-name", "Function name for Lambda") orelse config.default_function_name orelse exe.name;
|
||||
const region = b.option([]const u8, "region", "AWS region") orelse null;
|
||||
const profile = b.option([]const u8, "profile", "AWS profile") orelse null;
|
||||
const role_name = b.option(
|
||||
[]const u8,
|
||||
"role-name",
|
||||
"IAM role name (default: lambda_basic_execution)",
|
||||
) orelse config.default_role_name;
|
||||
const payload = b.option(
|
||||
[]const u8,
|
||||
"payload",
|
||||
|
|
@ -146,12 +92,12 @@ pub fn configureBuild(
|
|||
[]const u8,
|
||||
"env-file",
|
||||
"Path to environment variables file (KEY=VALUE format)",
|
||||
) orelse options.default_env_file;
|
||||
const config_file_override = b.option(
|
||||
) orelse config.default_env_file;
|
||||
const allow_principal = b.option(
|
||||
[]const u8,
|
||||
"awslambda-config-file",
|
||||
"Path to Lambda build config JSON file (overrides function_config)",
|
||||
);
|
||||
"allow-principal",
|
||||
"AWS service principal to grant invoke permission (e.g., alexa-appkit.amazon.com)",
|
||||
) orelse config.default_allow_principal;
|
||||
|
||||
// Determine architecture for Lambda
|
||||
const target_arch = exe.root_module.resolved_target.?.result.cpu.arch;
|
||||
|
|
@ -166,39 +112,6 @@ pub fn configureBuild(
|
|||
}
|
||||
};
|
||||
|
||||
// Determine config file source - resolves to a path and required flag
|
||||
// Internal struct since we need nullable path for the .none case
|
||||
const ResolvedConfig = struct {
|
||||
path: ?std.Build.LazyPath,
|
||||
required: bool,
|
||||
};
|
||||
|
||||
const config_file: ResolvedConfig = if (config_file_override) |override|
|
||||
.{ .path = .{ .cwd_relative = override }, .required = true }
|
||||
else switch (options.lambda_config) {
|
||||
.none => .{ .path = null, .required = false },
|
||||
.file => |cf| .{ .path = cf.path, .required = cf.required },
|
||||
.config => |func_config| blk: {
|
||||
// Serialize inline config to JSON and write to generated file
|
||||
const json_content = std.fmt.allocPrint(b.allocator, "{f}", .{
|
||||
std.json.fmt(func_config, .{}),
|
||||
}) catch @panic("OOM");
|
||||
const wf = b.addWriteFiles();
|
||||
break :blk .{ .path = wf.add("lambda-config.json", json_content), .required = true };
|
||||
},
|
||||
};
|
||||
|
||||
// Helper to add config file arg to a command
|
||||
const addConfigArg = struct {
|
||||
fn add(cmd: *std.Build.Step.Run, file: ResolvedConfig) void {
|
||||
if (file.path) |f| {
|
||||
const flag = if (file.required) "--config-file" else "--config-file-optional";
|
||||
cmd.addArg(flag);
|
||||
cmd.addFileArg(f);
|
||||
}
|
||||
}
|
||||
}.add;
|
||||
|
||||
// Package step - output goes to cache based on input hash
|
||||
const package_cmd = b.addRunArtifact(cli);
|
||||
package_cmd.step.name = try std.fmt.allocPrint(b.allocator, "{s} package", .{cli.name});
|
||||
|
|
@ -216,8 +129,7 @@ pub fn configureBuild(
|
|||
iam_cmd.step.name = try std.fmt.allocPrint(b.allocator, "{s} iam", .{cli.name});
|
||||
if (profile) |p| iam_cmd.addArgs(&.{ "--profile", p });
|
||||
if (region) |r| iam_cmd.addArgs(&.{ "--region", r });
|
||||
iam_cmd.addArg("iam");
|
||||
addConfigArg(iam_cmd, config_file);
|
||||
iam_cmd.addArgs(&.{ "iam", "--role-name", role_name });
|
||||
|
||||
const iam_step = b.step("awslambda_iam", "Create/verify IAM role for Lambda");
|
||||
iam_step.dependOn(&iam_cmd.step);
|
||||
|
|
@ -238,11 +150,13 @@ pub fn configureBuild(
|
|||
});
|
||||
deploy_cmd.addFileArg(zip_output);
|
||||
deploy_cmd.addArgs(&.{
|
||||
"--role-name",
|
||||
role_name,
|
||||
"--arch",
|
||||
arch_str,
|
||||
});
|
||||
if (env_file) |ef| deploy_cmd.addArgs(&.{ "--env-file", ef });
|
||||
addConfigArg(deploy_cmd, config_file);
|
||||
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");
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
.fingerprint = 0x6e61de08e7e51114,
|
||||
.dependencies = .{
|
||||
.aws = .{
|
||||
.url = "git+https://git.lerch.org/lobo/aws-sdk-for-zig#1a03250fbeb2840ab8b6010f1ad4e899cdfc185a",
|
||||
.hash = "aws-0.0.1-SbsFcCg7CgC0yYv2Y7aOjonSAU3mltOSfY0x2w9jZlMV",
|
||||
.url = "git+https://git.lerch.org/lobo/aws-sdk-for-zig#5c7aed071f6251d53a1627080a21d604ff58f0a5",
|
||||
.hash = "aws-0.0.1-SbsFcFE7CgDBilPa15i4gIB6Qr5ozBz328O63abDQDDk",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
|
|
|
|||
|
|
@ -1,195 +0,0 @@
|
|||
//! Lambda build configuration types.
|
||||
//!
|
||||
//! These types define the JSON schema for lambda.json configuration files,
|
||||
//! encompassing IAM, Lambda function, and deployment settings.
|
||||
//!
|
||||
//! Used by both the build system (lambdabuild.zig) and the CLI commands
|
||||
//! (deploy.zig, iam.zig).
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const LambdaBuildConfig = @This();
|
||||
|
||||
/// Wrapper for parsed config that owns both the JSON parse result
|
||||
/// and the source file data (since parsed strings point into it).
|
||||
pub const Parsed = struct {
|
||||
parsed: std.json.Parsed(LambdaBuildConfig),
|
||||
source_data: []const u8,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
pub fn deinit(self: *Parsed) void {
|
||||
self.parsed.deinit();
|
||||
self.allocator.free(self.source_data);
|
||||
}
|
||||
};
|
||||
|
||||
// === IAM Configuration ===
|
||||
|
||||
/// IAM role name for the Lambda function.
|
||||
role_name: []const u8 = "lambda_basic_execution",
|
||||
// Future: policy_statements, trust_policy, etc.
|
||||
|
||||
// === Deployment Settings ===
|
||||
|
||||
/// AWS service principal to grant invoke permission.
|
||||
/// Example: "alexa-appkit.amazon.com" for Alexa Skills.
|
||||
allow_principal: ?[]const u8 = null,
|
||||
|
||||
// === Lambda Function Configuration ===
|
||||
|
||||
/// Human-readable description of the function.
|
||||
description: ?[]const u8 = null,
|
||||
|
||||
/// Maximum execution time in seconds (1-900).
|
||||
timeout: ?i64 = null,
|
||||
|
||||
/// Memory allocation in MB (128-10240).
|
||||
memory_size: ?i64 = null,
|
||||
|
||||
/// KMS key ARN for environment variable encryption.
|
||||
kmskey_arn: ?[]const u8 = null,
|
||||
|
||||
// Nested configs
|
||||
vpc_config: ?VpcConfig = null,
|
||||
dead_letter_config: ?DeadLetterConfig = null,
|
||||
tracing_config: ?TracingConfig = null,
|
||||
ephemeral_storage: ?EphemeralStorage = null,
|
||||
logging_config: ?LoggingConfig = null,
|
||||
|
||||
// Collections
|
||||
tags: ?[]const Tag = null,
|
||||
layers: ?[]const []const u8 = null,
|
||||
|
||||
pub const VpcConfig = struct {
|
||||
subnet_ids: ?[]const []const u8 = null,
|
||||
security_group_ids: ?[]const []const u8 = null,
|
||||
ipv6_allowed_for_dual_stack: ?bool = null,
|
||||
};
|
||||
|
||||
pub const DeadLetterConfig = struct {
|
||||
target_arn: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const TracingConfig = struct {
|
||||
/// "Active" or "PassThrough"
|
||||
mode: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const EphemeralStorage = struct {
|
||||
/// Size in MB (512-10240)
|
||||
size: i64,
|
||||
};
|
||||
|
||||
pub const LoggingConfig = struct {
|
||||
/// "JSON" or "Text"
|
||||
log_format: ?[]const u8 = null,
|
||||
/// "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"
|
||||
application_log_level: ?[]const u8 = null,
|
||||
system_log_level: ?[]const u8 = null,
|
||||
log_group: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const Tag = struct {
|
||||
key: []const u8,
|
||||
value: []const u8,
|
||||
};
|
||||
|
||||
/// Validate configuration values are within AWS limits.
|
||||
pub fn validate(self: LambdaBuildConfig) !void {
|
||||
// Timeout: 1-900 seconds
|
||||
if (self.timeout) |t| {
|
||||
if (t < 1 or t > 900) {
|
||||
std.log.err("Invalid timeout: {} (must be 1-900 seconds)", .{t});
|
||||
return error.InvalidTimeout;
|
||||
}
|
||||
}
|
||||
|
||||
// Memory: 128-10240 MB
|
||||
if (self.memory_size) |m| {
|
||||
if (m < 128 or m > 10240) {
|
||||
std.log.err("Invalid memory_size: {} (must be 128-10240 MB)", .{m});
|
||||
return error.InvalidMemorySize;
|
||||
}
|
||||
}
|
||||
|
||||
// Ephemeral storage: 512-10240 MB
|
||||
if (self.ephemeral_storage) |es| {
|
||||
if (es.size < 512 or es.size > 10240) {
|
||||
std.log.err("Invalid ephemeral_storage.size: {} (must be 512-10240 MB)", .{es.size});
|
||||
return error.InvalidEphemeralStorage;
|
||||
}
|
||||
}
|
||||
|
||||
// Tracing mode validation
|
||||
if (self.tracing_config) |tc| {
|
||||
if (tc.mode) |mode| {
|
||||
if (!std.mem.eql(u8, mode, "Active") and !std.mem.eql(u8, mode, "PassThrough")) {
|
||||
std.log.err("Invalid tracing_config.mode: '{s}' (must be 'Active' or 'PassThrough')", .{mode});
|
||||
return error.InvalidTracingMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log format validation
|
||||
if (self.logging_config) |lc| {
|
||||
if (lc.log_format) |format| {
|
||||
if (!std.mem.eql(u8, format, "JSON") and !std.mem.eql(u8, format, "Text")) {
|
||||
std.log.err("Invalid logging_config.log_format: '{s}' (must be 'JSON' or 'Text')", .{format});
|
||||
return error.InvalidLogFormat;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load configuration from a JSON file.
|
||||
///
|
||||
/// If is_default is true and the file doesn't exist, returns null.
|
||||
/// If is_default is false (explicitly specified) and file doesn't exist, returns error.
|
||||
pub fn loadFromFile(
|
||||
allocator: std.mem.Allocator,
|
||||
path: []const u8,
|
||||
is_default: bool,
|
||||
) !?Parsed {
|
||||
const file = std.fs.cwd().openFile(path, .{}) catch |err| {
|
||||
if (err == error.FileNotFound) {
|
||||
if (is_default) {
|
||||
std.log.debug("Config file '{s}' not found, using defaults", .{path});
|
||||
return null;
|
||||
}
|
||||
std.log.err("Config file not found: {s}", .{path});
|
||||
return error.ConfigFileNotFound;
|
||||
}
|
||||
std.log.err("Failed to open config file '{s}': {}", .{ path, err });
|
||||
return error.ConfigFileOpenError;
|
||||
};
|
||||
defer file.close();
|
||||
|
||||
// Read entire file
|
||||
var read_buffer: [4096]u8 = undefined;
|
||||
var file_reader = file.reader(&read_buffer);
|
||||
const content = file_reader.interface.allocRemaining(allocator, std.Io.Limit.limited(64 * 1024)) catch |err| {
|
||||
std.log.err("Error reading config file: {}", .{err});
|
||||
return error.ConfigFileReadError;
|
||||
};
|
||||
errdefer allocator.free(content);
|
||||
|
||||
// Parse JSON - strings will point into content, which we keep alive
|
||||
const parsed = std.json.parseFromSlice(
|
||||
LambdaBuildConfig,
|
||||
allocator,
|
||||
content,
|
||||
.{},
|
||||
) catch |err| {
|
||||
std.log.err("Error parsing config JSON: {}", .{err});
|
||||
return error.ConfigFileParseError;
|
||||
};
|
||||
errdefer parsed.deinit();
|
||||
|
||||
try parsed.value.validate();
|
||||
|
||||
return .{
|
||||
.parsed = parsed,
|
||||
.source_data = content,
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
|
@ -2,13 +2,11 @@
|
|||
//!
|
||||
//! Creates a new function or updates an existing one.
|
||||
//! Supports setting environment variables via --env or --env-file.
|
||||
//! Function configuration (timeout, memory, VPC, etc.) comes from --config-file.
|
||||
|
||||
const std = @import("std");
|
||||
const aws = @import("aws");
|
||||
const iam_cmd = @import("iam.zig");
|
||||
const RunOptions = @import("main.zig").RunOptions;
|
||||
const LambdaBuildConfig = @import("LambdaBuildConfig.zig");
|
||||
|
||||
// Get Lambda EnvironmentVariableKeyValue type from AWS SDK
|
||||
const EnvVar = aws.services.lambda.EnvironmentVariableKeyValue;
|
||||
|
|
@ -17,10 +15,10 @@ pub fn run(args: []const []const u8, options: RunOptions) !void {
|
|||
var function_name: ?[]const u8 = null;
|
||||
var zip_file: ?[]const u8 = null;
|
||||
var role_arn: ?[]const u8 = null;
|
||||
var role_name: []const u8 = "lambda_basic_execution";
|
||||
var arch: ?[]const u8 = null;
|
||||
var allow_principal: ?[]const u8 = null;
|
||||
var deploy_output: ?[]const u8 = null;
|
||||
var config_file: ?[]const u8 = null;
|
||||
var is_config_required = false;
|
||||
|
||||
// Environment variables storage
|
||||
var env_vars = std.StringHashMap([]const u8).init(options.allocator);
|
||||
|
|
@ -48,6 +46,10 @@ pub fn run(args: []const []const u8, options: RunOptions) !void {
|
|||
i += 1;
|
||||
if (i >= args.len) return error.MissingRoleArn;
|
||||
role_arn = args[i];
|
||||
} else if (std.mem.eql(u8, arg, "--role-name")) {
|
||||
i += 1;
|
||||
if (i >= args.len) return error.MissingRoleName;
|
||||
role_name = args[i];
|
||||
} else if (std.mem.eql(u8, arg, "--arch")) {
|
||||
i += 1;
|
||||
if (i >= args.len) return error.MissingArch;
|
||||
|
|
@ -60,16 +62,10 @@ pub fn run(args: []const []const u8, options: RunOptions) !void {
|
|||
i += 1;
|
||||
if (i >= args.len) return error.MissingEnvFile;
|
||||
try loadEnvFile(args[i], &env_vars, options.allocator);
|
||||
} else if (std.mem.eql(u8, arg, "--config-file")) {
|
||||
} else if (std.mem.eql(u8, arg, "--allow-principal")) {
|
||||
i += 1;
|
||||
if (i >= args.len) return error.MissingConfigFile;
|
||||
config_file = args[i];
|
||||
is_config_required = true;
|
||||
} else if (std.mem.eql(u8, arg, "--config-file-optional")) {
|
||||
i += 1;
|
||||
if (i >= args.len) return error.MissingConfigFile;
|
||||
config_file = args[i];
|
||||
is_config_required = false;
|
||||
if (i >= args.len) return error.MissingAllowPrincipal;
|
||||
allow_principal = args[i];
|
||||
} else if (std.mem.eql(u8, arg, "--deploy-output")) {
|
||||
i += 1;
|
||||
if (i >= args.len) return error.MissingDeployOutput;
|
||||
|
|
@ -99,21 +95,15 @@ pub fn run(args: []const []const u8, options: RunOptions) !void {
|
|||
return error.MissingZipFile;
|
||||
}
|
||||
|
||||
// Load config file if provided
|
||||
var parsed_config = if (config_file) |path|
|
||||
try LambdaBuildConfig.loadFromFile(options.allocator, path, !is_config_required)
|
||||
else
|
||||
null;
|
||||
defer if (parsed_config) |*pc| pc.deinit();
|
||||
|
||||
try deployFunction(.{
|
||||
.function_name = function_name.?,
|
||||
.zip_file = zip_file.?,
|
||||
.role_arn = role_arn,
|
||||
.role_name = role_name,
|
||||
.arch = arch,
|
||||
.env_vars = if (env_vars.count() > 0) &env_vars else null,
|
||||
.allow_principal = allow_principal,
|
||||
.deploy_output = deploy_output,
|
||||
.config = if (parsed_config) |pc| &pc.parsed.value else null,
|
||||
}, options);
|
||||
}
|
||||
|
||||
|
|
@ -202,25 +192,15 @@ fn printHelp(writer: anytype) void {
|
|||
\\ --function-name <name> Name of the Lambda function (required)
|
||||
\\ --zip-file <path> Path to the deployment zip (required)
|
||||
\\ --role-arn <arn> IAM role ARN (optional - creates role if omitted)
|
||||
\\ --role-name <name> IAM role name if creating (default: lambda_basic_execution)
|
||||
\\ --arch <arch> Architecture: x86_64 or aarch64 (default: x86_64)
|
||||
\\ --env <KEY=VALUE> Set environment variable (can be repeated)
|
||||
\\ --env-file <path> Load environment variables from file
|
||||
\\ --config-file <path> Path to JSON config file (required, error if missing)
|
||||
\\ --config-file-optional <path> Path to JSON config file (optional, use defaults if missing)
|
||||
\\ --deploy-output <path> Write deployment info to JSON file
|
||||
\\ --env-file <path> Load environment variables from file (KEY=VALUE format)
|
||||
\\ --allow-principal <p> Grant invoke permission to AWS service principal
|
||||
\\ (e.g., alexa-appkit.amazon.com)
|
||||
\\ --deploy-output <path> Write deployment info (ARN, region, etc.) to JSON file
|
||||
\\ --help, -h Show this help message
|
||||
\\
|
||||
\\Config File:
|
||||
\\ The config file specifies function settings:
|
||||
\\ {{
|
||||
\\ "role_name": "my_lambda_role",
|
||||
\\ "timeout": 30,
|
||||
\\ "memory_size": 512,
|
||||
\\ "allow_principal": "alexa-appkit.amazon.com",
|
||||
\\ "description": "My function",
|
||||
\\ "tags": [{{ "key": "Env", "value": "prod" }}]
|
||||
\\ }}
|
||||
\\
|
||||
\\Environment File Format:
|
||||
\\ The --env-file option reads a file with KEY=VALUE pairs, one per line.
|
||||
\\ Lines starting with # are treated as comments. Empty lines are ignored.
|
||||
|
|
@ -240,10 +220,11 @@ const DeployOptions = struct {
|
|||
function_name: []const u8,
|
||||
zip_file: []const u8,
|
||||
role_arn: ?[]const u8,
|
||||
role_name: []const u8,
|
||||
arch: ?[]const u8,
|
||||
env_vars: ?*const std.StringHashMap([]const u8),
|
||||
allow_principal: ?[]const u8,
|
||||
deploy_output: ?[]const u8,
|
||||
config: ?*const LambdaBuildConfig,
|
||||
};
|
||||
|
||||
fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
|
||||
|
|
@ -253,14 +234,11 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
|
|||
return error.InvalidArchitecture;
|
||||
}
|
||||
|
||||
// Get role_name from config or use default
|
||||
const role_name = if (deploy_opts.config) |c| c.role_name else "lambda_basic_execution";
|
||||
|
||||
// Get or create IAM role if not provided
|
||||
const role_arn = if (deploy_opts.role_arn) |r|
|
||||
try options.allocator.dupe(u8, r)
|
||||
else
|
||||
try iam_cmd.getOrCreateRole(role_name, options);
|
||||
try iam_cmd.getOrCreateRole(deploy_opts.role_name, options);
|
||||
|
||||
defer options.allocator.free(role_arn);
|
||||
|
||||
|
|
@ -298,58 +276,6 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
|
|||
options.allocator.free(vars);
|
||||
};
|
||||
|
||||
// Build config-based parameters
|
||||
const config = deploy_opts.config;
|
||||
|
||||
// Build tags array if present in config
|
||||
const tags = if (config) |c| if (c.tags) |t| blk: {
|
||||
var tag_arr = try options.allocator.alloc(aws.services.lambda.TagKeyValue, t.len);
|
||||
for (t, 0..) |tag, idx| {
|
||||
tag_arr[idx] = .{ .key = tag.key, .value = tag.value };
|
||||
}
|
||||
break :blk tag_arr;
|
||||
} else null else null;
|
||||
defer if (tags) |t| options.allocator.free(t);
|
||||
|
||||
// Build VPC config if present
|
||||
const vpc_config: ?aws.services.lambda.VpcConfig = if (config) |c| if (c.vpc_config) |vc|
|
||||
.{
|
||||
.subnet_ids = if (vc.subnet_ids) |ids| @constCast(ids) else null,
|
||||
.security_group_ids = if (vc.security_group_ids) |ids| @constCast(ids) else null,
|
||||
.ipv6_allowed_for_dual_stack = vc.ipv6_allowed_for_dual_stack,
|
||||
}
|
||||
else
|
||||
null else null;
|
||||
|
||||
// Build dead letter config if present
|
||||
const dead_letter_config: ?aws.services.lambda.DeadLetterConfig = if (config) |c| if (c.dead_letter_config) |dlc|
|
||||
.{ .target_arn = dlc.target_arn }
|
||||
else
|
||||
null else null;
|
||||
|
||||
// Build tracing config if present
|
||||
const tracing_config: ?aws.services.lambda.TracingConfig = if (config) |c| if (c.tracing_config) |tc|
|
||||
.{ .mode = tc.mode }
|
||||
else
|
||||
null else null;
|
||||
|
||||
// Build ephemeral storage if present
|
||||
const ephemeral_storage: ?aws.services.lambda.EphemeralStorage = if (config) |c| if (c.ephemeral_storage) |es|
|
||||
.{ .size = es.size }
|
||||
else
|
||||
null else null;
|
||||
|
||||
// Build logging config if present
|
||||
const logging_config: ?aws.services.lambda.LoggingConfig = if (config) |c| if (c.logging_config) |lc|
|
||||
.{
|
||||
.log_format = lc.log_format,
|
||||
.application_log_level = lc.application_log_level,
|
||||
.system_log_level = lc.system_log_level,
|
||||
.log_group = lc.log_group,
|
||||
}
|
||||
else
|
||||
null else null;
|
||||
|
||||
// Try to create the function first - if it already exists, we'll update it
|
||||
std.log.info("Attempting to create function: {s}", .{deploy_opts.function_name});
|
||||
|
||||
|
|
@ -378,18 +304,6 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
|
|||
.runtime = "provided.al2023",
|
||||
.role = role_arn,
|
||||
.environment = if (env_variables) |vars| .{ .variables = vars } else null,
|
||||
// Config-based parameters
|
||||
.description = if (config) |c| c.description else null,
|
||||
.timeout = if (config) |c| c.timeout else null,
|
||||
.memory_size = if (config) |c| c.memory_size else null,
|
||||
.kmskey_arn = if (config) |c| c.kmskey_arn else null,
|
||||
.vpc_config = vpc_config,
|
||||
.dead_letter_config = dead_letter_config,
|
||||
.tracing_config = tracing_config,
|
||||
.ephemeral_storage = ephemeral_storage,
|
||||
.logging_config = logging_config,
|
||||
.tags = tags,
|
||||
.layers = if (config) |c| if (c.layers) |l| @constCast(l) else null else null,
|
||||
}, create_options) catch |err| {
|
||||
defer create_diagnostics.deinit();
|
||||
|
||||
|
|
@ -414,23 +328,20 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
|
|||
// Wait for function to be ready before updating configuration
|
||||
try waitForFunctionReady(deploy_opts.function_name, options);
|
||||
|
||||
// Update function configuration if we have config or env variables
|
||||
if (config != null or env_variables != null)
|
||||
try updateFunctionConfiguration(
|
||||
deploy_opts.function_name,
|
||||
env_variables,
|
||||
config,
|
||||
options,
|
||||
);
|
||||
// Update environment variables if provided
|
||||
if (env_variables) |vars| {
|
||||
try updateFunctionConfiguration(deploy_opts.function_name, vars, options);
|
||||
}
|
||||
|
||||
// Add invoke permission if requested
|
||||
if (config) |c|
|
||||
if (c.allow_principal) |principal|
|
||||
if (deploy_opts.allow_principal) |principal| {
|
||||
try addPermission(deploy_opts.function_name, principal, options);
|
||||
}
|
||||
|
||||
// Write deploy output if requested
|
||||
if (deploy_opts.deploy_output) |output_path|
|
||||
if (deploy_opts.deploy_output) |output_path| {
|
||||
try writeDeployOutput(output_path, function_arn.?, role_arn, lambda_arch, deploy_opts.env_vars);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -453,13 +364,14 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
|
|||
try waitForFunctionReady(deploy_opts.function_name, options);
|
||||
|
||||
// Add invoke permission if requested
|
||||
if (config) |c|
|
||||
if (c.allow_principal) |principal|
|
||||
if (deploy_opts.allow_principal) |principal| {
|
||||
try addPermission(deploy_opts.function_name, principal, options);
|
||||
}
|
||||
|
||||
// Write deploy output if requested
|
||||
if (deploy_opts.deploy_output) |output_path|
|
||||
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
|
||||
|
|
@ -486,74 +398,23 @@ fn buildEnvVariables(
|
|||
return result;
|
||||
}
|
||||
|
||||
/// Update function configuration (environment variables and config settings)
|
||||
/// Update function configuration (environment variables)
|
||||
fn updateFunctionConfiguration(
|
||||
function_name: []const u8,
|
||||
env_variables: ?[]EnvVar,
|
||||
config: ?*const LambdaBuildConfig,
|
||||
env_variables: []EnvVar,
|
||||
options: RunOptions,
|
||||
) !void {
|
||||
const services = aws.Services(.{.lambda}){};
|
||||
|
||||
std.log.info("Updating function configuration for: {s}", .{function_name});
|
||||
|
||||
// Build VPC config if present
|
||||
const vpc_config: ?aws.services.lambda.VpcConfig = if (config) |c| if (c.vpc_config) |vc|
|
||||
.{
|
||||
.subnet_ids = if (vc.subnet_ids) |ids| @constCast(ids) else null,
|
||||
.security_group_ids = if (vc.security_group_ids) |ids| @constCast(ids) else null,
|
||||
.ipv6_allowed_for_dual_stack = vc.ipv6_allowed_for_dual_stack,
|
||||
}
|
||||
else
|
||||
null else null;
|
||||
|
||||
// Build dead letter config if present
|
||||
const dead_letter_config: ?aws.services.lambda.DeadLetterConfig = if (config) |c| if (c.dead_letter_config) |dlc|
|
||||
.{ .target_arn = dlc.target_arn }
|
||||
else
|
||||
null else null;
|
||||
|
||||
// Build tracing config if present
|
||||
const tracing_config: ?aws.services.lambda.TracingConfig = if (config) |c| if (c.tracing_config) |tc|
|
||||
.{ .mode = tc.mode }
|
||||
else
|
||||
null else null;
|
||||
|
||||
// Build ephemeral storage if present
|
||||
const ephemeral_storage: ?aws.services.lambda.EphemeralStorage = if (config) |c| if (c.ephemeral_storage) |es|
|
||||
.{ .size = es.size }
|
||||
else
|
||||
null else null;
|
||||
|
||||
// Build logging config if present
|
||||
const logging_config: ?aws.services.lambda.LoggingConfig = if (config) |c| if (c.logging_config) |lc|
|
||||
.{
|
||||
.log_format = lc.log_format,
|
||||
.application_log_level = lc.application_log_level,
|
||||
.system_log_level = lc.system_log_level,
|
||||
.log_group = lc.log_group,
|
||||
}
|
||||
else
|
||||
null else null;
|
||||
|
||||
const update_config_result = try aws.Request(services.lambda.update_function_configuration).call(.{
|
||||
.function_name = function_name,
|
||||
.environment = if (env_variables) |vars| .{ .variables = vars } else null,
|
||||
// Config-based parameters
|
||||
.description = if (config) |c| c.description else null,
|
||||
.timeout = if (config) |c| c.timeout else null,
|
||||
.memory_size = if (config) |c| c.memory_size else null,
|
||||
.kmskey_arn = if (config) |c| c.kmskey_arn else null,
|
||||
.vpc_config = vpc_config,
|
||||
.dead_letter_config = dead_letter_config,
|
||||
.tracing_config = tracing_config,
|
||||
.ephemeral_storage = ephemeral_storage,
|
||||
.logging_config = logging_config,
|
||||
.layers = if (config) |c| if (c.layers) |l| @constCast(l) else null else null,
|
||||
.environment = .{ .variables = env_variables },
|
||||
}, options.aws_options);
|
||||
defer update_config_result.deinit();
|
||||
|
||||
try options.stdout.print("Updated function configuration\n", .{});
|
||||
try options.stdout.print("Updated environment variables\n", .{});
|
||||
try options.stdout.flush();
|
||||
|
||||
// Wait for configuration update to complete
|
||||
|
|
@ -576,17 +437,21 @@ fn waitForFunctionReady(function_name: []const u8, options: RunOptions) !void {
|
|||
defer result.deinit();
|
||||
|
||||
// Check if function is ready
|
||||
if (result.response.configuration) |cfg| {
|
||||
if (cfg.last_update_status) |status| {
|
||||
if (result.response.configuration) |config| {
|
||||
if (config.last_update_status) |status| {
|
||||
if (std.mem.eql(u8, status, "Successful")) {
|
||||
std.log.debug("Function is ready", .{});
|
||||
std.log.info("Function is ready", .{});
|
||||
return;
|
||||
} else if (std.mem.eql(u8, status, "Failed")) {
|
||||
return error.FunctionUpdateFailed;
|
||||
}
|
||||
// "InProgress" - keep waiting
|
||||
} else return; // No status means it's ready
|
||||
} else return; // No configuration means we can't check, assume ready
|
||||
} else {
|
||||
return; // No status means it's ready
|
||||
}
|
||||
} else {
|
||||
return; // No configuration means we can't check, assume ready
|
||||
}
|
||||
|
||||
std.Thread.sleep(200 * std.time.ns_per_ms);
|
||||
}
|
||||
|
|
@ -679,7 +544,7 @@ fn writeDeployOutput(
|
|||
const region = arn_parts.next() orelse return error.InvalidArn;
|
||||
const account_id = arn_parts.next() orelse return error.InvalidArn;
|
||||
_ = arn_parts.next(); // function
|
||||
const fn_name = arn_parts.next() orelse return error.InvalidArn;
|
||||
const function_name = arn_parts.next() orelse return error.InvalidArn;
|
||||
|
||||
const file = try std.fs.cwd().createFile(output_path, .{});
|
||||
defer file.close();
|
||||
|
|
@ -699,7 +564,7 @@ fn writeDeployOutput(
|
|||
\\ "role_arn": "{s}",
|
||||
\\ "architecture": "{s}",
|
||||
\\ "environment_keys": [
|
||||
, .{ function_arn, fn_name, partition, region, account_id, role_arn, architecture });
|
||||
, .{ function_arn, function_name, partition, region, account_id, role_arn, architecture });
|
||||
|
||||
// Write environment variable keys
|
||||
if (env_vars) |vars| {
|
||||
|
|
|
|||
|
|
@ -3,25 +3,17 @@
|
|||
const std = @import("std");
|
||||
const aws = @import("aws");
|
||||
const RunOptions = @import("main.zig").RunOptions;
|
||||
const LambdaBuildConfig = @import("LambdaBuildConfig.zig");
|
||||
|
||||
pub fn run(args: []const []const u8, options: RunOptions) !void {
|
||||
var config_file: ?[]const u8 = null;
|
||||
var is_config_required = false;
|
||||
var role_name: ?[]const u8 = null;
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < args.len) : (i += 1) {
|
||||
const arg = args[i];
|
||||
if (std.mem.eql(u8, arg, "--config-file")) {
|
||||
if (std.mem.eql(u8, arg, "--role-name")) {
|
||||
i += 1;
|
||||
if (i >= args.len) return error.MissingConfigFile;
|
||||
config_file = args[i];
|
||||
is_config_required = true;
|
||||
} else if (std.mem.eql(u8, arg, "--config-file-optional")) {
|
||||
i += 1;
|
||||
if (i >= args.len) return error.MissingConfigFile;
|
||||
config_file = args[i];
|
||||
is_config_required = false;
|
||||
if (i >= args.len) return error.MissingRoleName;
|
||||
role_name = args[i];
|
||||
} else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
|
||||
printHelp(options.stdout);
|
||||
try options.stdout.flush();
|
||||
|
|
@ -33,44 +25,30 @@ pub fn run(args: []const []const u8, options: RunOptions) !void {
|
|||
}
|
||||
}
|
||||
|
||||
// Load config file if provided
|
||||
var parsed_config = if (config_file) |path|
|
||||
try LambdaBuildConfig.loadFromFile(options.allocator, path, !is_config_required)
|
||||
else
|
||||
null;
|
||||
defer if (parsed_config) |*pc| pc.deinit();
|
||||
if (role_name == null) {
|
||||
try options.stderr.print("Error: --role-name is required\n", .{});
|
||||
printHelp(options.stderr);
|
||||
try options.stderr.flush();
|
||||
return error.MissingRoleName;
|
||||
}
|
||||
|
||||
// Get role_name from config or use default
|
||||
const role_name = if (parsed_config) |pc|
|
||||
pc.parsed.value.role_name
|
||||
else
|
||||
"lambda_basic_execution";
|
||||
|
||||
const arn = try getOrCreateRole(role_name, options);
|
||||
const arn = try getOrCreateRole(role_name.?, options);
|
||||
defer options.allocator.free(arn);
|
||||
|
||||
try options.stdout.print("{s}\n", .{arn});
|
||||
try options.stdout.flush();
|
||||
}
|
||||
|
||||
fn printHelp(writer: anytype) void {
|
||||
fn printHelp(writer: *std.Io.Writer) void {
|
||||
writer.print(
|
||||
\\Usage: lambda-build iam [options]
|
||||
\\
|
||||
\\Create or retrieve an IAM role for Lambda execution.
|
||||
\\
|
||||
\\Options:
|
||||
\\ --config-file <path> Path to JSON config file (required, error if missing)
|
||||
\\ --config-file-optional <path> Path to JSON config file (optional, use defaults if missing)
|
||||
\\ --role-name <name> Name of the IAM role (required)
|
||||
\\ --help, -h Show this help message
|
||||
\\
|
||||
\\Config File:
|
||||
\\ The config file can specify the IAM role name:
|
||||
\\ {{
|
||||
\\ "role_name": "my_lambda_role"
|
||||
\\ }}
|
||||
\\
|
||||
\\If no config file is provided, uses "lambda_basic_execution" as the role name.
|
||||
\\If the role exists, its ARN is returned. If not, a new role is created
|
||||
\\with the AWSLambdaExecute policy attached.
|
||||
\\
|
||||
|
|
@ -93,20 +71,19 @@ pub fn getOrCreateRole(role_name: []const u8, options: RunOptions) ![]const u8 {
|
|||
// Use the shared aws_options but add diagnostics for this call
|
||||
var aws_options = options.aws_options;
|
||||
aws_options.diagnostics = &diagnostics;
|
||||
defer aws_options.diagnostics = null;
|
||||
|
||||
const get_result = aws.Request(services.iam.get_role).call(.{
|
||||
.role_name = role_name,
|
||||
}, aws_options) catch |err| {
|
||||
defer diagnostics.deinit();
|
||||
|
||||
// Check for "not found" via HTTP status or error response body
|
||||
if (diagnostics.response_status == .not_found or
|
||||
std.mem.indexOf(u8, diagnostics.response_body, "NoSuchEntity") != null)
|
||||
if (diagnostics.response_status == .not_found) {
|
||||
// Role doesn't exist, create it
|
||||
return try createRole(role_name, options);
|
||||
|
||||
std.log.err("IAM GetRole failed: {} (HTTP {})", .{ err, diagnostics.response_status });
|
||||
}
|
||||
std.log.err(
|
||||
"IAM GetRole failed: {} (HTTP Response code {})",
|
||||
.{ err, diagnostics.response_status },
|
||||
);
|
||||
return error.IamGetRoleFailed;
|
||||
};
|
||||
defer get_result.deinit();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue