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)
|
* **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
|
||||||
* **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
|
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.
|
||||||
|
|
@ -26,155 +27,6 @@ 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
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
|
@ -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
|
This is required for services like Alexa Skills Kit, API Gateway, or S3 to trigger
|
||||||
your Lambda function.
|
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
|
```sh
|
||||||
{
|
# Allow Alexa Skills Kit to invoke the function
|
||||||
"allow_principal": "alexa-appkit.amazon.com"
|
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:
|
### Using the CLI directly
|
||||||
- `alexa-appkit.amazon.com` - Alexa Skills Kit
|
|
||||||
- `apigateway.amazonaws.com` - API Gateway
|
```sh
|
||||||
- `s3.amazonaws.com` - S3 event notifications
|
./lambda-build deploy --function-name my-fn --zip-file function.zip \
|
||||||
- `events.amazonaws.com` - EventBridge/CloudWatch Events
|
--allow-principal alexa-appkit.amazon.com
|
||||||
|
```
|
||||||
|
|
||||||
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,23 +110,9 @@ 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
|
||||||
const lambdabuild = @import("lambdabuild.zig");
|
pub const LambdaConfig = @import("lambdabuild.zig").Config;
|
||||||
|
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.
|
||||||
///
|
///
|
||||||
|
|
@ -150,42 +136,19 @@ pub const BuildInfo = lambdabuild.BuildInfo;
|
||||||
///
|
///
|
||||||
/// ## Build Options
|
/// ## 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
|
/// - `-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
|
/// - `-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)
|
||||||
/// - `-Dconfig-file=[string]`: Path to Lambda build config JSON file (overrides function_config)
|
/// - `-Dallow-principal=[string]`: AWS service principal to grant invoke permission
|
||||||
///
|
/// (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
|
||||||
///
|
///
|
||||||
|
|
@ -207,8 +170,6 @@ pub const BuildInfo = lambdabuild.BuildInfo;
|
||||||
///
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
///
|
///
|
||||||
/// ### Basic Usage (uses lambda.json if present)
|
|
||||||
///
|
|
||||||
/// ```zig
|
/// ```zig
|
||||||
/// const lambda_zig = @import("lambda_zig");
|
/// const lambda_zig = @import("lambda_zig");
|
||||||
///
|
///
|
||||||
|
|
@ -224,73 +185,27 @@ pub const BuildInfo = lambdabuild.BuildInfo;
|
||||||
/// const exe = b.addExecutable(.{ ... });
|
/// const exe = b.addExecutable(.{ ... });
|
||||||
/// b.installArtifact(exe);
|
/// b.installArtifact(exe);
|
||||||
///
|
///
|
||||||
/// _ = try lambda_zig.configureBuild(b, lambda_zig_dep, exe, .{});
|
/// // Configure Lambda build and get deployment info
|
||||||
/// }
|
/// 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,
|
||||||
options: Options,
|
config: LambdaConfig,
|
||||||
) !BuildInfo {
|
) !LambdaBuildInfo {
|
||||||
// Get lambda_build from the lambda_zig dependency's Build context
|
// Get lambda_build from the lambda_zig dependency's Build context
|
||||||
const lambda_build_dep = lambda_zig_dep.builder.dependency("lambda_build", .{
|
const lambda_build_dep = lambda_zig_dep.builder.dependency("lambda_build", .{
|
||||||
.target = b.graph.host,
|
.target = b.graph.host,
|
||||||
.optimize = .ReleaseSafe,
|
.optimize = .ReleaseSafe,
|
||||||
});
|
});
|
||||||
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");
|
const std = @import("std");
|
||||||
|
|
||||||
pub const LambdaBuildConfig = @import("tools/build/src/LambdaBuildConfig.zig");
|
/// Configuration options for Lambda build integration.
|
||||||
|
|
||||||
/// 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 Options = struct {
|
pub const Config = 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",
|
||||||
|
|
||||||
/// Lambda build configuration source.
|
/// Default AWS service principal to grant invoke permission.
|
||||||
/// Defaults to looking for "lambda.json" (optional - uses defaults if missing).
|
/// For Alexa skills, use "alexa-appkit.amazon.com".
|
||||||
///
|
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.
|
||||||
|
|
@ -88,39 +60,8 @@ 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
|
||||||
///
|
///
|
||||||
/// ## Configuration
|
/// The `config` parameter allows setting project-level defaults that can
|
||||||
///
|
/// 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.
|
||||||
|
|
@ -128,15 +69,20 @@ 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,
|
||||||
options: Options,
|
config: Config,
|
||||||
) !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 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 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",
|
||||||
|
|
@ -146,12 +92,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 options.default_env_file;
|
) orelse config.default_env_file;
|
||||||
const config_file_override = b.option(
|
const allow_principal = b.option(
|
||||||
[]const u8,
|
[]const u8,
|
||||||
"awslambda-config-file",
|
"allow-principal",
|
||||||
"Path to Lambda build config JSON file (overrides function_config)",
|
"AWS service principal to grant invoke permission (e.g., alexa-appkit.amazon.com)",
|
||||||
);
|
) 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;
|
||||||
|
|
@ -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
|
// 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});
|
||||||
|
|
@ -216,8 +129,7 @@ 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.addArg("iam");
|
iam_cmd.addArgs(&.{ "iam", "--role-name", role_name });
|
||||||
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);
|
||||||
|
|
@ -238,11 +150,13 @@ 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 });
|
||||||
addConfigArg(deploy_cmd, config_file);
|
if (allow_principal) |ap| deploy_cmd.addArgs(&.{ "--allow-principal", ap });
|
||||||
// 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#1a03250fbeb2840ab8b6010f1ad4e899cdfc185a",
|
.url = "git+https://git.lerch.org/lobo/aws-sdk-for-zig#5c7aed071f6251d53a1627080a21d604ff58f0a5",
|
||||||
.hash = "aws-0.0.1-SbsFcCg7CgC0yYv2Y7aOjonSAU3mltOSfY0x2w9jZlMV",
|
.hash = "aws-0.0.1-SbsFcFE7CgDBilPa15i4gIB6Qr5ozBz328O63abDQDDk",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
.paths = .{
|
.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.
|
//! 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;
|
||||||
|
|
@ -17,10 +15,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);
|
||||||
|
|
@ -48,6 +46,10 @@ 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;
|
||||||
|
|
@ -60,16 +62,10 @@ 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, "--config-file")) {
|
} else if (std.mem.eql(u8, arg, "--allow-principal")) {
|
||||||
i += 1;
|
i += 1;
|
||||||
if (i >= args.len) return error.MissingConfigFile;
|
if (i >= args.len) return error.MissingAllowPrincipal;
|
||||||
config_file = args[i];
|
allow_principal = 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;
|
||||||
|
|
@ -99,21 +95,15 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,25 +192,15 @@ 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
|
\\ --env-file <path> Load environment variables from file (KEY=VALUE format)
|
||||||
\\ --config-file <path> Path to JSON config file (required, error if missing)
|
\\ --allow-principal <p> Grant invoke permission to AWS service principal
|
||||||
\\ --config-file-optional <path> Path to JSON config file (optional, use defaults if missing)
|
\\ (e.g., alexa-appkit.amazon.com)
|
||||||
\\ --deploy-output <path> Write deployment info to JSON file
|
\\ --deploy-output <path> Write deployment info (ARN, region, etc.) 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.
|
||||||
|
|
@ -240,10 +220,11 @@ 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 {
|
||||||
|
|
@ -253,14 +234,11 @@ 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(role_name, options);
|
try iam_cmd.getOrCreateRole(deploy_opts.role_name, options);
|
||||||
|
|
||||||
defer options.allocator.free(role_arn);
|
defer options.allocator.free(role_arn);
|
||||||
|
|
||||||
|
|
@ -298,58 +276,6 @@ 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});
|
||||||
|
|
||||||
|
|
@ -378,18 +304,6 @@ 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();
|
||||||
|
|
||||||
|
|
@ -414,23 +328,20 @@ 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 function configuration if we have config or env variables
|
// Update environment variables if provided
|
||||||
if (config != null or env_variables != null)
|
if (env_variables) |vars| {
|
||||||
try updateFunctionConfiguration(
|
try updateFunctionConfiguration(deploy_opts.function_name, vars, options);
|
||||||
deploy_opts.function_name,
|
}
|
||||||
env_variables,
|
|
||||||
config,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add invoke permission if requested
|
// Add invoke permission if requested
|
||||||
if (config) |c|
|
if (deploy_opts.allow_principal) |principal| {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
@ -453,13 +364,14 @@ 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 (config) |c|
|
if (deploy_opts.allow_principal) |principal| {
|
||||||
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
|
||||||
|
|
@ -486,74 +398,23 @@ fn buildEnvVariables(
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update function configuration (environment variables and config settings)
|
/// Update function configuration (environment variables)
|
||||||
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 = if (env_variables) |vars| .{ .variables = vars } else null,
|
.environment = .{ .variables = env_variables },
|
||||||
// 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 function configuration\n", .{});
|
try options.stdout.print("Updated environment variables\n", .{});
|
||||||
try options.stdout.flush();
|
try options.stdout.flush();
|
||||||
|
|
||||||
// Wait for configuration update to complete
|
// Wait for configuration update to complete
|
||||||
|
|
@ -576,17 +437,21 @@ 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) |cfg| {
|
if (result.response.configuration) |config| {
|
||||||
if (cfg.last_update_status) |status| {
|
if (config.last_update_status) |status| {
|
||||||
if (std.mem.eql(u8, status, "Successful")) {
|
if (std.mem.eql(u8, status, "Successful")) {
|
||||||
std.log.debug("Function is ready", .{});
|
std.log.info("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 return; // No status means it's ready
|
} else {
|
||||||
} else return; // No configuration means we can't check, assume ready
|
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);
|
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 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 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, .{});
|
const file = try std.fs.cwd().createFile(output_path, .{});
|
||||||
defer file.close();
|
defer file.close();
|
||||||
|
|
@ -699,7 +564,7 @@ fn writeDeployOutput(
|
||||||
\\ "role_arn": "{s}",
|
\\ "role_arn": "{s}",
|
||||||
\\ "architecture": "{s}",
|
\\ "architecture": "{s}",
|
||||||
\\ "environment_keys": [
|
\\ "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
|
// Write environment variable keys
|
||||||
if (env_vars) |vars| {
|
if (env_vars) |vars| {
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,17 @@
|
||||||
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 config_file: ?[]const u8 = null;
|
var role_name: ?[]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, "--config-file")) {
|
if (std.mem.eql(u8, arg, "--role-name")) {
|
||||||
i += 1;
|
i += 1;
|
||||||
if (i >= args.len) return error.MissingConfigFile;
|
if (i >= args.len) return error.MissingRoleName;
|
||||||
config_file = args[i];
|
role_name = 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();
|
||||||
|
|
@ -33,44 +25,30 @@ pub fn run(args: []const []const u8, options: RunOptions) !void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load config file if provided
|
if (role_name == null) {
|
||||||
var parsed_config = if (config_file) |path|
|
try options.stderr.print("Error: --role-name is required\n", .{});
|
||||||
try LambdaBuildConfig.loadFromFile(options.allocator, path, !is_config_required)
|
printHelp(options.stderr);
|
||||||
else
|
try options.stderr.flush();
|
||||||
null;
|
return error.MissingRoleName;
|
||||||
defer if (parsed_config) |*pc| pc.deinit();
|
}
|
||||||
|
|
||||||
// Get role_name from config or use default
|
const arn = try getOrCreateRole(role_name.?, options);
|
||||||
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: anytype) void {
|
fn printHelp(writer: *std.Io.Writer) 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:
|
||||||
\\ --config-file <path> Path to JSON config file (required, error if missing)
|
\\ --role-name <name> Name of the IAM role (required)
|
||||||
\\ --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.
|
||||||
\\
|
\\
|
||||||
|
|
@ -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
|
// 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("IAM GetRole failed: {} (HTTP {})", .{ err, diagnostics.response_status });
|
std.log.err(
|
||||||
|
"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