rework configuration
A config file option is now available for all non-managed parameters to CreateFunction. This data can also be passed via build configuration if desired. Net net, we gain flexibility and reduce the number of build options we add
This commit is contained in:
parent
fb84eb8d86
commit
5292283c53
7 changed files with 809 additions and 140 deletions
177
README.md
177
README.md
|
|
@ -17,9 +17,8 @@ Build options:
|
||||||
* **payload**: JSON payload for function invocation (used with awslambda_run)
|
* **payload**: JSON payload for function invocation (used with awslambda_run)
|
||||||
* **region**: AWS region for deployment and invocation
|
* **region**: AWS region for deployment and invocation
|
||||||
* **profile**: AWS profile to use for credentials
|
* **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
|
* **env-file**: Path to environment variables file for the Lambda function
|
||||||
* **allow-principal**: AWS service principal to grant invoke permission (e.g., alexa-appkit.amazon.com)
|
* **config-file**: Path to lambda.json configuration file (overrides build.zig settings)
|
||||||
|
|
||||||
The Lambda function can be compiled for x86_64 or aarch64. The build system
|
The Lambda function can be compiled for x86_64 or aarch64. The build system
|
||||||
automatically configures the Lambda architecture based on the target.
|
automatically configures the Lambda architecture based on the target.
|
||||||
|
|
@ -27,6 +26,155 @@ automatically configures the Lambda architecture based on the target.
|
||||||
A sample project using this runtime can be found at
|
A sample project using this runtime can be found at
|
||||||
https://git.lerch.org/lobo/lambda-zig-sample
|
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
|
Environment Variables
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
|
@ -81,24 +229,21 @@ 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
|
This is required for services like Alexa Skills Kit, API Gateway, or S3 to trigger
|
||||||
your Lambda function.
|
your Lambda function.
|
||||||
|
|
||||||
### Using the build system
|
### Using lambda.json (Recommended)
|
||||||
|
|
||||||
Pass the `-Dallow-principal` option to grant invoke permission to a service:
|
Add `allow_principal` to your configuration file:
|
||||||
|
|
||||||
```sh
|
```json
|
||||||
# Allow Alexa Skills Kit to invoke the function
|
{
|
||||||
zig build awslambda_deploy -Dfunction-name=my-skill -Dallow-principal=alexa-appkit.amazon.com
|
"allow_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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using the CLI directly
|
Common service principals:
|
||||||
|
- `alexa-appkit.amazon.com` - Alexa Skills Kit
|
||||||
```sh
|
- `apigateway.amazonaws.com` - API Gateway
|
||||||
./lambda-build deploy --function-name my-fn --zip-file function.zip \
|
- `s3.amazonaws.com` - S3 event notifications
|
||||||
--allow-principal alexa-appkit.amazon.com
|
- `events.amazonaws.com` - EventBridge/CloudWatch Events
|
||||||
```
|
|
||||||
|
|
||||||
The permission is idempotent - if it already exists, the deployment will continue
|
The permission is idempotent - if it already exists, the deployment will continue
|
||||||
successfully.
|
successfully.
|
||||||
|
|
|
||||||
119
build.zig
119
build.zig
|
|
@ -110,9 +110,23 @@ fn configureBuildInternal(b: *std.Build, exe: *std.Build.Step.Compile) !void {
|
||||||
_ = try @import("lambdabuild.zig").configureBuild(b, lambda_build_dep, exe, .{});
|
_ = try @import("lambdabuild.zig").configureBuild(b, lambda_build_dep, exe, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Re-export types for consumers
|
// Re-export types for consumers
|
||||||
pub const LambdaConfig = @import("lambdabuild.zig").Config;
|
const lambdabuild = @import("lambdabuild.zig");
|
||||||
pub const LambdaBuildInfo = @import("lambdabuild.zig").BuildInfo;
|
|
||||||
|
/// 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;
|
||||||
|
|
||||||
/// Configure Lambda build steps for a Zig project.
|
/// Configure Lambda build steps for a Zig project.
|
||||||
///
|
///
|
||||||
|
|
@ -136,19 +150,42 @@ pub const LambdaBuildInfo = @import("lambdabuild.zig").BuildInfo;
|
||||||
///
|
///
|
||||||
/// ## Build Options
|
/// ## Build Options
|
||||||
///
|
///
|
||||||
/// The following options are added to the build (command-line options override
|
/// The following command-line options are available:
|
||||||
/// config defaults):
|
|
||||||
///
|
///
|
||||||
/// - `-Dfunction-name=[string]`: Name of the Lambda function
|
/// - `-Dfunction-name=[string]`: Name of the Lambda function
|
||||||
/// (default: "zig-fn", or as provided by config parameter)
|
/// (default: exe.name, or as provided by config parameter)
|
||||||
/// - `-Dregion=[string]`: AWS region for deployment and invocation
|
/// - `-Dregion=[string]`: AWS region for deployment and invocation
|
||||||
/// - `-Dprofile=[string]`: AWS profile to use for credentials
|
/// - `-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: "{}")
|
/// - `-Dpayload=[string]`: JSON payload for invocation (default: "{}")
|
||||||
/// - `-Denv-file=[string]`: Path to environment variables file (KEY=VALUE format)
|
/// - `-Denv-file=[string]`: Path to environment variables file (KEY=VALUE format)
|
||||||
/// - `-Dallow-principal=[string]`: AWS service principal to grant invoke permission
|
/// - `-Dconfig-file=[string]`: Path to Lambda build config JSON file (overrides function_config)
|
||||||
/// (e.g., "alexa-appkit.amazon.com" for Alexa Skills Kit)
|
///
|
||||||
|
/// ## 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"
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
///
|
///
|
||||||
/// ## Deploy Output
|
/// ## Deploy Output
|
||||||
///
|
///
|
||||||
|
|
@ -170,6 +207,8 @@ pub const LambdaBuildInfo = @import("lambdabuild.zig").BuildInfo;
|
||||||
///
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
///
|
///
|
||||||
|
/// ### Basic Usage (uses lambda.json if present)
|
||||||
|
///
|
||||||
/// ```zig
|
/// ```zig
|
||||||
/// const lambda_zig = @import("lambda_zig");
|
/// const lambda_zig = @import("lambda_zig");
|
||||||
///
|
///
|
||||||
|
|
@ -185,27 +224,73 @@ pub const LambdaBuildInfo = @import("lambdabuild.zig").BuildInfo;
|
||||||
/// const exe = b.addExecutable(.{ ... });
|
/// const exe = b.addExecutable(.{ ... });
|
||||||
/// b.installArtifact(exe);
|
/// b.installArtifact(exe);
|
||||||
///
|
///
|
||||||
/// // 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, .{
|
/// }
|
||||||
/// .default_function_name = "my-function",
|
/// ```
|
||||||
|
///
|
||||||
|
/// ### 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",
|
||||||
|
/// }},
|
||||||
/// });
|
/// });
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ### 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
|
/// // Use lambda.deploy_output in other steps that need the ARN
|
||||||
/// const my_step = b.addRunArtifact(my_tool);
|
/// const my_step = b.addRunArtifact(my_tool);
|
||||||
/// my_step.addFileArg(lambda.deploy_output);
|
/// my_step.addFileArg(lambda.deploy_output);
|
||||||
/// my_step.step.dependOn(lambda.deploy_step); // Ensure deploy runs first
|
/// my_step.step.dependOn(lambda.deploy_step); // Ensure deploy runs first
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn configureBuild(
|
pub fn configureBuild(
|
||||||
b: *std.Build,
|
b: *std.Build,
|
||||||
lambda_zig_dep: *std.Build.Dependency,
|
lambda_zig_dep: *std.Build.Dependency,
|
||||||
exe: *std.Build.Step.Compile,
|
exe: *std.Build.Step.Compile,
|
||||||
config: LambdaConfig,
|
options: Options,
|
||||||
) !LambdaBuildInfo {
|
) !BuildInfo {
|
||||||
// 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,
|
||||||
});
|
});
|
||||||
return @import("lambdabuild.zig").configureBuild(b, lambda_build_dep, exe, config);
|
return lambdabuild.configureBuild(b, lambda_build_dep, exe, options);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
138
lambdabuild.zig
138
lambdabuild.zig
|
|
@ -5,25 +5,53 @@
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
/// Configuration options for Lambda build integration.
|
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.
|
||||||
///
|
///
|
||||||
/// These provide project-level defaults that can still be overridden
|
/// These provide project-level defaults that can still be overridden
|
||||||
/// via command-line options (e.g., `-Dfunction-name=...`).
|
/// via command-line options (e.g., `-Dfunction-name=...`).
|
||||||
pub const Config = struct {
|
pub const Options = struct {
|
||||||
/// Default function name if not specified via -Dfunction-name.
|
/// Default function name if not specified via -Dfunction-name.
|
||||||
/// If null, falls back to the executable name (exe.name).
|
/// If null, falls back to the executable name (exe.name).
|
||||||
default_function_name: ?[]const u8 = null,
|
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.
|
/// Default environment file if not specified via -Denv-file.
|
||||||
/// If the file doesn't exist, it's silently skipped.
|
/// If the file doesn't exist, it's silently skipped.
|
||||||
default_env_file: ?[]const u8 = ".env",
|
default_env_file: ?[]const u8 = ".env",
|
||||||
|
|
||||||
/// Default AWS service principal to grant invoke permission.
|
/// Lambda build configuration source.
|
||||||
/// For Alexa skills, use "alexa-appkit.amazon.com".
|
/// Defaults to looking for "lambda.json" (optional - uses defaults if missing).
|
||||||
default_allow_principal: ?[]const u8 = null,
|
///
|
||||||
|
/// 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 } },
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Information about the configured Lambda build steps.
|
/// Information about the configured Lambda build steps.
|
||||||
|
|
@ -60,8 +88,39 @@ pub const BuildInfo = struct {
|
||||||
/// - awslambda_deploy: Deploy the function to AWS
|
/// - awslambda_deploy: Deploy the function to AWS
|
||||||
/// - awslambda_run: Invoke the deployed function
|
/// - awslambda_run: Invoke the deployed function
|
||||||
///
|
///
|
||||||
/// The `config` parameter allows setting project-level defaults that can
|
/// ## Configuration
|
||||||
/// still be overridden via command-line options.
|
///
|
||||||
|
/// 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,
|
||||||
|
/// }},
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
///
|
///
|
||||||
/// Returns a `BuildInfo` struct containing references to all steps and
|
/// Returns a `BuildInfo` struct containing references to all steps and
|
||||||
/// a `deploy_output` LazyPath to the deployment info JSON file.
|
/// a `deploy_output` LazyPath to the deployment info JSON file.
|
||||||
|
|
@ -69,20 +128,15 @@ 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,
|
options: Options,
|
||||||
) !BuildInfo {
|
) !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");
|
||||||
|
|
||||||
// Get configuration options (command-line overrides config defaults)
|
// Get configuration options (command-line overrides config defaults)
|
||||||
const function_name = b.option([]const u8, "function-name", "Function name for Lambda") orelse config.default_function_name orelse exe.name;
|
const function_name = b.option([]const u8, "function-name", "Function name for Lambda") orelse options.default_function_name orelse exe.name;
|
||||||
const region = b.option([]const u8, "region", "AWS region") orelse null;
|
const region = b.option([]const u8, "region", "AWS region") orelse null;
|
||||||
const profile = b.option([]const u8, "profile", "AWS profile") 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 payload = b.option(
|
||||||
[]const u8,
|
[]const u8,
|
||||||
"payload",
|
"payload",
|
||||||
|
|
@ -92,12 +146,12 @@ pub fn configureBuild(
|
||||||
[]const u8,
|
[]const u8,
|
||||||
"env-file",
|
"env-file",
|
||||||
"Path to environment variables file (KEY=VALUE format)",
|
"Path to environment variables file (KEY=VALUE format)",
|
||||||
) orelse config.default_env_file;
|
) orelse options.default_env_file;
|
||||||
const allow_principal = b.option(
|
const config_file_override = b.option(
|
||||||
[]const u8,
|
[]const u8,
|
||||||
"allow-principal",
|
"config-file",
|
||||||
"AWS service principal to grant invoke permission (e.g., alexa-appkit.amazon.com)",
|
"Path to Lambda build config JSON file (overrides function_config)",
|
||||||
) orelse config.default_allow_principal;
|
);
|
||||||
|
|
||||||
// Determine architecture for Lambda
|
// Determine architecture for Lambda
|
||||||
const target_arch = exe.root_module.resolved_target.?.result.cpu.arch;
|
const target_arch = exe.root_module.resolved_target.?.result.cpu.arch;
|
||||||
|
|
@ -112,6 +166,39 @@ 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
|
// Package step - output goes to cache based on input hash
|
||||||
const package_cmd = b.addRunArtifact(cli);
|
const package_cmd = b.addRunArtifact(cli);
|
||||||
package_cmd.step.name = try std.fmt.allocPrint(b.allocator, "{s} package", .{cli.name});
|
package_cmd.step.name = try std.fmt.allocPrint(b.allocator, "{s} package", .{cli.name});
|
||||||
|
|
@ -129,7 +216,8 @@ pub fn configureBuild(
|
||||||
iam_cmd.step.name = try std.fmt.allocPrint(b.allocator, "{s} iam", .{cli.name});
|
iam_cmd.step.name = try std.fmt.allocPrint(b.allocator, "{s} iam", .{cli.name});
|
||||||
if (profile) |p| iam_cmd.addArgs(&.{ "--profile", p });
|
if (profile) |p| iam_cmd.addArgs(&.{ "--profile", p });
|
||||||
if (region) |r| iam_cmd.addArgs(&.{ "--region", r });
|
if (region) |r| iam_cmd.addArgs(&.{ "--region", r });
|
||||||
iam_cmd.addArgs(&.{ "iam", "--role-name", role_name });
|
iam_cmd.addArg("iam");
|
||||||
|
addConfigArg(iam_cmd, config_file);
|
||||||
|
|
||||||
const iam_step = b.step("awslambda_iam", "Create/verify IAM role for Lambda");
|
const iam_step = b.step("awslambda_iam", "Create/verify IAM role for Lambda");
|
||||||
iam_step.dependOn(&iam_cmd.step);
|
iam_step.dependOn(&iam_cmd.step);
|
||||||
|
|
@ -150,13 +238,11 @@ pub fn configureBuild(
|
||||||
});
|
});
|
||||||
deploy_cmd.addFileArg(zip_output);
|
deploy_cmd.addFileArg(zip_output);
|
||||||
deploy_cmd.addArgs(&.{
|
deploy_cmd.addArgs(&.{
|
||||||
"--role-name",
|
|
||||||
role_name,
|
|
||||||
"--arch",
|
"--arch",
|
||||||
arch_str,
|
arch_str,
|
||||||
});
|
});
|
||||||
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 });
|
addConfigArg(deploy_cmd, config_file);
|
||||||
// Add deploy output file for deployment info JSON
|
// Add deploy output file for deployment info JSON
|
||||||
deploy_cmd.addArg("--deploy-output");
|
deploy_cmd.addArg("--deploy-output");
|
||||||
const deploy_output = deploy_cmd.addOutputFileArg("deploy-output.json");
|
const deploy_output = deploy_cmd.addOutputFileArg("deploy-output.json");
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
.fingerprint = 0x6e61de08e7e51114,
|
.fingerprint = 0x6e61de08e7e51114,
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
.aws = .{
|
.aws = .{
|
||||||
.url = "git+https://git.lerch.org/lobo/aws-sdk-for-zig#5c7aed071f6251d53a1627080a21d604ff58f0a5",
|
.url = "git+https://git.lerch.org/lobo/aws-sdk-for-zig#1a03250fbeb2840ab8b6010f1ad4e899cdfc185a",
|
||||||
.hash = "aws-0.0.1-SbsFcFE7CgDBilPa15i4gIB6Qr5ozBz328O63abDQDDk",
|
.hash = "aws-0.0.1-SbsFcCg7CgC0yYv2Y7aOjonSAU3mltOSfY0x2w9jZlMV",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
.paths = .{
|
.paths = .{
|
||||||
|
|
|
||||||
195
tools/build/src/LambdaBuildConfig.zig
Normal file
195
tools/build/src/LambdaBuildConfig.zig
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
//! 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,11 +2,13 @@
|
||||||
//!
|
//!
|
||||||
//! Creates a new function or updates an existing one.
|
//! Creates a new function or updates an existing one.
|
||||||
//! Supports setting environment variables via --env or --env-file.
|
//! Supports setting environment variables via --env or --env-file.
|
||||||
|
//! Function configuration (timeout, memory, VPC, etc.) comes from --config-file.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const aws = @import("aws");
|
const aws = @import("aws");
|
||||||
const iam_cmd = @import("iam.zig");
|
const iam_cmd = @import("iam.zig");
|
||||||
const RunOptions = @import("main.zig").RunOptions;
|
const RunOptions = @import("main.zig").RunOptions;
|
||||||
|
const LambdaBuildConfig = @import("LambdaBuildConfig.zig");
|
||||||
|
|
||||||
// Get Lambda EnvironmentVariableKeyValue type from AWS SDK
|
// Get Lambda EnvironmentVariableKeyValue type from AWS SDK
|
||||||
const EnvVar = aws.services.lambda.EnvironmentVariableKeyValue;
|
const EnvVar = aws.services.lambda.EnvironmentVariableKeyValue;
|
||||||
|
|
@ -15,10 +17,10 @@ pub fn run(args: []const []const u8, options: RunOptions) !void {
|
||||||
var function_name: ?[]const u8 = null;
|
var function_name: ?[]const u8 = null;
|
||||||
var zip_file: ?[]const u8 = null;
|
var zip_file: ?[]const u8 = null;
|
||||||
var role_arn: ?[]const u8 = null;
|
var role_arn: ?[]const u8 = null;
|
||||||
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 deploy_output: ?[]const u8 = null;
|
var deploy_output: ?[]const u8 = null;
|
||||||
|
var config_file: ?[]const u8 = null;
|
||||||
|
var is_config_required = false;
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
@ -46,10 +48,6 @@ pub fn run(args: []const []const u8, options: RunOptions) !void {
|
||||||
i += 1;
|
i += 1;
|
||||||
if (i >= args.len) return error.MissingRoleArn;
|
if (i >= args.len) return error.MissingRoleArn;
|
||||||
role_arn = args[i];
|
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")) {
|
} else if (std.mem.eql(u8, arg, "--arch")) {
|
||||||
i += 1;
|
i += 1;
|
||||||
if (i >= args.len) return error.MissingArch;
|
if (i >= args.len) return error.MissingArch;
|
||||||
|
|
@ -62,10 +60,16 @@ pub fn run(args: []const []const u8, options: RunOptions) !void {
|
||||||
i += 1;
|
i += 1;
|
||||||
if (i >= args.len) return error.MissingEnvFile;
|
if (i >= args.len) return error.MissingEnvFile;
|
||||||
try loadEnvFile(args[i], &env_vars, options.allocator);
|
try loadEnvFile(args[i], &env_vars, options.allocator);
|
||||||
} else if (std.mem.eql(u8, arg, "--allow-principal")) {
|
} else if (std.mem.eql(u8, arg, "--config-file")) {
|
||||||
i += 1;
|
i += 1;
|
||||||
if (i >= args.len) return error.MissingAllowPrincipal;
|
if (i >= args.len) return error.MissingConfigFile;
|
||||||
allow_principal = args[i];
|
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;
|
||||||
} else if (std.mem.eql(u8, arg, "--deploy-output")) {
|
} else if (std.mem.eql(u8, arg, "--deploy-output")) {
|
||||||
i += 1;
|
i += 1;
|
||||||
if (i >= args.len) return error.MissingDeployOutput;
|
if (i >= args.len) return error.MissingDeployOutput;
|
||||||
|
|
@ -95,15 +99,21 @@ pub fn run(args: []const []const u8, options: RunOptions) !void {
|
||||||
return error.MissingZipFile;
|
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(.{
|
try deployFunction(.{
|
||||||
.function_name = function_name.?,
|
.function_name = function_name.?,
|
||||||
.zip_file = zip_file.?,
|
.zip_file = zip_file.?,
|
||||||
.role_arn = role_arn,
|
.role_arn = role_arn,
|
||||||
.role_name = role_name,
|
|
||||||
.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,
|
|
||||||
.deploy_output = deploy_output,
|
.deploy_output = deploy_output,
|
||||||
|
.config = if (parsed_config) |pc| &pc.parsed.value else null,
|
||||||
}, options);
|
}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,15 +202,25 @@ fn printHelp(writer: anytype) void {
|
||||||
\\ --function-name <name> Name of the Lambda function (required)
|
\\ --function-name <name> Name of the Lambda function (required)
|
||||||
\\ --zip-file <path> Path to the deployment zip (required)
|
\\ --zip-file <path> Path to the deployment zip (required)
|
||||||
\\ --role-arn <arn> IAM role ARN (optional - creates role if omitted)
|
\\ --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)
|
\\ --arch <arch> Architecture: x86_64 or aarch64 (default: x86_64)
|
||||||
\\ --env <KEY=VALUE> Set environment variable (can be repeated)
|
\\ --env <KEY=VALUE> Set environment variable (can be repeated)
|
||||||
\\ --env-file <path> Load environment variables from file (KEY=VALUE format)
|
\\ --env-file <path> Load environment variables from file
|
||||||
\\ --allow-principal <p> Grant invoke permission to AWS service principal
|
\\ --config-file <path> Path to JSON config file (required, error if missing)
|
||||||
\\ (e.g., alexa-appkit.amazon.com)
|
\\ --config-file-optional <path> Path to JSON config file (optional, use defaults if missing)
|
||||||
\\ --deploy-output <path> Write deployment info (ARN, region, etc.) to JSON file
|
\\ --deploy-output <path> Write deployment info to JSON file
|
||||||
\\ --help, -h Show this help message
|
\\ --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:
|
\\Environment File Format:
|
||||||
\\ The --env-file option reads a file with KEY=VALUE pairs, one per line.
|
\\ 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.
|
\\ Lines starting with # are treated as comments. Empty lines are ignored.
|
||||||
|
|
@ -220,11 +240,10 @@ const DeployOptions = struct {
|
||||||
function_name: []const u8,
|
function_name: []const u8,
|
||||||
zip_file: []const u8,
|
zip_file: []const u8,
|
||||||
role_arn: ?[]const u8,
|
role_arn: ?[]const u8,
|
||||||
role_name: []const u8,
|
|
||||||
arch: ?[]const u8,
|
arch: ?[]const u8,
|
||||||
env_vars: ?*const std.StringHashMap([]const u8),
|
env_vars: ?*const std.StringHashMap([]const u8),
|
||||||
allow_principal: ?[]const u8,
|
|
||||||
deploy_output: ?[]const u8,
|
deploy_output: ?[]const u8,
|
||||||
|
config: ?*const LambdaBuildConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
|
fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
|
||||||
|
|
@ -234,11 +253,14 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
|
||||||
return error.InvalidArchitecture;
|
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
|
// Get or create IAM role if not provided
|
||||||
const role_arn = if (deploy_opts.role_arn) |r|
|
const role_arn = if (deploy_opts.role_arn) |r|
|
||||||
try options.allocator.dupe(u8, r)
|
try options.allocator.dupe(u8, r)
|
||||||
else
|
else
|
||||||
try iam_cmd.getOrCreateRole(deploy_opts.role_name, options);
|
try iam_cmd.getOrCreateRole(role_name, options);
|
||||||
|
|
||||||
defer options.allocator.free(role_arn);
|
defer options.allocator.free(role_arn);
|
||||||
|
|
||||||
|
|
@ -276,6 +298,58 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
|
||||||
options.allocator.free(vars);
|
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
|
// 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});
|
std.log.info("Attempting to create function: {s}", .{deploy_opts.function_name});
|
||||||
|
|
||||||
|
|
@ -304,6 +378,18 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
|
||||||
.runtime = "provided.al2023",
|
.runtime = "provided.al2023",
|
||||||
.role = role_arn,
|
.role = role_arn,
|
||||||
.environment = if (env_variables) |vars| .{ .variables = vars } else null,
|
.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| {
|
}, create_options) catch |err| {
|
||||||
defer create_diagnostics.deinit();
|
defer create_diagnostics.deinit();
|
||||||
|
|
||||||
|
|
@ -328,20 +414,23 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
|
||||||
// Wait for function to be ready before updating configuration
|
// Wait for function to be ready before updating configuration
|
||||||
try waitForFunctionReady(deploy_opts.function_name, options);
|
try waitForFunctionReady(deploy_opts.function_name, options);
|
||||||
|
|
||||||
// Update environment variables if provided
|
// Update function configuration if we have config or env variables
|
||||||
if (env_variables) |vars| {
|
if (config != null or env_variables != null)
|
||||||
try updateFunctionConfiguration(deploy_opts.function_name, vars, options);
|
try updateFunctionConfiguration(
|
||||||
}
|
deploy_opts.function_name,
|
||||||
|
env_variables,
|
||||||
|
config,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
// Add invoke permission if requested
|
// Add invoke permission if requested
|
||||||
if (deploy_opts.allow_principal) |principal| {
|
if (config) |c|
|
||||||
|
if (c.allow_principal) |principal|
|
||||||
try addPermission(deploy_opts.function_name, principal, options);
|
try addPermission(deploy_opts.function_name, principal, options);
|
||||||
}
|
|
||||||
|
|
||||||
// Write deploy output if requested
|
// 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);
|
try writeDeployOutput(output_path, function_arn.?, role_arn, lambda_arch, deploy_opts.env_vars);
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -364,14 +453,13 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
|
||||||
try waitForFunctionReady(deploy_opts.function_name, options);
|
try waitForFunctionReady(deploy_opts.function_name, options);
|
||||||
|
|
||||||
// Add invoke permission if requested
|
// Add invoke permission if requested
|
||||||
if (deploy_opts.allow_principal) |principal| {
|
if (config) |c|
|
||||||
|
if (c.allow_principal) |principal|
|
||||||
try addPermission(deploy_opts.function_name, principal, options);
|
try addPermission(deploy_opts.function_name, principal, options);
|
||||||
}
|
|
||||||
|
|
||||||
// Write deploy output if requested
|
// 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);
|
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
|
||||||
|
|
@ -398,23 +486,74 @@ fn buildEnvVariables(
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update function configuration (environment variables)
|
/// Update function configuration (environment variables and config settings)
|
||||||
fn updateFunctionConfiguration(
|
fn updateFunctionConfiguration(
|
||||||
function_name: []const u8,
|
function_name: []const u8,
|
||||||
env_variables: []EnvVar,
|
env_variables: ?[]EnvVar,
|
||||||
|
config: ?*const LambdaBuildConfig,
|
||||||
options: RunOptions,
|
options: RunOptions,
|
||||||
) !void {
|
) !void {
|
||||||
const services = aws.Services(.{.lambda}){};
|
const services = aws.Services(.{.lambda}){};
|
||||||
|
|
||||||
std.log.info("Updating function configuration for: {s}", .{function_name});
|
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(.{
|
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 = 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,
|
||||||
}, options.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 function configuration\n", .{});
|
||||||
try options.stdout.flush();
|
try options.stdout.flush();
|
||||||
|
|
||||||
// Wait for configuration update to complete
|
// Wait for configuration update to complete
|
||||||
|
|
@ -437,21 +576,17 @@ fn waitForFunctionReady(function_name: []const u8, options: RunOptions) !void {
|
||||||
defer result.deinit();
|
defer result.deinit();
|
||||||
|
|
||||||
// Check if function is ready
|
// Check if function is ready
|
||||||
if (result.response.configuration) |config| {
|
if (result.response.configuration) |cfg| {
|
||||||
if (config.last_update_status) |status| {
|
if (cfg.last_update_status) |status| {
|
||||||
if (std.mem.eql(u8, status, "Successful")) {
|
if (std.mem.eql(u8, status, "Successful")) {
|
||||||
std.log.info("Function is ready", .{});
|
std.log.debug("Function is ready", .{});
|
||||||
return;
|
return;
|
||||||
} else if (std.mem.eql(u8, status, "Failed")) {
|
} else if (std.mem.eql(u8, status, "Failed")) {
|
||||||
return error.FunctionUpdateFailed;
|
return error.FunctionUpdateFailed;
|
||||||
}
|
}
|
||||||
// "InProgress" - keep waiting
|
// "InProgress" - keep waiting
|
||||||
} else {
|
} else return; // No status means it's ready
|
||||||
return; // No status means it's ready
|
} else return; // No configuration means we can't check, assume ready
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return; // No configuration means we can't check, assume ready
|
|
||||||
}
|
|
||||||
|
|
||||||
std.Thread.sleep(200 * std.time.ns_per_ms);
|
std.Thread.sleep(200 * std.time.ns_per_ms);
|
||||||
}
|
}
|
||||||
|
|
@ -544,7 +679,7 @@ fn writeDeployOutput(
|
||||||
const region = arn_parts.next() orelse return error.InvalidArn;
|
const region = arn_parts.next() orelse return error.InvalidArn;
|
||||||
const account_id = arn_parts.next() orelse return error.InvalidArn;
|
const account_id = arn_parts.next() orelse return error.InvalidArn;
|
||||||
_ = arn_parts.next(); // function
|
_ = arn_parts.next(); // function
|
||||||
const function_name = arn_parts.next() orelse return error.InvalidArn;
|
const fn_name = arn_parts.next() orelse return error.InvalidArn;
|
||||||
|
|
||||||
const file = try std.fs.cwd().createFile(output_path, .{});
|
const file = try std.fs.cwd().createFile(output_path, .{});
|
||||||
defer file.close();
|
defer file.close();
|
||||||
|
|
@ -564,7 +699,7 @@ fn writeDeployOutput(
|
||||||
\\ "role_arn": "{s}",
|
\\ "role_arn": "{s}",
|
||||||
\\ "architecture": "{s}",
|
\\ "architecture": "{s}",
|
||||||
\\ "environment_keys": [
|
\\ "environment_keys": [
|
||||||
, .{ function_arn, function_name, partition, region, account_id, role_arn, architecture });
|
, .{ function_arn, fn_name, partition, region, account_id, role_arn, architecture });
|
||||||
|
|
||||||
// Write environment variable keys
|
// Write environment variable keys
|
||||||
if (env_vars) |vars| {
|
if (env_vars) |vars| {
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,25 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const aws = @import("aws");
|
const aws = @import("aws");
|
||||||
const RunOptions = @import("main.zig").RunOptions;
|
const RunOptions = @import("main.zig").RunOptions;
|
||||||
|
const LambdaBuildConfig = @import("LambdaBuildConfig.zig");
|
||||||
|
|
||||||
pub fn run(args: []const []const u8, options: RunOptions) !void {
|
pub fn run(args: []const []const u8, options: RunOptions) !void {
|
||||||
var role_name: ?[]const u8 = null;
|
var config_file: ?[]const u8 = null;
|
||||||
|
var is_config_required = false;
|
||||||
|
|
||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
while (i < args.len) : (i += 1) {
|
while (i < args.len) : (i += 1) {
|
||||||
const arg = args[i];
|
const arg = args[i];
|
||||||
if (std.mem.eql(u8, arg, "--role-name")) {
|
if (std.mem.eql(u8, arg, "--config-file")) {
|
||||||
i += 1;
|
i += 1;
|
||||||
if (i >= args.len) return error.MissingRoleName;
|
if (i >= args.len) return error.MissingConfigFile;
|
||||||
role_name = args[i];
|
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;
|
||||||
} 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();
|
||||||
|
|
@ -25,30 +33,44 @@ pub fn run(args: []const []const u8, options: RunOptions) !void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role_name == null) {
|
// Load config file if provided
|
||||||
try options.stderr.print("Error: --role-name is required\n", .{});
|
var parsed_config = if (config_file) |path|
|
||||||
printHelp(options.stderr);
|
try LambdaBuildConfig.loadFromFile(options.allocator, path, !is_config_required)
|
||||||
try options.stderr.flush();
|
else
|
||||||
return error.MissingRoleName;
|
null;
|
||||||
}
|
defer if (parsed_config) |*pc| pc.deinit();
|
||||||
|
|
||||||
const arn = try getOrCreateRole(role_name.?, options);
|
// 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);
|
||||||
defer options.allocator.free(arn);
|
defer options.allocator.free(arn);
|
||||||
|
|
||||||
try options.stdout.print("{s}\n", .{arn});
|
try options.stdout.print("{s}\n", .{arn});
|
||||||
try options.stdout.flush();
|
try options.stdout.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn printHelp(writer: *std.Io.Writer) void {
|
fn printHelp(writer: anytype) void {
|
||||||
writer.print(
|
writer.print(
|
||||||
\\Usage: lambda-build iam [options]
|
\\Usage: lambda-build iam [options]
|
||||||
\\
|
\\
|
||||||
\\Create or retrieve an IAM role for Lambda execution.
|
\\Create or retrieve an IAM role for Lambda execution.
|
||||||
\\
|
\\
|
||||||
\\Options:
|
\\Options:
|
||||||
\\ --role-name <name> Name of the IAM role (required)
|
\\ --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)
|
||||||
\\ --help, -h Show this help message
|
\\ --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
|
\\If the role exists, its ARN is returned. If not, a new role is created
|
||||||
\\with the AWSLambdaExecute policy attached.
|
\\with the AWSLambdaExecute policy attached.
|
||||||
\\
|
\\
|
||||||
|
|
@ -71,19 +93,20 @@ pub fn getOrCreateRole(role_name: []const u8, options: RunOptions) ![]const u8 {
|
||||||
// Use the shared aws_options but add diagnostics for this call
|
// Use the shared aws_options but add diagnostics for this call
|
||||||
var aws_options = options.aws_options;
|
var aws_options = options.aws_options;
|
||||||
aws_options.diagnostics = &diagnostics;
|
aws_options.diagnostics = &diagnostics;
|
||||||
|
defer aws_options.diagnostics = null;
|
||||||
|
|
||||||
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,
|
||||||
}, aws_options) catch |err| {
|
}, aws_options) catch |err| {
|
||||||
defer diagnostics.deinit();
|
defer diagnostics.deinit();
|
||||||
if (diagnostics.response_status == .not_found) {
|
|
||||||
|
// 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)
|
||||||
// Role doesn't exist, create it
|
// Role doesn't exist, create it
|
||||||
return try createRole(role_name, options);
|
return try createRole(role_name, options);
|
||||||
}
|
|
||||||
std.log.err(
|
std.log.err("IAM GetRole failed: {} (HTTP {})", .{ err, diagnostics.response_status });
|
||||||
"IAM GetRole failed: {} (HTTP Response code {})",
|
|
||||||
.{ err, diagnostics.response_status },
|
|
||||||
);
|
|
||||||
return error.IamGetRoleFailed;
|
return error.IamGetRoleFailed;
|
||||||
};
|
};
|
||||||
defer get_result.deinit();
|
defer get_result.deinit();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue