Compare commits
No commits in common. "master" and "master" have entirely different histories.
19 changed files with 274 additions and 2425 deletions
|
|
@ -1,37 +0,0 @@
|
||||||
name: Lambda-Zig Build
|
|
||||||
run-name: ${{ github.actor }} building lambda-zig
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- '*'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out repository code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Zig
|
|
||||||
uses: https://codeberg.org/mlugg/setup-zig@v2.2.1
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: zig build --summary all
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: zig build test --summary all
|
|
||||||
|
|
||||||
- name: Build for other platforms
|
|
||||||
run: |
|
|
||||||
zig build -Dtarget=aarch64-linux
|
|
||||||
zig build -Dtarget=x86_64-linux
|
|
||||||
|
|
||||||
- name: Notify
|
|
||||||
uses: https://git.lerch.org/lobo/action-notify-ntfy@v2
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.NTFY_HOST }}
|
|
||||||
topic: ${{ secrets.NTFY_TOPIC }}
|
|
||||||
user: ${{ secrets.NTFY_USER }}
|
|
||||||
password: ${{ secrets.NTFY_PASSWORD }}
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
|
.gyro/
|
||||||
zig-cache/
|
zig-cache/
|
||||||
zig-out/
|
zig-out/
|
||||||
.zig-cache
|
deps.zig
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
[tools]
|
|
||||||
zig = "0.15.2"
|
|
||||||
zls = "0.15.1"
|
|
||||||
"ubi:DonIsaac/zlint" = "0.7.6"
|
|
||||||
prek = "0.3.1"
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
# See https://pre-commit.com for more information
|
|
||||||
# See https://pre-commit.com/hooks.html for more hooks
|
|
||||||
repos:
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v6.0.0
|
|
||||||
hooks:
|
|
||||||
- id: trailing-whitespace
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
- id: check-yaml
|
|
||||||
- id: check-added-large-files
|
|
||||||
- repo: https://github.com/batmac/pre-commit-zig
|
|
||||||
rev: v0.3.0
|
|
||||||
hooks:
|
|
||||||
- id: zig-fmt
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: zlint
|
|
||||||
name: Run zlint
|
|
||||||
entry: zlint
|
|
||||||
args: ["--deny-warnings", "--fix"]
|
|
||||||
language: system
|
|
||||||
types: [zig]
|
|
||||||
- repo: https://github.com/batmac/pre-commit-zig
|
|
||||||
rev: v0.3.0
|
|
||||||
hooks:
|
|
||||||
- id: zig-build
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: test
|
|
||||||
name: Run zig build test
|
|
||||||
entry: zig
|
|
||||||
# args: ["build", "coverage", "-Dcoverage-threshold=80"]
|
|
||||||
args: ["build", "test"]
|
|
||||||
language: system
|
|
||||||
types: [file]
|
|
||||||
pass_filenames: false
|
|
||||||
167
README.md
167
README.md
|
|
@ -1,155 +1,38 @@
|
||||||
lambda-zig: A Custom Runtime for AWS Lambda
|
lambda-zig: A Custom Runtime for AWS Lambda
|
||||||
===========================================
|
===========================================
|
||||||
|
|
||||||
This is a custom runtime built in Zig (0.15). Simple projects will
|
This is a sample custom runtime built in zig. Simple projects will execute
|
||||||
execute in <1ms, with a cold start init time of approximately 11ms.
|
in <1ms, with a cold start init time of approximately 11ms.
|
||||||
|
|
||||||
Custom build steps are available for packaging and deploying Lambda functions:
|
Some custom build steps have been added to build.zig:
|
||||||
|
|
||||||
* `zig build awslambda_package`: Package the Lambda function into a zip file
|
* `zig build iam`: Deploy and record a default IAM role for the lambda function
|
||||||
* `zig build awslambda_iam`: Create or verify IAM role for the Lambda function
|
* `zig build package`: Package the lambda function for upload
|
||||||
* `zig build awslambda_deploy`: Deploy the Lambda function to AWS
|
* `zig build deploy`: Deploy the lambda function
|
||||||
* `zig build awslambda_run`: Invoke the deployed Lambda function
|
* `zig build run`: Run the lambda function
|
||||||
|
|
||||||
Build options:
|
Custom options:
|
||||||
|
|
||||||
* **function-name**: Name of the AWS Lambda function
|
* **debug**: boolean flag to avoid the debug symbols to be stripped. Useful to see
|
||||||
* **payload**: JSON payload for function invocation (used with awslambda_run)
|
error return traces in the AWS Lambda logs
|
||||||
* **region**: AWS region for deployment and invocation
|
* **function-name**: set the name of the AWS Lambda function
|
||||||
* **profile**: AWS profile to use for credentials
|
* **payload**: Use this to set the payload of the function when run using `zig build run`
|
||||||
* **role-name**: IAM role name for the function (default: lambda_basic_execution)
|
|
||||||
* **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)
|
|
||||||
|
|
||||||
The Lambda function can be compiled for x86_64 or aarch64. The build system
|
Additionally, a custom IAM role can be used for the function by appending ``-- --role myawesomerole``
|
||||||
automatically configures the Lambda architecture based on the target.
|
to the `zig build deploy` command. This has not really been tested. The role name
|
||||||
|
is cached in zig-out/bin/iam_role_name, so you can also just set that to the full
|
||||||
|
arn of your iam role if you'd like.
|
||||||
|
|
||||||
A sample project using this runtime can be found at
|
The AWS Lambda function is compiled as a linux ARM64 executable. Since the build.zig
|
||||||
https://git.lerch.org/lobo/lambda-zig-sample
|
calls out to the shell for AWS operations, you will need AWS CLI v2.2.43 or greater.
|
||||||
|
|
||||||
Environment Variables
|
This project vendors dependencies with [gyro](https://github.com/mattnite/gyro), so
|
||||||
---------------------
|
first time build should be done with `gyro build`. This should be working
|
||||||
|
on zig master - certain build.zig constructs are not available in zig 0.8.1.
|
||||||
|
|
||||||
Lambda functions can be configured with environment variables during deployment.
|
|
||||||
This is useful for passing configuration, secrets, or credentials to your function.
|
|
||||||
|
|
||||||
### Using the build system
|
Caveats:
|
||||||
|
|
||||||
Pass the `-Denv-file` option to specify a file containing environment variables:
|
* Small inbound lambda payloads seem to be confusing [requestz](https://github.com/ducdetronquito/requestz),
|
||||||
|
which just never returns, causing timeouts
|
||||||
```sh
|
* Unhandled invocation errors seem to be causing the same problem
|
||||||
zig build awslambda_deploy -Dfunction-name=my-function -Denv-file=.env
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using the CLI directly
|
|
||||||
|
|
||||||
The `lambda-build` CLI supports both `--env` flags and `--env-file`:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Set individual variables
|
|
||||||
./lambda-build deploy --function-name my-fn --zip-file function.zip \
|
|
||||||
--env DB_HOST=localhost --env DB_PORT=5432
|
|
||||||
|
|
||||||
# Load from file
|
|
||||||
./lambda-build deploy --function-name my-fn --zip-file function.zip \
|
|
||||||
--env-file .env
|
|
||||||
|
|
||||||
# Combine both (--env values override --env-file)
|
|
||||||
./lambda-build deploy --function-name my-fn --zip-file function.zip \
|
|
||||||
--env-file .env --env DEBUG=true
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment file format
|
|
||||||
|
|
||||||
The environment file uses a simple `KEY=VALUE` format, one variable per line:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Database configuration
|
|
||||||
DB_HOST=localhost
|
|
||||||
DB_PORT=5432
|
|
||||||
|
|
||||||
# API keys
|
|
||||||
API_KEY=secret123
|
|
||||||
```
|
|
||||||
|
|
||||||
Lines starting with `#` are treated as comments. Empty lines are ignored.
|
|
||||||
|
|
||||||
Service Permissions
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
Lambda functions can be configured to allow invocation by AWS service principals.
|
|
||||||
This is required for services like Alexa Skills Kit, API Gateway, or S3 to trigger
|
|
||||||
your Lambda function.
|
|
||||||
|
|
||||||
### Using the build system
|
|
||||||
|
|
||||||
Pass the `-Dallow-principal` option to grant invoke permission to a service:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Allow Alexa Skills Kit to invoke the function
|
|
||||||
zig build awslambda_deploy -Dfunction-name=my-skill -Dallow-principal=alexa-appkit.amazon.com
|
|
||||||
|
|
||||||
# Allow API Gateway to invoke the function
|
|
||||||
zig build awslambda_deploy -Dfunction-name=my-api -Dallow-principal=apigateway.amazonaws.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using the CLI directly
|
|
||||||
|
|
||||||
```sh
|
|
||||||
./lambda-build deploy --function-name my-fn --zip-file function.zip \
|
|
||||||
--allow-principal alexa-appkit.amazon.com
|
|
||||||
```
|
|
||||||
|
|
||||||
The permission is idempotent - if it already exists, the deployment will continue
|
|
||||||
successfully.
|
|
||||||
|
|
||||||
Using the Zig Package Manager
|
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
To add Lambda package/deployment steps to another project:
|
|
||||||
|
|
||||||
1. Fetch the dependency:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
zig fetch --save git+https://git.lerch.org/lobo/lambda-zig
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Update your `build.zig`:
|
|
||||||
|
|
||||||
```zig
|
|
||||||
const std = @import("std");
|
|
||||||
const lambda_zig = @import("lambda_zig");
|
|
||||||
|
|
||||||
pub fn build(b: *std.Build) !void {
|
|
||||||
const target = b.standardTargetOptions(.{});
|
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
|
||||||
|
|
||||||
// Get lambda-zig dependency
|
|
||||||
const lambda_zig_dep = b.dependency("lambda_zig", .{
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
|
|
||||||
const exe_module = b.createModule(.{
|
|
||||||
.root_source_file = b.path("src/main.zig"),
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add lambda runtime to your module
|
|
||||||
exe_module.addImport("aws_lambda_runtime", lambda_zig_dep.module("lambda_runtime"));
|
|
||||||
|
|
||||||
const exe = b.addExecutable(.{
|
|
||||||
.name = "bootstrap",
|
|
||||||
.root_module = exe_module,
|
|
||||||
});
|
|
||||||
|
|
||||||
b.installArtifact(exe);
|
|
||||||
|
|
||||||
// Add Lambda build steps
|
|
||||||
try lambda_zig.configureBuild(b, lambda_zig_dep, exe);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: The build function return type must be `!void` or catch/deal with errors
|
|
||||||
to support the Lambda build integration.
|
|
||||||
|
|
|
||||||
345
build.zig
345
build.zig
|
|
@ -1,211 +1,174 @@
|
||||||
|
const builtin = @import("builtin");
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const pkgs = @import("deps.zig").pkgs;
|
||||||
|
|
||||||
// Although this function looks imperative, note that its job is to
|
pub fn build(b: *std.build.Builder) !void {
|
||||||
// declaratively construct a build graph that will be executed by an external
|
|
||||||
// runner.
|
|
||||||
pub fn build(b: *std.Build) !void {
|
|
||||||
// Standard target options allows the person running `zig build` to choose
|
// Standard target options allows the person running `zig build` to choose
|
||||||
// what target to build for. Here we do not override the defaults, which
|
// what target to build for. Here we do not override the defaults, which
|
||||||
// means any target is allowed, and the default is native. Other options
|
// means any target is allowed, and the default is native. Other options
|
||||||
// for restricting supported target set are available.
|
// for restricting supported target set are available.
|
||||||
const target = b.standardTargetOptions(.{});
|
// We want the target to be aarch64-linux for deploys
|
||||||
|
const target = std.zig.CrossTarget{
|
||||||
|
.cpu_arch = .aarch64,
|
||||||
|
.os_tag = .linux,
|
||||||
|
};
|
||||||
|
|
||||||
// Standard optimization options allow the person running `zig build` to select
|
// Standard release options allow the person running `zig build` to select
|
||||||
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
|
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
|
||||||
// set a preferred release mode, allowing the user to decide how to optimize.
|
// const mode = b.standardReleaseOptions();
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
|
||||||
|
|
||||||
// Create a module for lambda.zig
|
const exe = b.addExecutable("bootstrap", "src/main.zig");
|
||||||
const lambda_module = b.createModule(.{
|
|
||||||
.root_source_file = b.path("src/lambda.zig"),
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
|
|
||||||
const lib = b.addLibrary(.{
|
pkgs.addAllTo(exe);
|
||||||
.name = "lambda-zig",
|
exe.setTarget(target);
|
||||||
.linkage = .static,
|
exe.setBuildMode(.ReleaseSafe);
|
||||||
.root_module = lambda_module,
|
const debug = b.option(bool, "debug", "Debug mode (do not strip executable)") orelse false;
|
||||||
});
|
exe.strip = !debug;
|
||||||
|
exe.install();
|
||||||
|
|
||||||
// Export the module for other packages to use
|
// TODO: We can cross-compile of course, but stripping and zip commands
|
||||||
_ = b.addModule("lambda_runtime", .{
|
// may vary
|
||||||
.root_source_file = b.path("src/lambda.zig"),
|
if (std.builtin.os.tag == .linux) {
|
||||||
.target = target,
|
// Package step
|
||||||
.optimize = optimize,
|
const package_step = b.step("package", "Package the function");
|
||||||
});
|
package_step.dependOn(b.getInstallStep());
|
||||||
|
// strip may not be installed or work for the target arch
|
||||||
|
// TODO: make this much less fragile
|
||||||
|
const strip = if (debug)
|
||||||
|
try std.fmt.allocPrint(b.allocator, "true", .{})
|
||||||
|
else
|
||||||
|
try std.fmt.allocPrint(b.allocator, "[ -x /usr/aarch64-linux-gnu/bin/strip ] && /usr/aarch64-linux-gnu/bin/strip {s}", .{b.getInstallPath(exe.install_step.?.dest_dir, exe.install_step.?.artifact.out_filename)});
|
||||||
|
defer b.allocator.free(strip);
|
||||||
|
package_step.dependOn(&b.addSystemCommand(&.{ "/bin/sh", "-c", strip }).step);
|
||||||
|
const function_zip = b.getInstallPath(exe.install_step.?.dest_dir, "function.zip");
|
||||||
|
const zip = try std.fmt.allocPrint(b.allocator, "zip -qj9 {s} {s}", .{ function_zip, b.getInstallPath(exe.install_step.?.dest_dir, exe.install_step.?.artifact.out_filename) });
|
||||||
|
defer b.allocator.free(zip);
|
||||||
|
package_step.dependOn(&b.addSystemCommand(&.{ "/bin/sh", "-c", zip }).step);
|
||||||
|
|
||||||
// This declares intent for the library to be installed into the standard
|
// Deployment
|
||||||
// location when the user invokes the "install" step (the default step when
|
const deploy_step = b.step("deploy", "Deploy the function");
|
||||||
// running `zig build`).
|
var deal_with_iam = true;
|
||||||
b.installArtifact(lib);
|
if (b.args) |args| {
|
||||||
|
for (args) |arg| {
|
||||||
|
if (std.mem.eql(u8, "--role", arg)) {
|
||||||
|
deal_with_iam = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var iam_role: []u8 = &.{};
|
||||||
|
const iam_step = b.step("iam", "Create/Get IAM role for function");
|
||||||
|
deploy_step.dependOn(iam_step); // iam_step will either be a noop or all the stuff below
|
||||||
|
if (deal_with_iam) {
|
||||||
|
// if someone adds '-- --role arn...' to the command line, we don't
|
||||||
|
// need to do anything with the iam role. Otherwise, we'll create/
|
||||||
|
// get the IAM role and stick the name in a file in our destination
|
||||||
|
// directory to be used later
|
||||||
|
const iam_role_name_file = b.getInstallPath(exe.install_step.?.dest_dir, "iam_role_name");
|
||||||
|
iam_role = try std.fmt.allocPrint(b.allocator, "--role $(cat {s})", .{iam_role_name_file});
|
||||||
|
// defer b.allocator.free(iam_role);
|
||||||
|
if (!fileExists(iam_role_name_file)) {
|
||||||
|
// Role get/creation command
|
||||||
|
const ifstatement_fmt =
|
||||||
|
\\ if aws iam get-role --role-name lambda_basic_execution 2>&1 |grep -q NoSuchEntity; then aws iam create-role --output text --query Role.Arn --role-name lambda_basic_execution --assume-role-policy-document '{
|
||||||
|
\\ "Version": "2012-10-17",
|
||||||
|
\\ "Statement": [
|
||||||
|
\\ {
|
||||||
|
\\ "Sid": "",
|
||||||
|
\\ "Effect": "Allow",
|
||||||
|
\\ "Principal": {
|
||||||
|
\\ "Service": "lambda.amazonaws.com"
|
||||||
|
\\ },
|
||||||
|
\\ "Action": "sts:AssumeRole"
|
||||||
|
\\ }
|
||||||
|
\\ ]}' > /dev/null; fi && \
|
||||||
|
\\ aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/AWSLambdaExecute --role-name lambda_basic_execution && \
|
||||||
|
\\ aws iam get-role --role-name lambda_basic_execution --query Role.Arn --output text >
|
||||||
|
;
|
||||||
|
|
||||||
// Creates a step for unit testing. This only builds the test executable
|
const ifstatement = try std.mem.concat(b.allocator, u8, &[_][]const u8{ ifstatement_fmt, iam_role_name_file });
|
||||||
// but does not run it.
|
defer b.allocator.free(ifstatement);
|
||||||
const test_module = b.createModule(.{
|
iam_step.dependOn(&b.addSystemCommand(&.{ "/bin/sh", "-c", ifstatement }).step);
|
||||||
.root_source_file = b.path("src/lambda.zig"),
|
}
|
||||||
.target = target,
|
}
|
||||||
.optimize = optimize,
|
const function_name = b.option([]const u8, "function-name", "Function name for Lambda [zig-fn]") orelse "zig-fn";
|
||||||
});
|
const function_name_file = b.getInstallPath(exe.install_step.?.dest_dir, function_name);
|
||||||
|
const ifstatement = "if [ ! -f {s} ] || [ {s} -nt {s} ]; then if aws lambda get-function --function-name {s} 2>&1 |grep -q ResourceNotFoundException; then echo not found > /dev/null; {s}; else echo found > /dev/null; {s}; fi; fi";
|
||||||
|
// The architectures option was introduced in 2.2.43 released 2021-10-01
|
||||||
|
// We want to use arm64 here because it is both faster and cheaper for most
|
||||||
|
// Amazon Linux 2 is the only arm64 supported option
|
||||||
|
const not_found = "aws lambda create-function --architectures arm64 --runtime provided.al2 --function-name {s} --zip-file fileb://{s} --handler not_applicable {s} && touch {s}";
|
||||||
|
const not_found_fmt = try std.fmt.allocPrint(b.allocator, not_found, .{ function_name, function_zip, iam_role, function_name_file });
|
||||||
|
defer b.allocator.free(not_found_fmt);
|
||||||
|
const found = "aws lambda update-function-code --function-name {s} --zip-file fileb://{s} && touch {s}";
|
||||||
|
const found_fmt = try std.fmt.allocPrint(b.allocator, found, .{ function_name, function_zip, function_name_file });
|
||||||
|
defer b.allocator.free(found_fmt);
|
||||||
|
var found_final: []const u8 = undefined;
|
||||||
|
var not_found_final: []const u8 = undefined;
|
||||||
|
if (b.args) |args| {
|
||||||
|
found_final = try addArgs(b.allocator, found_fmt, args);
|
||||||
|
not_found_final = try addArgs(b.allocator, not_found_fmt, args);
|
||||||
|
} else {
|
||||||
|
found_final = found_fmt;
|
||||||
|
not_found_final = not_found_fmt;
|
||||||
|
}
|
||||||
|
const cmd = try std.fmt.allocPrint(b.allocator, ifstatement, .{
|
||||||
|
function_name_file,
|
||||||
|
std.fs.path.dirname(exe.root_src.?.path),
|
||||||
|
function_name_file,
|
||||||
|
function_name,
|
||||||
|
not_found_fmt,
|
||||||
|
found_fmt,
|
||||||
|
});
|
||||||
|
|
||||||
const main_tests = b.addTest(.{
|
defer b.allocator.free(cmd);
|
||||||
.name = "test",
|
|
||||||
.root_module = test_module,
|
|
||||||
});
|
|
||||||
|
|
||||||
const run_main_tests = b.addRunArtifact(main_tests);
|
// std.debug.print("{s}\n", .{cmd});
|
||||||
|
deploy_step.dependOn(package_step);
|
||||||
|
deploy_step.dependOn(&b.addSystemCommand(&.{ "/bin/sh", "-c", cmd }).step);
|
||||||
|
|
||||||
// Build the lambda-build CLI to ensure it compiles
|
// TODO: Looks like IquanaTLS isn't playing nicely with payloads this small
|
||||||
// This catches dependency version mismatches between tools/build and the main project
|
// const payload = b.option([]const u8, "payload", "Lambda payload [{\"foo\":\"bar\"}]") orelse
|
||||||
const lambda_build_dep = b.dependency("lambda_build", .{
|
// \\ {"foo": "bar"}"
|
||||||
.target = b.graph.host,
|
// ;
|
||||||
.optimize = optimize,
|
const payload = b.option([]const u8, "payload", "Lambda payload [{\"foo\":\"bar\", \"baz\": \"qux\"}]") orelse
|
||||||
});
|
\\ {"foo": "bar", "baz": "qux"}"
|
||||||
const lambda_build_exe = lambda_build_dep.artifact("lambda-build");
|
;
|
||||||
|
|
||||||
// This creates a build step. It will be visible in the `zig build --help` menu,
|
const run_script =
|
||||||
// and can be selected like this: `zig build test`
|
\\ f=$(mktemp) && \
|
||||||
// This will evaluate the `test` step rather than the default, which is "install".
|
\\ logs=$(aws lambda invoke \
|
||||||
const test_step = b.step("test", "Run library tests");
|
\\ --cli-binary-format raw-in-base64-out \
|
||||||
test_step.dependOn(&run_main_tests.step);
|
\\ --invocation-type RequestResponse \
|
||||||
test_step.dependOn(&lambda_build_exe.step);
|
\\ --function-name {s} \
|
||||||
|
\\ --payload '{s}' \
|
||||||
|
\\ --log-type Tail \
|
||||||
|
\\ --query LogResult \
|
||||||
|
\\ --output text "$f" |base64 -d) && \
|
||||||
|
\\ cat "$f" && rm "$f" && \
|
||||||
|
\\ echo && echo && echo "$logs"
|
||||||
|
;
|
||||||
|
const run_script_fmt = try std.fmt.allocPrint(b.allocator, run_script, .{ function_name, payload });
|
||||||
|
defer b.allocator.free(run_script_fmt);
|
||||||
|
const run_cmd = b.addSystemCommand(&.{ "/bin/sh", "-c", run_script_fmt });
|
||||||
|
run_cmd.step.dependOn(deploy_step);
|
||||||
|
if (b.args) |args| {
|
||||||
|
run_cmd.addArgs(args);
|
||||||
|
}
|
||||||
|
|
||||||
// Create executable module
|
const run_step = b.step("run", "Run the app");
|
||||||
const exe_module = b.createModule(.{
|
run_step.dependOn(&run_cmd.step);
|
||||||
.root_source_file = b.path("src/sample-main.zig"),
|
}
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
|
|
||||||
const exe = b.addExecutable(.{
|
|
||||||
.name = "custom",
|
|
||||||
.root_module = exe_module,
|
|
||||||
});
|
|
||||||
|
|
||||||
b.installArtifact(exe);
|
|
||||||
try configureBuildInternal(b, exe);
|
|
||||||
}
|
}
|
||||||
|
fn fileExists(file_name: []const u8) bool {
|
||||||
/// Internal version of configureBuild for lambda-zig's own build.
|
const file = std.fs.openFileAbsolute(file_name, .{}) catch return false;
|
||||||
///
|
defer file.close();
|
||||||
/// Both this and configureBuild do the same thing, but resolve the lambda_build
|
return true;
|
||||||
/// dependency differently:
|
|
||||||
///
|
|
||||||
/// - Here: we call `b.dependency("lambda_build", ...)` directly since `b` is
|
|
||||||
/// lambda-zig's own Build context, which has lambda_build in its build.zig.zon
|
|
||||||
///
|
|
||||||
/// - configureBuild: consumers pass in their lambda_zig dependency, and we use
|
|
||||||
/// `lambda_zig_dep.builder.dependency("lambda_build", ...)` to resolve it from
|
|
||||||
/// lambda-zig's build.zig.zon rather than the consumer's
|
|
||||||
///
|
|
||||||
/// This avoids requiring consumers to declare lambda_build as a transitive
|
|
||||||
/// dependency in their own build.zig.zon.
|
|
||||||
fn configureBuildInternal(b: *std.Build, exe: *std.Build.Step.Compile) !void {
|
|
||||||
// When called from lambda-zig's own build, use local dependency
|
|
||||||
const lambda_build_dep = b.dependency("lambda_build", .{
|
|
||||||
.target = b.graph.host,
|
|
||||||
.optimize = .ReleaseSafe,
|
|
||||||
});
|
|
||||||
// Ignore return value for internal builds
|
|
||||||
_ = try @import("lambdabuild.zig").configureBuild(b, lambda_build_dep, exe, .{});
|
|
||||||
}
|
}
|
||||||
|
fn addArgs(allocator: *std.mem.Allocator, original: []const u8, args: [][]const u8) ![]const u8 {
|
||||||
/// Re-export types for consumers
|
var rc = original;
|
||||||
pub const LambdaConfig = @import("lambdabuild.zig").Config;
|
for (args) |arg| {
|
||||||
pub const LambdaBuildInfo = @import("lambdabuild.zig").BuildInfo;
|
rc = try std.mem.concat(allocator, u8, &.{ rc, " ", arg });
|
||||||
|
}
|
||||||
/// Configure Lambda build steps for a Zig project.
|
return rc;
|
||||||
///
|
|
||||||
/// This function adds build steps and options for packaging and deploying
|
|
||||||
/// Lambda functions to AWS. The `lambda_zig_dep` parameter must be the
|
|
||||||
/// dependency object obtained from `b.dependency("lambda_zig", ...)`.
|
|
||||||
///
|
|
||||||
/// Returns a `LambdaBuildInfo` struct containing:
|
|
||||||
/// - References to all build steps (package, iam, deploy, invoke)
|
|
||||||
/// - A `deploy_output` LazyPath to a JSON file with deployment info
|
|
||||||
/// - The function name used
|
|
||||||
///
|
|
||||||
/// ## Build Steps
|
|
||||||
///
|
|
||||||
/// The following build steps are added:
|
|
||||||
///
|
|
||||||
/// - `awslambda_package`: Package the executable into a Lambda deployment zip
|
|
||||||
/// - `awslambda_iam`: Create or verify the IAM role for the Lambda function
|
|
||||||
/// - `awslambda_deploy`: Deploy the function to AWS Lambda (depends on package)
|
|
||||||
/// - `awslambda_run`: Invoke the deployed function (depends on deploy)
|
|
||||||
///
|
|
||||||
/// ## Build Options
|
|
||||||
///
|
|
||||||
/// The following options are added to the build (command-line options override
|
|
||||||
/// config defaults):
|
|
||||||
///
|
|
||||||
/// - `-Dfunction-name=[string]`: Name of the Lambda function
|
|
||||||
/// (default: "zig-fn", or as provided by config parameter)
|
|
||||||
/// - `-Dregion=[string]`: AWS region for deployment and invocation
|
|
||||||
/// - `-Dprofile=[string]`: AWS profile to use for credentials
|
|
||||||
/// - `-Drole-name=[string]`: IAM role name
|
|
||||||
/// (default: "lambda_basic_execution", or as provided by config parameter)
|
|
||||||
/// - `-Dpayload=[string]`: JSON payload for invocation (default: "{}")
|
|
||||||
/// - `-Denv-file=[string]`: Path to environment variables file (KEY=VALUE format)
|
|
||||||
/// - `-Dallow-principal=[string]`: AWS service principal to grant invoke permission
|
|
||||||
/// (e.g., "alexa-appkit.amazon.com" for Alexa Skills Kit)
|
|
||||||
///
|
|
||||||
/// ## Deploy Output
|
|
||||||
///
|
|
||||||
/// The `deploy_output` field in the returned struct is a LazyPath to a JSON file
|
|
||||||
/// containing deployment information (available after deploy completes):
|
|
||||||
///
|
|
||||||
/// ```json
|
|
||||||
/// {
|
|
||||||
/// "arn": "arn:aws:lambda:us-east-1:123456789012:function:my-function",
|
|
||||||
/// "function_name": "my-function",
|
|
||||||
/// "partition": "aws",
|
|
||||||
/// "region": "us-east-1",
|
|
||||||
/// "account_id": "123456789012",
|
|
||||||
/// "role_arn": "arn:aws:iam::123456789012:role/lambda_basic_execution",
|
|
||||||
/// "architecture": "arm64",
|
|
||||||
/// "environment_keys": ["MY_VAR"]
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ## Example
|
|
||||||
///
|
|
||||||
/// ```zig
|
|
||||||
/// const lambda_zig = @import("lambda_zig");
|
|
||||||
///
|
|
||||||
/// pub fn build(b: *std.Build) !void {
|
|
||||||
/// const target = b.standardTargetOptions(.{});
|
|
||||||
/// const optimize = b.standardOptimizeOption(.{});
|
|
||||||
///
|
|
||||||
/// const lambda_zig_dep = b.dependency("lambda_zig", .{
|
|
||||||
/// .target = target,
|
|
||||||
/// .optimize = optimize,
|
|
||||||
/// });
|
|
||||||
///
|
|
||||||
/// const exe = b.addExecutable(.{ ... });
|
|
||||||
/// b.installArtifact(exe);
|
|
||||||
///
|
|
||||||
/// // Configure Lambda build and get deployment info
|
|
||||||
/// const lambda = try lambda_zig.configureBuild(b, lambda_zig_dep, exe, .{
|
|
||||||
/// .default_function_name = "my-function",
|
|
||||||
/// });
|
|
||||||
///
|
|
||||||
/// // Use lambda.deploy_output in other steps that need the ARN
|
|
||||||
/// const my_step = b.addRunArtifact(my_tool);
|
|
||||||
/// my_step.addFileArg(lambda.deploy_output);
|
|
||||||
/// my_step.step.dependOn(lambda.deploy_step); // Ensure deploy runs first
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub fn configureBuild(
|
|
||||||
b: *std.Build,
|
|
||||||
lambda_zig_dep: *std.Build.Dependency,
|
|
||||||
exe: *std.Build.Step.Compile,
|
|
||||||
config: LambdaConfig,
|
|
||||||
) !LambdaBuildInfo {
|
|
||||||
// Get lambda_build from the lambda_zig dependency's Build context
|
|
||||||
const lambda_build_dep = lambda_zig_dep.builder.dependency("lambda_build", .{
|
|
||||||
.target = b.graph.host,
|
|
||||||
.optimize = .ReleaseSafe,
|
|
||||||
});
|
|
||||||
return @import("lambdabuild.zig").configureBuild(b, lambda_build_dep, exe, config);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
.{
|
|
||||||
.name = .lambda_zig,
|
|
||||||
// This is a [Semantic Version](https://semver.org/).
|
|
||||||
// In a future version of Zig it will be used for package deduplication.
|
|
||||||
.version = "0.1.0",
|
|
||||||
.fingerprint = 0xae58341fff376efc,
|
|
||||||
.minimum_zig_version = "0.15.2",
|
|
||||||
.dependencies = .{
|
|
||||||
.lambda_build = .{
|
|
||||||
.path = "tools/build",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Specifies the set of files and directories that are included in this package.
|
|
||||||
// Only files and directories listed here are included in the `hash` that
|
|
||||||
// is computed for this package.
|
|
||||||
// Paths are relative to the build root. Use the empty string (`""`) to refer to
|
|
||||||
// the build root itself.
|
|
||||||
// A directory listed here means that all files within, recursively, are included.
|
|
||||||
.paths = .{
|
|
||||||
"build.zig",
|
|
||||||
"build.zig.zon",
|
|
||||||
"lambdabuild.zig",
|
|
||||||
"src",
|
|
||||||
"tools",
|
|
||||||
"LICENSE",
|
|
||||||
"README.md",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
5
gyro.lock
Normal file
5
gyro.lock
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
pkg default ducdetronquito http 0.1.3
|
||||||
|
pkg default ducdetronquito h11 0.1.1
|
||||||
|
github nektro iguanaTLS 953ad821fae6c920fb82399493663668cd91bde7 src/main.zig 953ad821fae6c920fb82399493663668cd91bde7
|
||||||
|
github MasterQ32 zig-network 15b88658809cac9022ec7d59449b0cd3ebfd0361 network.zig 15b88658809cac9022ec7d59449b0cd3ebfd0361
|
||||||
|
github elerch requestz 1fa8157641300805b9503f98cd201d0959d19631 src/main.zig 1fa8157641300805b9503f98cd201d0959d19631
|
||||||
7
gyro.zzz
Normal file
7
gyro.zzz
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
deps:
|
||||||
|
requestz:
|
||||||
|
src:
|
||||||
|
github:
|
||||||
|
user: elerch
|
||||||
|
repo: requestz
|
||||||
|
ref: 1fa8157641300805b9503f98cd201d0959d19631
|
||||||
193
lambdabuild.zig
193
lambdabuild.zig
|
|
@ -1,193 +0,0 @@
|
||||||
//! Lambda Build Integration for Zig Build System
|
|
||||||
//!
|
|
||||||
//! This module provides build steps for packaging and deploying Lambda functions.
|
|
||||||
//! It builds the lambda-build CLI tool and invokes it for each operation.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
/// Configuration options for Lambda build integration.
|
|
||||||
///
|
|
||||||
/// These provide project-level defaults that can still be overridden
|
|
||||||
/// via command-line options (e.g., `-Dfunction-name=...`).
|
|
||||||
pub const Config = struct {
|
|
||||||
/// Default function name if not specified via -Dfunction-name.
|
|
||||||
/// If null, falls back to the executable name (exe.name).
|
|
||||||
default_function_name: ?[]const u8 = null,
|
|
||||||
|
|
||||||
/// Default IAM role name if not specified via -Drole-name.
|
|
||||||
default_role_name: []const u8 = "lambda_basic_execution",
|
|
||||||
|
|
||||||
/// Default environment file if not specified via -Denv-file.
|
|
||||||
/// If the file doesn't exist, it's silently skipped.
|
|
||||||
default_env_file: ?[]const u8 = ".env",
|
|
||||||
|
|
||||||
/// Default AWS service principal to grant invoke permission.
|
|
||||||
/// For Alexa skills, use "alexa-appkit.amazon.com".
|
|
||||||
default_allow_principal: ?[]const u8 = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Information about the configured Lambda build steps.
|
|
||||||
///
|
|
||||||
/// Returned by `configureBuild` to allow consumers to depend on steps
|
|
||||||
/// and access deployment outputs.
|
|
||||||
pub const BuildInfo = struct {
|
|
||||||
/// Package step - creates the deployment zip
|
|
||||||
package_step: *std.Build.Step,
|
|
||||||
|
|
||||||
/// IAM step - creates/verifies the IAM role
|
|
||||||
iam_step: *std.Build.Step,
|
|
||||||
|
|
||||||
/// Deploy step - deploys the function to AWS Lambda
|
|
||||||
deploy_step: *std.Build.Step,
|
|
||||||
|
|
||||||
/// Invoke step - invokes the deployed function
|
|
||||||
invoke_step: *std.Build.Step,
|
|
||||||
|
|
||||||
/// LazyPath to JSON file with deployment info.
|
|
||||||
/// Contains: arn, function_name, region, account_id, role_arn, architecture, environment_keys
|
|
||||||
/// Available after deploy_step completes.
|
|
||||||
deploy_output: std.Build.LazyPath,
|
|
||||||
|
|
||||||
/// The function name used for deployment
|
|
||||||
function_name: []const u8,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Configure Lambda build steps for a Zig project.
|
|
||||||
///
|
|
||||||
/// Adds the following build steps:
|
|
||||||
/// - awslambda_package: Package the function into a zip file
|
|
||||||
/// - awslambda_iam: Create/verify IAM role
|
|
||||||
/// - awslambda_deploy: Deploy the function to AWS
|
|
||||||
/// - awslambda_run: Invoke the deployed function
|
|
||||||
///
|
|
||||||
/// The `config` parameter allows setting project-level defaults that can
|
|
||||||
/// still be overridden via command-line options.
|
|
||||||
///
|
|
||||||
/// Returns a `BuildInfo` struct containing references to all steps and
|
|
||||||
/// a `deploy_output` LazyPath to the deployment info JSON file.
|
|
||||||
pub fn configureBuild(
|
|
||||||
b: *std.Build,
|
|
||||||
lambda_build_dep: *std.Build.Dependency,
|
|
||||||
exe: *std.Build.Step.Compile,
|
|
||||||
config: Config,
|
|
||||||
) !BuildInfo {
|
|
||||||
// Get the lambda-build CLI artifact from the dependency
|
|
||||||
const cli = lambda_build_dep.artifact("lambda-build");
|
|
||||||
|
|
||||||
// Get configuration options (command-line overrides config defaults)
|
|
||||||
const function_name = b.option([]const u8, "function-name", "Function name for Lambda") orelse config.default_function_name orelse exe.name;
|
|
||||||
const region = b.option([]const u8, "region", "AWS region") orelse null;
|
|
||||||
const profile = b.option([]const u8, "profile", "AWS profile") orelse null;
|
|
||||||
const role_name = b.option(
|
|
||||||
[]const u8,
|
|
||||||
"role-name",
|
|
||||||
"IAM role name (default: lambda_basic_execution)",
|
|
||||||
) orelse config.default_role_name;
|
|
||||||
const payload = b.option(
|
|
||||||
[]const u8,
|
|
||||||
"payload",
|
|
||||||
"Lambda invocation payload",
|
|
||||||
) orelse "{}";
|
|
||||||
const env_file = b.option(
|
|
||||||
[]const u8,
|
|
||||||
"env-file",
|
|
||||||
"Path to environment variables file (KEY=VALUE format)",
|
|
||||||
) orelse config.default_env_file;
|
|
||||||
const allow_principal = b.option(
|
|
||||||
[]const u8,
|
|
||||||
"allow-principal",
|
|
||||||
"AWS service principal to grant invoke permission (e.g., alexa-appkit.amazon.com)",
|
|
||||||
) orelse config.default_allow_principal;
|
|
||||||
|
|
||||||
// Determine architecture for Lambda
|
|
||||||
const target_arch = exe.root_module.resolved_target.?.result.cpu.arch;
|
|
||||||
const arch_str = blk: {
|
|
||||||
switch (target_arch) {
|
|
||||||
.aarch64 => break :blk "aarch64",
|
|
||||||
.x86_64 => break :blk "x86_64",
|
|
||||||
else => {
|
|
||||||
std.log.warn("Unsupported architecture for Lambda: {}, defaulting to x86_64", .{target_arch});
|
|
||||||
break :blk "x86_64";
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Package step - output goes to cache based on input hash
|
|
||||||
const package_cmd = b.addRunArtifact(cli);
|
|
||||||
package_cmd.step.name = try std.fmt.allocPrint(b.allocator, "{s} package", .{cli.name});
|
|
||||||
package_cmd.addArgs(&.{ "package", "--exe" });
|
|
||||||
package_cmd.addFileArg(exe.getEmittedBin());
|
|
||||||
package_cmd.addArgs(&.{"--output"});
|
|
||||||
const zip_output = package_cmd.addOutputFileArg("function.zip");
|
|
||||||
package_cmd.step.dependOn(&exe.step);
|
|
||||||
|
|
||||||
const package_step = b.step("awslambda_package", "Package the Lambda function");
|
|
||||||
package_step.dependOn(&package_cmd.step);
|
|
||||||
|
|
||||||
// IAM step
|
|
||||||
const iam_cmd = b.addRunArtifact(cli);
|
|
||||||
iam_cmd.step.name = try std.fmt.allocPrint(b.allocator, "{s} iam", .{cli.name});
|
|
||||||
if (profile) |p| iam_cmd.addArgs(&.{ "--profile", p });
|
|
||||||
if (region) |r| iam_cmd.addArgs(&.{ "--region", r });
|
|
||||||
iam_cmd.addArgs(&.{ "iam", "--role-name", role_name });
|
|
||||||
|
|
||||||
const iam_step = b.step("awslambda_iam", "Create/verify IAM role for Lambda");
|
|
||||||
iam_step.dependOn(&iam_cmd.step);
|
|
||||||
|
|
||||||
// Deploy step (depends on package)
|
|
||||||
// NOTE: has_side_effects = true ensures this always runs, since AWS state
|
|
||||||
// can change externally (e.g., function deleted via console)
|
|
||||||
const deploy_cmd = b.addRunArtifact(cli);
|
|
||||||
deploy_cmd.has_side_effects = true;
|
|
||||||
deploy_cmd.step.name = try std.fmt.allocPrint(b.allocator, "{s} deploy", .{cli.name});
|
|
||||||
if (profile) |p| deploy_cmd.addArgs(&.{ "--profile", p });
|
|
||||||
if (region) |r| deploy_cmd.addArgs(&.{ "--region", r });
|
|
||||||
deploy_cmd.addArgs(&.{
|
|
||||||
"deploy",
|
|
||||||
"--function-name",
|
|
||||||
function_name,
|
|
||||||
"--zip-file",
|
|
||||||
});
|
|
||||||
deploy_cmd.addFileArg(zip_output);
|
|
||||||
deploy_cmd.addArgs(&.{
|
|
||||||
"--role-name",
|
|
||||||
role_name,
|
|
||||||
"--arch",
|
|
||||||
arch_str,
|
|
||||||
});
|
|
||||||
if (env_file) |ef| deploy_cmd.addArgs(&.{ "--env-file", ef });
|
|
||||||
if (allow_principal) |ap| deploy_cmd.addArgs(&.{ "--allow-principal", ap });
|
|
||||||
// Add deploy output file for deployment info JSON
|
|
||||||
deploy_cmd.addArg("--deploy-output");
|
|
||||||
const deploy_output = deploy_cmd.addOutputFileArg("deploy-output.json");
|
|
||||||
deploy_cmd.step.dependOn(&package_cmd.step);
|
|
||||||
|
|
||||||
const deploy_step = b.step("awslambda_deploy", "Deploy the Lambda function");
|
|
||||||
deploy_step.dependOn(&deploy_cmd.step);
|
|
||||||
|
|
||||||
// Invoke/run step (depends on deploy)
|
|
||||||
const invoke_cmd = b.addRunArtifact(cli);
|
|
||||||
invoke_cmd.step.name = try std.fmt.allocPrint(b.allocator, "{s} invoke", .{cli.name});
|
|
||||||
if (profile) |p| invoke_cmd.addArgs(&.{ "--profile", p });
|
|
||||||
if (region) |r| invoke_cmd.addArgs(&.{ "--region", r });
|
|
||||||
invoke_cmd.addArgs(&.{
|
|
||||||
"invoke",
|
|
||||||
"--function-name",
|
|
||||||
function_name,
|
|
||||||
"--payload",
|
|
||||||
payload,
|
|
||||||
});
|
|
||||||
invoke_cmd.step.dependOn(&deploy_cmd.step);
|
|
||||||
|
|
||||||
const run_step = b.step("awslambda_run", "Invoke the deployed Lambda function");
|
|
||||||
run_step.dependOn(&invoke_cmd.step);
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.package_step = package_step,
|
|
||||||
.iam_step = iam_step,
|
|
||||||
.deploy_step = deploy_step,
|
|
||||||
.invoke_step = run_step,
|
|
||||||
.deploy_output = deploy_output,
|
|
||||||
.function_name = function_name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
562
src/lambda.zig
562
src/lambda.zig
|
|
@ -1,500 +1,96 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const requestz = @import("requestz");
|
||||||
|
|
||||||
pub const HandlerFn = *const fn (std.mem.Allocator, []const u8) anyerror![]const u8;
|
pub fn run(event_handler: fn (*std.mem.Allocator, []const u8) anyerror![]const u8) !void { // TODO: remove inferred error set?
|
||||||
|
const prefix = "http://";
|
||||||
const log = std.log.scoped(.lambda);
|
const postfix = "/2018-06-01/runtime/invocation";
|
||||||
|
const lambda_runtime_uri = std.os.getenv("AWS_LAMBDA_RUNTIME_API");
|
||||||
var client: ?std.http.Client = null;
|
|
||||||
|
|
||||||
const prefix = "http://";
|
|
||||||
const postfix = "/2018-06-01/runtime/invocation";
|
|
||||||
|
|
||||||
pub fn deinit() void {
|
|
||||||
if (client) |*c| c.deinit();
|
|
||||||
client = null;
|
|
||||||
}
|
|
||||||
/// Starts the lambda framework. Handler will be called when an event is processing
|
|
||||||
/// If an allocator is not provided, an approrpriate allocator will be selected and used
|
|
||||||
/// This function is intended to loop infinitely. If not used in this manner,
|
|
||||||
/// make sure to call the deinit() function
|
|
||||||
pub fn run(allocator: ?std.mem.Allocator, event_handler: HandlerFn) !void { // TODO: remove inferred error set?
|
|
||||||
const lambda_runtime_uri = std.posix.getenv("AWS_LAMBDA_RUNTIME_API") orelse test_lambda_runtime_uri.?;
|
|
||||||
// TODO: If this is null, go into single use command line mode
|
|
||||||
|
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
defer _ = gpa.deinit();
|
defer _ = gpa.deinit();
|
||||||
const alloc = allocator orelse gpa.allocator();
|
const allocator = &gpa.allocator;
|
||||||
|
|
||||||
const url = try std.fmt.allocPrint(alloc, "{s}{s}{s}/next", .{ prefix, lambda_runtime_uri, postfix });
|
const url = try std.fmt.allocPrint(allocator, "{s}{s}{s}/next", .{ prefix, lambda_runtime_uri, postfix });
|
||||||
defer alloc.free(url);
|
defer allocator.free(url);
|
||||||
const uri = try std.Uri.parse(url);
|
|
||||||
|
|
||||||
// TODO: Simply adding this line without even using the client is enough
|
std.log.notice("Bootstrap initializing with event url: {s}", .{url});
|
||||||
// to cause seg faults!?
|
|
||||||
// client = client orelse .{ .allocator = alloc };
|
|
||||||
// so we'll do this instead
|
|
||||||
if (client != null) return error.MustDeInitBeforeCallingRunAgain;
|
|
||||||
client = .{ .allocator = alloc };
|
|
||||||
log.info("tid {d} (lambda): Bootstrap initializing with event url: {s}", .{ std.Thread.getCurrentId(), url });
|
|
||||||
|
|
||||||
while (lambda_remaining_requests == null or lambda_remaining_requests.? > 0) {
|
while (true) {
|
||||||
if (lambda_remaining_requests) |*r| {
|
var req_alloc = std.heap.ArenaAllocator.init(allocator);
|
||||||
// we're under test
|
|
||||||
log.debug("lambda remaining requests: {d}", .{r.*});
|
|
||||||
r.* -= 1;
|
|
||||||
}
|
|
||||||
var req_alloc = std.heap.ArenaAllocator.init(alloc);
|
|
||||||
defer req_alloc.deinit();
|
defer req_alloc.deinit();
|
||||||
const req_allocator = req_alloc.allocator();
|
const req_allocator = &req_alloc.allocator;
|
||||||
|
var client = try requestz.Client.init(req_allocator);
|
||||||
// Fundamentally we're doing 3 things:
|
// defer client.deinit();
|
||||||
// 1. Get the next event from Lambda (event data and request id)
|
// Lambda freezes the process at this line of code. During warm start,
|
||||||
// 2. Call our handler to get the response
|
// the process will unfreeze and data will be sent in response to client.get
|
||||||
// 3. Post the response back to Lambda
|
var response = client.get(url, .{}) catch |err| {
|
||||||
var ev = getEvent(req_allocator, uri) catch |err| {
|
std.log.err("Get fail: {}", .{err});
|
||||||
// Well, at this point all we can do is shout at the void
|
|
||||||
log.err("Error fetching event details: {}", .{err});
|
|
||||||
std.posix.exit(1);
|
|
||||||
// continue;
|
|
||||||
};
|
|
||||||
if (ev == null) continue; // this gets logged in getEvent, and without
|
|
||||||
// a request id, we still can't do anything
|
|
||||||
// reasonable to report back
|
|
||||||
const event = ev.?;
|
|
||||||
defer ev.?.deinit();
|
|
||||||
const event_response = event_handler(req_allocator, event.event_data) catch |err| {
|
|
||||||
event.reportError(@errorReturnTrace(), err, lambda_runtime_uri) catch @panic("Error reporting error");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
event.postResponse(lambda_runtime_uri, event_response) catch |err| {
|
|
||||||
event.reportError(@errorReturnTrace(), err, lambda_runtime_uri) catch @panic("Error reporting error");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Event = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
event_data: []const u8,
|
|
||||||
request_id: []const u8,
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, event_data: []const u8, request_id: []const u8) Self {
|
|
||||||
return .{
|
|
||||||
.allocator = allocator,
|
|
||||||
.event_data = event_data,
|
|
||||||
.request_id = request_id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
pub fn deinit(self: *Self) void {
|
|
||||||
self.allocator.free(self.event_data);
|
|
||||||
self.allocator.free(self.request_id);
|
|
||||||
}
|
|
||||||
fn reportError(
|
|
||||||
self: Self,
|
|
||||||
return_trace: ?*std.builtin.StackTrace,
|
|
||||||
err: anytype,
|
|
||||||
lambda_runtime_uri: []const u8,
|
|
||||||
) !void {
|
|
||||||
// If we fail in this function, we're pretty hosed up
|
|
||||||
if (return_trace) |rt|
|
|
||||||
log.err("Caught error: {}. Return Trace: {any}", .{ err, rt })
|
|
||||||
else
|
|
||||||
log.err("Caught error: {}. No return trace available", .{err});
|
|
||||||
const err_url = try std.fmt.allocPrint(
|
|
||||||
self.allocator,
|
|
||||||
"{s}{s}{s}/{s}/error",
|
|
||||||
.{ prefix, lambda_runtime_uri, postfix, self.request_id },
|
|
||||||
);
|
|
||||||
defer self.allocator.free(err_url);
|
|
||||||
const err_uri = try std.Uri.parse(err_url);
|
|
||||||
const content =
|
|
||||||
\\{{
|
|
||||||
\\ "errorMessage": "{s}",
|
|
||||||
\\ "errorType": "HandlerReturnedError",
|
|
||||||
\\ "stackTrace": [ "{any}" ]
|
|
||||||
\\}}
|
|
||||||
;
|
|
||||||
const content_fmt = if (return_trace) |rt|
|
|
||||||
try std.fmt.allocPrint(self.allocator, content, .{ @errorName(err), rt })
|
|
||||||
else
|
|
||||||
try std.fmt.allocPrint(self.allocator, content, .{ @errorName(err), "no return trace available" });
|
|
||||||
defer self.allocator.free(content_fmt);
|
|
||||||
log.err("Posting to {s}: Data {s}", .{ err_url, content_fmt });
|
|
||||||
|
|
||||||
// TODO: There is something up with using a shared client in this way
|
|
||||||
// so we're taking a perf hit in favor of stability. In a practical
|
|
||||||
// sense, without making HTTPS connections (lambda environment is
|
|
||||||
// non-ssl), this shouldn't be a big issue
|
|
||||||
var cl = std.http.Client{ .allocator = self.allocator };
|
|
||||||
defer cl.deinit();
|
|
||||||
|
|
||||||
var req = cl.request(.POST, err_uri, .{
|
|
||||||
.extra_headers = &.{
|
|
||||||
.{
|
|
||||||
.name = "Lambda-Runtime-Function-Error-Type",
|
|
||||||
.value = "HandlerReturned",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}) catch |req_err| {
|
|
||||||
log.err("Error creating request for request id {s}: {}", .{ self.request_id, req_err });
|
|
||||||
std.posix.exit(1);
|
|
||||||
};
|
|
||||||
defer req.deinit();
|
|
||||||
|
|
||||||
req.transfer_encoding = .{ .content_length = content_fmt.len };
|
|
||||||
var body_writer = req.sendBodyUnflushed(&.{}) catch |send_err| {
|
|
||||||
log.err("Error sending body for request id {s}: {}", .{ self.request_id, send_err });
|
|
||||||
std.posix.exit(1);
|
|
||||||
};
|
|
||||||
body_writer.writer.writeAll(content_fmt) catch |write_err| {
|
|
||||||
log.err("Error writing body for request id {s}: {}", .{ self.request_id, write_err });
|
|
||||||
std.posix.exit(1);
|
|
||||||
};
|
|
||||||
body_writer.end() catch |end_err| {
|
|
||||||
log.err("Error ending body for request id {s}: {}", .{ self.request_id, end_err });
|
|
||||||
std.posix.exit(1);
|
|
||||||
};
|
|
||||||
req.connection.?.flush() catch |flush_err| {
|
|
||||||
log.err("Error flushing for request id {s}: {}", .{ self.request_id, flush_err });
|
|
||||||
std.posix.exit(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
var redirect_buffer: [1024]u8 = undefined;
|
|
||||||
const response = req.receiveHead(&redirect_buffer) catch |recv_err| {
|
|
||||||
log.err("Error receiving response for request id {s}: {}", .{ self.request_id, recv_err });
|
|
||||||
std.posix.exit(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (response.head.status != .ok) {
|
|
||||||
// Documentation says something about "exit immediately". The
|
// Documentation says something about "exit immediately". The
|
||||||
// Lambda infrastrucutre restarts, so it's unclear if that's necessary.
|
// Lambda infrastrucutre restarts, so it's unclear if that's necessary.
|
||||||
// It seems as though a continue should be fine, and slightly faster
|
// It seems as though a continue should be fine, and slightly faster
|
||||||
log.err("Post fail: {} {s}", .{
|
// std.os.exit(1);
|
||||||
@intFromEnum(response.head.status),
|
continue;
|
||||||
response.head.reason,
|
|
||||||
});
|
|
||||||
std.posix.exit(1);
|
|
||||||
}
|
|
||||||
log.err("Error reporting post complete", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn postResponse(self: Self, lambda_runtime_uri: []const u8, event_response: []const u8) !void {
|
|
||||||
const response_url = try std.fmt.allocPrint(
|
|
||||||
self.allocator,
|
|
||||||
"{s}{s}{s}/{s}/response",
|
|
||||||
.{ prefix, lambda_runtime_uri, postfix, self.request_id },
|
|
||||||
);
|
|
||||||
defer self.allocator.free(response_url);
|
|
||||||
const response_uri = try std.Uri.parse(response_url);
|
|
||||||
|
|
||||||
var cl = std.http.Client{ .allocator = self.allocator };
|
|
||||||
defer cl.deinit();
|
|
||||||
|
|
||||||
// Lambda does different things, depending on the runtime. Go 1.x takes
|
|
||||||
// any return value but escapes double quotes. Custom runtimes can
|
|
||||||
// do whatever they want. node I believe wraps as a json object. We're
|
|
||||||
// going to leave the return value up to the handler, and they can
|
|
||||||
// use a seperate API for normalization so we're explicit. As a result,
|
|
||||||
// we can just post event_response completely raw here
|
|
||||||
|
|
||||||
var req = try cl.request(.POST, response_uri, .{});
|
|
||||||
defer req.deinit();
|
|
||||||
|
|
||||||
req.transfer_encoding = .{ .content_length = event_response.len };
|
|
||||||
var body_writer = try req.sendBodyUnflushed(&.{});
|
|
||||||
try body_writer.writer.writeAll(event_response);
|
|
||||||
try body_writer.end();
|
|
||||||
try req.connection.?.flush();
|
|
||||||
|
|
||||||
var redirect_buffer: [1024]u8 = undefined;
|
|
||||||
const response = try req.receiveHead(&redirect_buffer);
|
|
||||||
|
|
||||||
// Lambda Runtime API returns 202 Accepted for successful response posts
|
|
||||||
if (response.head.status != .ok and response.head.status != .accepted) {
|
|
||||||
return error.UnexpectedStatusFromPostResponse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn getEvent(allocator: std.mem.Allocator, event_data_uri: std.Uri) !?Event {
|
|
||||||
// TODO: There is something up with using a shared client in this way
|
|
||||||
// so we're taking a perf hit in favor of stability. In a practical
|
|
||||||
// sense, without making HTTPS connections (lambda environment is
|
|
||||||
// non-ssl), this shouldn't be a big issue
|
|
||||||
var cl = std.http.Client{ .allocator = allocator };
|
|
||||||
defer cl.deinit();
|
|
||||||
|
|
||||||
// Lambda freezes the process at this line of code. During warm start,
|
|
||||||
// the process will unfreeze and data will be sent in response to client.get
|
|
||||||
var req = try cl.request(.GET, event_data_uri, .{});
|
|
||||||
defer req.deinit();
|
|
||||||
|
|
||||||
try req.sendBodiless();
|
|
||||||
|
|
||||||
var redirect_buffer: [0]u8 = undefined;
|
|
||||||
var response = try req.receiveHead(&redirect_buffer);
|
|
||||||
|
|
||||||
if (response.head.status != .ok) {
|
|
||||||
// Documentation says something about "exit immediately". The
|
|
||||||
// Lambda infrastrucutre restarts, so it's unclear if that's necessary.
|
|
||||||
// It seems as though a continue should be fine, and slightly faster
|
|
||||||
// std.os.exit(1);
|
|
||||||
log.err("Lambda server event response returned bad error code: {} {s}", .{
|
|
||||||
@intFromEnum(response.head.status),
|
|
||||||
response.head.reason,
|
|
||||||
});
|
|
||||||
return error.EventResponseNotOkResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract request ID from response headers
|
|
||||||
var request_id: ?[]const u8 = null;
|
|
||||||
var header_it = response.head.iterateHeaders();
|
|
||||||
while (header_it.next()) |h| {
|
|
||||||
if (std.ascii.eqlIgnoreCase(h.name, "Lambda-Runtime-Aws-Request-Id"))
|
|
||||||
request_id = h.value;
|
|
||||||
// TODO: XRay uses an environment variable to do its magic. It's our
|
|
||||||
// responsibility to set this, but no zig-native setenv(3)/putenv(3)
|
|
||||||
// exists. I would kind of rather not link in libc for this,
|
|
||||||
// so we'll hold for now and think on this
|
|
||||||
// if (std.mem.indexOf(u8, h.name.value, "Lambda-Runtime-Trace-Id")) |_|
|
|
||||||
// std.process.
|
|
||||||
// std.os.setenv("AWS_LAMBDA_RUNTIME_API");
|
|
||||||
}
|
|
||||||
if (request_id == null) {
|
|
||||||
// We can't report back an issue because the runtime error reporting endpoint
|
|
||||||
// uses request id in its path. So the best we can do is log the error and move
|
|
||||||
// on here.
|
|
||||||
log.err("Could not find request id: skipping request", .{});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const req_id = request_id.?;
|
|
||||||
log.debug("got lambda request with id {s}", .{req_id});
|
|
||||||
|
|
||||||
// Read response body using a transfer buffer
|
|
||||||
var transfer_buffer: [64 * 1024]u8 = undefined;
|
|
||||||
const body_reader = response.reader(&transfer_buffer);
|
|
||||||
|
|
||||||
// Read all data into an allocated buffer
|
|
||||||
// We use content_length if available, otherwise read chunks
|
|
||||||
const content_len = response.head.content_length orelse (10 * 1024 * 1024); // 10MB max if not specified
|
|
||||||
var event_data = try allocator.alloc(u8, content_len);
|
|
||||||
errdefer allocator.free(event_data);
|
|
||||||
|
|
||||||
var total_read: usize = 0;
|
|
||||||
while (total_read < content_len) {
|
|
||||||
const remaining = event_data[total_read..];
|
|
||||||
const bytes_read = body_reader.readSliceShort(remaining) catch |err| switch (err) {
|
|
||||||
error.ReadFailed => return error.ReadFailed,
|
|
||||||
};
|
};
|
||||||
if (bytes_read == 0) break;
|
defer response.deinit();
|
||||||
total_read += bytes_read;
|
|
||||||
}
|
|
||||||
event_data = try allocator.realloc(event_data, total_read);
|
|
||||||
|
|
||||||
return Event.init(
|
var request_id: ?[]const u8 = null;
|
||||||
allocator,
|
for (response.headers.items()) |h| {
|
||||||
event_data,
|
if (std.mem.indexOf(u8, h.name.value, "Lambda-Runtime-Aws-Request-Id")) |_|
|
||||||
try allocator.dupe(u8, req_id),
|
request_id = h.value;
|
||||||
);
|
// TODO: XRay uses an environment variable to do its magic. It's our
|
||||||
}
|
// responsibility to set this, but no zig-native setenv(3)/putenv(3)
|
||||||
|
// exists. I would kind of rather not link in libc for this,
|
||||||
////////////////////////////////////////////////////////////////////////
|
// so we'll hold for now and think on this
|
||||||
// All code below this line is for testing
|
// if (std.mem.indexOf(u8, h.name.value, "Lambda-Runtime-Trace-Id")) |_|
|
||||||
////////////////////////////////////////////////////////////////////////
|
// std.process.
|
||||||
var server_port: ?u16 = null;
|
// std.os.setenv("AWS_LAMBDA_RUNTIME_API");
|
||||||
var server_remaining_requests: usize = 0;
|
|
||||||
var lambda_remaining_requests: ?usize = null;
|
|
||||||
var server_response: []const u8 = "unset";
|
|
||||||
var server_request_aka_lambda_response: []u8 = "";
|
|
||||||
var test_lambda_runtime_uri: ?[]u8 = null;
|
|
||||||
|
|
||||||
var server_ready = false;
|
|
||||||
/// This starts a test server. We're not testing the server itself,
|
|
||||||
/// so the main tests will start this thing up and create an arena around the
|
|
||||||
/// whole thing so we can just deallocate everything at once at the end,
|
|
||||||
/// leaks be damned
|
|
||||||
fn startServer(allocator: std.mem.Allocator) !std.Thread {
|
|
||||||
return try std.Thread.spawn(
|
|
||||||
.{},
|
|
||||||
threadMain,
|
|
||||||
.{allocator},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn threadMain(allocator: std.mem.Allocator) !void {
|
|
||||||
const address = try std.net.Address.parseIp("127.0.0.1", 0);
|
|
||||||
var http_server = try address.listen(.{ .reuse_address = true });
|
|
||||||
server_port = http_server.listen_address.in.getPort();
|
|
||||||
|
|
||||||
test_lambda_runtime_uri = try std.fmt.allocPrint(allocator, "127.0.0.1:{d}", .{server_port.?});
|
|
||||||
log.debug("server listening at {s}", .{test_lambda_runtime_uri.?});
|
|
||||||
defer test_lambda_runtime_uri = null;
|
|
||||||
defer server_port = null;
|
|
||||||
log.info("starting server thread, tid {d}", .{std.Thread.getCurrentId()});
|
|
||||||
var arena = std.heap.ArenaAllocator.init(allocator);
|
|
||||||
defer arena.deinit();
|
|
||||||
const aa = arena.allocator();
|
|
||||||
// We're in control of all requests/responses, so this flag will tell us
|
|
||||||
// when it's time to shut down
|
|
||||||
while (server_remaining_requests > 0) {
|
|
||||||
server_remaining_requests -= 1;
|
|
||||||
|
|
||||||
processRequest(aa, &http_server) catch |e| {
|
|
||||||
log.err("Unexpected error processing request: {any}", .{e});
|
|
||||||
if (@errorReturnTrace()) |trace| {
|
|
||||||
std.debug.dumpStackTrace(trace.*);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn processRequest(allocator: std.mem.Allocator, server: *std.net.Server) !void {
|
|
||||||
server_ready = true;
|
|
||||||
errdefer server_ready = false;
|
|
||||||
log.debug(
|
|
||||||
"tid {d} (server): server waiting to accept. requests remaining: {d}",
|
|
||||||
.{ std.Thread.getCurrentId(), server_remaining_requests + 1 },
|
|
||||||
);
|
|
||||||
var connection = try server.accept();
|
|
||||||
defer connection.stream.close();
|
|
||||||
server_ready = false;
|
|
||||||
|
|
||||||
var read_buffer: [1024 * 16]u8 = undefined;
|
|
||||||
var write_buffer: [1024 * 16]u8 = undefined;
|
|
||||||
var stream_reader = std.net.Stream.Reader.init(connection.stream, &read_buffer);
|
|
||||||
var stream_writer = std.net.Stream.Writer.init(connection.stream, &write_buffer);
|
|
||||||
|
|
||||||
var http_server = std.http.Server.init(stream_reader.interface(), &stream_writer.interface);
|
|
||||||
|
|
||||||
const request = http_server.receiveHead() catch |err| switch (err) {
|
|
||||||
error.HttpConnectionClosing => return,
|
|
||||||
else => {
|
|
||||||
std.log.err("closing http connection: {s}", .{@errorName(err)});
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Read request body if present
|
|
||||||
if (request.head.content_length) |content_len| {
|
|
||||||
if (content_len > 0) {
|
|
||||||
var body_transfer_buffer: [64 * 1024]u8 = undefined;
|
|
||||||
const body_reader = http_server.reader.bodyReader(&body_transfer_buffer, request.head.transfer_encoding, request.head.content_length);
|
|
||||||
var body_data = try allocator.alloc(u8, content_len);
|
|
||||||
errdefer allocator.free(body_data);
|
|
||||||
var total_read: usize = 0;
|
|
||||||
while (total_read < content_len) {
|
|
||||||
const remaining = body_data[total_read..];
|
|
||||||
const bytes_read = body_reader.readSliceShort(remaining) catch break;
|
|
||||||
if (bytes_read == 0) break;
|
|
||||||
total_read += bytes_read;
|
|
||||||
}
|
|
||||||
server_request_aka_lambda_response = try allocator.realloc(body_data, total_read);
|
|
||||||
}
|
}
|
||||||
|
if (request_id == null) {
|
||||||
|
// We can't report back an issue because the runtime error reporting endpoint
|
||||||
|
// uses request id in its path. So the best we can do is log the error and move
|
||||||
|
// on here.
|
||||||
|
std.log.err("Could not find request id: skipping request", .{});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const req_id = request_id.?;
|
||||||
|
|
||||||
|
const event_response = event_handler(req_allocator, response.body) catch |err| {
|
||||||
|
// Stack trace will return null if stripped
|
||||||
|
const return_trace = @errorReturnTrace();
|
||||||
|
std.log.err("Caught error: {}. Return Trace: {}", .{ err, return_trace });
|
||||||
|
const err_url = try std.fmt.allocPrint(req_allocator, "{s}{s}/runtime/invocation/{s}/error", .{ prefix, lambda_runtime_uri, req_id });
|
||||||
|
defer req_allocator.free(err_url);
|
||||||
|
const content =
|
||||||
|
\\ {s}
|
||||||
|
\\ "errorMessage": "{s}",
|
||||||
|
\\ "errorType": "HandlerReturnedError",
|
||||||
|
\\ "stackTrace": [ "{}" ]
|
||||||
|
\\ {s}
|
||||||
|
;
|
||||||
|
const content_fmt = try std.fmt.allocPrint(req_allocator, content, .{ "{", @errorName(err), return_trace, "}" });
|
||||||
|
defer req_allocator.free(content_fmt);
|
||||||
|
std.log.err("Posting to {s}: Data {s}", .{ err_url, content_fmt });
|
||||||
|
var headers = .{.{ "Lambda-Runtime-Function-Error-Type", "HandlerReturned" }};
|
||||||
|
// TODO: Determine why this post is not returning
|
||||||
|
var err_resp = client.post(err_url, .{
|
||||||
|
.content = content_fmt,
|
||||||
|
.headers = headers,
|
||||||
|
}) catch |post_err| { // Well, at this point all we can do is shout at the void
|
||||||
|
std.log.err("Error posting response for request id {s}: {}", .{ req_id, post_err });
|
||||||
|
std.os.exit(0);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
std.log.err("Post complete", .{});
|
||||||
|
defer err_resp.deinit();
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
const response_url = try std.fmt.allocPrint(req_allocator, "{s}{s}{s}/{s}/response", .{ prefix, lambda_runtime_uri, postfix, req_id });
|
||||||
|
// defer req_allocator.free(response_url);
|
||||||
|
var resp_resp = client.post(response_url, .{ .content = event_response }) catch |err| {
|
||||||
|
// TODO: report error
|
||||||
|
std.log.err("Error posting response for request id {s}: {}", .{ req_id, err });
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
defer resp_resp.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build and send response
|
|
||||||
const response_bytes = serve();
|
|
||||||
var respond_request = request;
|
|
||||||
try respond_request.respond(response_bytes, .{
|
|
||||||
.extra_headers = &.{
|
|
||||||
.{ .name = "Lambda-Runtime-Aws-Request-Id", .value = "69" },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
log.debug(
|
|
||||||
"tid {d} (server): sent response: {s}",
|
|
||||||
.{ std.Thread.getCurrentId(), response_bytes },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serve() []const u8 {
|
|
||||||
return server_response;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handler(allocator: std.mem.Allocator, event_data: []const u8) ![]const u8 {
|
|
||||||
_ = allocator;
|
|
||||||
return event_data;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn test_lambda_request(allocator: std.mem.Allocator, request: []const u8, request_count: usize, handler_fn: HandlerFn) ![]u8 {
|
|
||||||
var arena = std.heap.ArenaAllocator.init(allocator);
|
|
||||||
defer arena.deinit();
|
|
||||||
const aa = arena.allocator();
|
|
||||||
// Setup our server to run, and set the response for the server to the
|
|
||||||
// request. There is a cognitive disconnect here between mental model and
|
|
||||||
// physical model.
|
|
||||||
//
|
|
||||||
// Mental model:
|
|
||||||
//
|
|
||||||
// Lambda request -> λ -> Lambda response
|
|
||||||
//
|
|
||||||
// Physcial Model:
|
|
||||||
//
|
|
||||||
// 1. λ requests instructions from server
|
|
||||||
// 2. server provides "Lambda request"
|
|
||||||
// 3. λ posts response back to server
|
|
||||||
//
|
|
||||||
// So here we are setting up our server, then our lambda request loop,
|
|
||||||
// but it all needs to be in seperate threads so we can control startup
|
|
||||||
// and shut down. Both server and Lambda are set up to watch global variable
|
|
||||||
// booleans to know when to shut down. This function is designed for a
|
|
||||||
// single request/response pair only
|
|
||||||
|
|
||||||
lambda_remaining_requests = request_count;
|
|
||||||
server_remaining_requests = lambda_remaining_requests.? * 2; // Lambda functions
|
|
||||||
// fetch from the server,
|
|
||||||
// then post back. Always
|
|
||||||
// 2, no more, no less
|
|
||||||
server_response = request; // set our instructions to lambda, which in our
|
|
||||||
// physical model above, is the server response
|
|
||||||
defer server_response = "unset"; // set it back so we don't get confused later
|
|
||||||
// when subsequent tests fail
|
|
||||||
const server_thread = try startServer(aa); // start the server, get it ready
|
|
||||||
while (!server_ready)
|
|
||||||
std.Thread.sleep(100);
|
|
||||||
|
|
||||||
log.debug("tid {d} (main): server reports ready", .{std.Thread.getCurrentId()});
|
|
||||||
// we aren't testing the server,
|
|
||||||
// so we'll use the arena allocator
|
|
||||||
defer server_thread.join(); // we'll be shutting everything down before we exit
|
|
||||||
|
|
||||||
// Now we need to start the lambda framework
|
|
||||||
try run(allocator, handler_fn); // We want our function under test to report leaks
|
|
||||||
return try allocator.dupe(u8, server_request_aka_lambda_response);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "basic request" {
|
|
||||||
// std.testing.log_level = .debug;
|
|
||||||
const allocator = std.testing.allocator;
|
|
||||||
const request =
|
|
||||||
\\{"foo": "bar", "baz": "qux"}
|
|
||||||
;
|
|
||||||
|
|
||||||
// This is what's actually coming back. Is this right?
|
|
||||||
const expected_response =
|
|
||||||
\\{"foo": "bar", "baz": "qux"}
|
|
||||||
;
|
|
||||||
const lambda_response = try test_lambda_request(allocator, request, 1, handler);
|
|
||||||
defer deinit();
|
|
||||||
defer allocator.free(lambda_response);
|
|
||||||
try std.testing.expectEqualStrings(expected_response, lambda_response);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "several requests do not fail" {
|
|
||||||
// std.testing.log_level = .debug;
|
|
||||||
const allocator = std.testing.allocator;
|
|
||||||
const request =
|
|
||||||
\\{"foo": "bar", "baz": "qux"}
|
|
||||||
;
|
|
||||||
|
|
||||||
// This is what's actually coming back. Is this right?
|
|
||||||
const expected_response =
|
|
||||||
\\{"foo": "bar", "baz": "qux"}
|
|
||||||
;
|
|
||||||
const lambda_response = try test_lambda_request(allocator, request, 5, handler);
|
|
||||||
defer deinit();
|
|
||||||
defer allocator.free(lambda_response);
|
|
||||||
try std.testing.expectEqualStrings(expected_response, lambda_response);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ const std = @import("std");
|
||||||
const lambda = @import("lambda.zig");
|
const lambda = @import("lambda.zig");
|
||||||
|
|
||||||
pub fn main() anyerror!void {
|
pub fn main() anyerror!void {
|
||||||
try lambda.run(null, handler);
|
try lambda.run(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handler(allocator: std.mem.Allocator, event_data: []const u8) ![]const u8 {
|
fn handler(allocator: *std.mem.Allocator, event_data: []const u8) ![]const u8 {
|
||||||
_ = allocator;
|
_ = allocator;
|
||||||
return event_data;
|
return event_data;
|
||||||
}
|
}
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
pub fn build(b: *std.Build) void {
|
|
||||||
const target = b.standardTargetOptions(.{});
|
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
|
||||||
|
|
||||||
// Create the main module for the CLI
|
|
||||||
const main_module = b.createModule(.{
|
|
||||||
.root_source_file = b.path("src/main.zig"),
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add aws dependency to the module
|
|
||||||
const aws_dep = b.dependency("aws", .{ .target = target, .optimize = optimize });
|
|
||||||
main_module.addImport("aws", aws_dep.module("aws"));
|
|
||||||
|
|
||||||
const exe = b.addExecutable(.{
|
|
||||||
.name = "lambda-build",
|
|
||||||
.root_module = main_module,
|
|
||||||
});
|
|
||||||
|
|
||||||
b.installArtifact(exe);
|
|
||||||
|
|
||||||
// Run step for testing: zig build run -- package --exe /path/to/exe --output /path/to/out.zip
|
|
||||||
const run_cmd = b.addRunArtifact(exe);
|
|
||||||
run_cmd.step.dependOn(b.getInstallStep());
|
|
||||||
if (b.args) |args|
|
|
||||||
run_cmd.addArgs(args);
|
|
||||||
|
|
||||||
const run_step = b.step("run", "Run the CLI");
|
|
||||||
run_step.dependOn(&run_cmd.step);
|
|
||||||
|
|
||||||
// Test step
|
|
||||||
const test_module = b.createModule(.{
|
|
||||||
.root_source_file = b.path("src/main.zig"),
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
test_module.addImport("aws", aws_dep.module("aws"));
|
|
||||||
|
|
||||||
const unit_tests = b.addTest(.{
|
|
||||||
.root_module = test_module,
|
|
||||||
});
|
|
||||||
|
|
||||||
const run_unit_tests = b.addRunArtifact(unit_tests);
|
|
||||||
const test_step = b.step("test", "Run unit tests");
|
|
||||||
test_step.dependOn(&run_unit_tests.step);
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
.{
|
|
||||||
.name = .lambda_build,
|
|
||||||
.version = "0.1.0",
|
|
||||||
.fingerprint = 0x6e61de08e7e51114,
|
|
||||||
.dependencies = .{
|
|
||||||
.aws = .{
|
|
||||||
.url = "git+https://git.lerch.org/lobo/aws-sdk-for-zig#5c7aed071f6251d53a1627080a21d604ff58f0a5",
|
|
||||||
.hash = "aws-0.0.1-SbsFcFE7CgDBilPa15i4gIB6Qr5ozBz328O63abDQDDk",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
.paths = .{
|
|
||||||
"build.zig",
|
|
||||||
"build.zig.zon",
|
|
||||||
"src",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
@ -1,591 +0,0 @@
|
||||||
//! Deploy command - deploys a Lambda function to AWS.
|
|
||||||
//!
|
|
||||||
//! Creates a new function or updates an existing one.
|
|
||||||
//! Supports setting environment variables via --env or --env-file.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const aws = @import("aws");
|
|
||||||
const iam_cmd = @import("iam.zig");
|
|
||||||
const RunOptions = @import("main.zig").RunOptions;
|
|
||||||
|
|
||||||
// Get Lambda EnvironmentVariableKeyValue type from AWS SDK
|
|
||||||
const EnvVar = aws.services.lambda.EnvironmentVariableKeyValue;
|
|
||||||
|
|
||||||
pub fn run(args: []const []const u8, options: RunOptions) !void {
|
|
||||||
var function_name: ?[]const u8 = null;
|
|
||||||
var zip_file: ?[]const u8 = null;
|
|
||||||
var role_arn: ?[]const u8 = null;
|
|
||||||
var role_name: []const u8 = "lambda_basic_execution";
|
|
||||||
var arch: ?[]const u8 = null;
|
|
||||||
var allow_principal: ?[]const u8 = null;
|
|
||||||
var deploy_output: ?[]const u8 = null;
|
|
||||||
|
|
||||||
// Environment variables storage
|
|
||||||
var env_vars = std.StringHashMap([]const u8).init(options.allocator);
|
|
||||||
defer {
|
|
||||||
var it = env_vars.iterator();
|
|
||||||
while (it.next()) |entry| {
|
|
||||||
options.allocator.free(entry.key_ptr.*);
|
|
||||||
options.allocator.free(entry.value_ptr.*);
|
|
||||||
}
|
|
||||||
env_vars.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < args.len) : (i += 1) {
|
|
||||||
const arg = args[i];
|
|
||||||
if (std.mem.eql(u8, arg, "--function-name")) {
|
|
||||||
i += 1;
|
|
||||||
if (i >= args.len) return error.MissingFunctionName;
|
|
||||||
function_name = args[i];
|
|
||||||
} else if (std.mem.eql(u8, arg, "--zip-file")) {
|
|
||||||
i += 1;
|
|
||||||
if (i >= args.len) return error.MissingZipFile;
|
|
||||||
zip_file = args[i];
|
|
||||||
} else if (std.mem.eql(u8, arg, "--role-arn")) {
|
|
||||||
i += 1;
|
|
||||||
if (i >= args.len) return error.MissingRoleArn;
|
|
||||||
role_arn = args[i];
|
|
||||||
} else if (std.mem.eql(u8, arg, "--role-name")) {
|
|
||||||
i += 1;
|
|
||||||
if (i >= args.len) return error.MissingRoleName;
|
|
||||||
role_name = args[i];
|
|
||||||
} else if (std.mem.eql(u8, arg, "--arch")) {
|
|
||||||
i += 1;
|
|
||||||
if (i >= args.len) return error.MissingArch;
|
|
||||||
arch = args[i];
|
|
||||||
} else if (std.mem.eql(u8, arg, "--env")) {
|
|
||||||
i += 1;
|
|
||||||
if (i >= args.len) return error.MissingEnvValue;
|
|
||||||
try parseEnvVar(args[i], &env_vars, options.allocator);
|
|
||||||
} else if (std.mem.eql(u8, arg, "--env-file")) {
|
|
||||||
i += 1;
|
|
||||||
if (i >= args.len) return error.MissingEnvFile;
|
|
||||||
try loadEnvFile(args[i], &env_vars, options.allocator);
|
|
||||||
} else if (std.mem.eql(u8, arg, "--allow-principal")) {
|
|
||||||
i += 1;
|
|
||||||
if (i >= args.len) return error.MissingAllowPrincipal;
|
|
||||||
allow_principal = args[i];
|
|
||||||
} else if (std.mem.eql(u8, arg, "--deploy-output")) {
|
|
||||||
i += 1;
|
|
||||||
if (i >= args.len) return error.MissingDeployOutput;
|
|
||||||
deploy_output = args[i];
|
|
||||||
} else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
|
|
||||||
printHelp(options.stdout);
|
|
||||||
try options.stdout.flush();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
try options.stderr.print("Unknown option: {s}\n", .{arg});
|
|
||||||
try options.stderr.flush();
|
|
||||||
return error.UnknownOption;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (function_name == null) {
|
|
||||||
try options.stderr.print("Error: --function-name is required\n", .{});
|
|
||||||
printHelp(options.stderr);
|
|
||||||
try options.stderr.flush();
|
|
||||||
return error.MissingFunctionName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (zip_file == null) {
|
|
||||||
try options.stderr.print("Error: --zip-file is required\n", .{});
|
|
||||||
printHelp(options.stderr);
|
|
||||||
try options.stderr.flush();
|
|
||||||
return error.MissingZipFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
try deployFunction(.{
|
|
||||||
.function_name = function_name.?,
|
|
||||||
.zip_file = zip_file.?,
|
|
||||||
.role_arn = role_arn,
|
|
||||||
.role_name = role_name,
|
|
||||||
.arch = arch,
|
|
||||||
.env_vars = if (env_vars.count() > 0) &env_vars else null,
|
|
||||||
.allow_principal = allow_principal,
|
|
||||||
.deploy_output = deploy_output,
|
|
||||||
}, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse a KEY=VALUE string and add to the env vars map
|
|
||||||
fn parseEnvVar(
|
|
||||||
env_str: []const u8,
|
|
||||||
env_vars: *std.StringHashMap([]const u8),
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
) !void {
|
|
||||||
const eq_pos = std.mem.indexOf(u8, env_str, "=") orelse {
|
|
||||||
return error.InvalidEnvFormat;
|
|
||||||
};
|
|
||||||
|
|
||||||
const key = try allocator.dupe(u8, env_str[0..eq_pos]);
|
|
||||||
errdefer allocator.free(key);
|
|
||||||
const value = try allocator.dupe(u8, env_str[eq_pos + 1 ..]);
|
|
||||||
errdefer allocator.free(value);
|
|
||||||
|
|
||||||
// If key already exists, free the old value
|
|
||||||
if (env_vars.fetchRemove(key)) |old| {
|
|
||||||
allocator.free(old.key);
|
|
||||||
allocator.free(old.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
try env_vars.put(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load environment variables from a file (KEY=VALUE format, one per line)
|
|
||||||
fn loadEnvFile(
|
|
||||||
path: []const u8,
|
|
||||||
env_vars: *std.StringHashMap([]const u8),
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
) !void {
|
|
||||||
const file = std.fs.cwd().openFile(path, .{}) catch |err| {
|
|
||||||
if (err == error.FileNotFound) {
|
|
||||||
std.log.info("Env file '{s}' not found, skipping", .{path});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
std.log.err("Failed to open env file '{s}': {}", .{ path, err });
|
|
||||||
return error.EnvFileOpenError;
|
|
||||||
};
|
|
||||||
defer file.close();
|
|
||||||
|
|
||||||
// Read entire file (env files are typically small)
|
|
||||||
// SAFETY: set on read
|
|
||||||
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 env file: {}", .{err});
|
|
||||||
return error.EnvFileReadError;
|
|
||||||
};
|
|
||||||
defer allocator.free(content);
|
|
||||||
|
|
||||||
// Parse line by line
|
|
||||||
var line_start: usize = 0;
|
|
||||||
for (content, 0..) |c, idx| {
|
|
||||||
if (c == '\n') {
|
|
||||||
const line = content[line_start..idx];
|
|
||||||
line_start = idx + 1;
|
|
||||||
|
|
||||||
// Skip empty lines and comments
|
|
||||||
const trimmed = std.mem.trim(u8, line, " \t\r");
|
|
||||||
if (trimmed.len == 0 or trimmed[0] == '#') continue;
|
|
||||||
|
|
||||||
try parseEnvVar(trimmed, env_vars, allocator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle last line if no trailing newline
|
|
||||||
if (line_start < content.len) {
|
|
||||||
const line = content[line_start..];
|
|
||||||
const trimmed = std.mem.trim(u8, line, " \t\r");
|
|
||||||
if (trimmed.len > 0 and trimmed[0] != '#') {
|
|
||||||
try parseEnvVar(trimmed, env_vars, allocator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn printHelp(writer: anytype) void {
|
|
||||||
writer.print(
|
|
||||||
\\Usage: lambda-build deploy [options]
|
|
||||||
\\
|
|
||||||
\\Deploy a Lambda function to AWS.
|
|
||||||
\\
|
|
||||||
\\Options:
|
|
||||||
\\ --function-name <name> Name of the Lambda function (required)
|
|
||||||
\\ --zip-file <path> Path to the deployment zip (required)
|
|
||||||
\\ --role-arn <arn> IAM role ARN (optional - creates role if omitted)
|
|
||||||
\\ --role-name <name> IAM role name if creating (default: lambda_basic_execution)
|
|
||||||
\\ --arch <arch> Architecture: x86_64 or aarch64 (default: x86_64)
|
|
||||||
\\ --env <KEY=VALUE> Set environment variable (can be repeated)
|
|
||||||
\\ --env-file <path> Load environment variables from file (KEY=VALUE format)
|
|
||||||
\\ --allow-principal <p> Grant invoke permission to AWS service principal
|
|
||||||
\\ (e.g., alexa-appkit.amazon.com)
|
|
||||||
\\ --deploy-output <path> Write deployment info (ARN, region, etc.) to JSON file
|
|
||||||
\\ --help, -h Show this help message
|
|
||||||
\\
|
|
||||||
\\Environment File Format:
|
|
||||||
\\ The --env-file option reads a file with KEY=VALUE pairs, one per line.
|
|
||||||
\\ Lines starting with # are treated as comments. Empty lines are ignored.
|
|
||||||
\\
|
|
||||||
\\ Example .env file:
|
|
||||||
\\ # Database configuration
|
|
||||||
\\ DB_HOST=localhost
|
|
||||||
\\ DB_PORT=5432
|
|
||||||
\\
|
|
||||||
\\If the function exists, its code is updated. Otherwise, a new function
|
|
||||||
\\is created with the provided configuration.
|
|
||||||
\\
|
|
||||||
, .{}) catch {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const DeployOptions = struct {
|
|
||||||
function_name: []const u8,
|
|
||||||
zip_file: []const u8,
|
|
||||||
role_arn: ?[]const u8,
|
|
||||||
role_name: []const u8,
|
|
||||||
arch: ?[]const u8,
|
|
||||||
env_vars: ?*const std.StringHashMap([]const u8),
|
|
||||||
allow_principal: ?[]const u8,
|
|
||||||
deploy_output: ?[]const u8,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
|
|
||||||
// Validate architecture
|
|
||||||
const arch_str = deploy_opts.arch orelse "x86_64";
|
|
||||||
if (!std.mem.eql(u8, arch_str, "x86_64") and !std.mem.eql(u8, arch_str, "aarch64") and !std.mem.eql(u8, arch_str, "arm64")) {
|
|
||||||
return error.InvalidArchitecture;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get or create IAM role if not provided
|
|
||||||
const role_arn = if (deploy_opts.role_arn) |r|
|
|
||||||
try options.allocator.dupe(u8, r)
|
|
||||||
else
|
|
||||||
try iam_cmd.getOrCreateRole(deploy_opts.role_name, options);
|
|
||||||
|
|
||||||
defer options.allocator.free(role_arn);
|
|
||||||
|
|
||||||
// Read the zip file and encode as base64
|
|
||||||
const zip_file = try std.fs.cwd().openFile(deploy_opts.zip_file, .{});
|
|
||||||
defer zip_file.close();
|
|
||||||
// SAFETY: set on read
|
|
||||||
var read_buffer: [4096]u8 = undefined;
|
|
||||||
var file_reader = zip_file.reader(&read_buffer);
|
|
||||||
const zip_data = try file_reader.interface.allocRemaining(options.allocator, std.Io.Limit.limited(50 * 1024 * 1024));
|
|
||||||
defer options.allocator.free(zip_data);
|
|
||||||
|
|
||||||
const base64_data = try std.fmt.allocPrint(options.allocator, "{b64}", .{zip_data});
|
|
||||||
defer options.allocator.free(base64_data);
|
|
||||||
|
|
||||||
const services = aws.Services(.{.lambda}){};
|
|
||||||
|
|
||||||
// Convert arch string to Lambda format
|
|
||||||
const lambda_arch: []const u8 = if (std.mem.eql(u8, arch_str, "aarch64") or std.mem.eql(u8, arch_str, "arm64"))
|
|
||||||
"arm64"
|
|
||||||
else
|
|
||||||
"x86_64";
|
|
||||||
|
|
||||||
// Use a mutable array so the slice type is [][]const u8, not []const []const u8
|
|
||||||
var architectures_arr = [_][]const u8{lambda_arch};
|
|
||||||
const architectures: [][]const u8 = &architectures_arr;
|
|
||||||
|
|
||||||
// Build environment variables for AWS API
|
|
||||||
const env_variables = try buildEnvVariables(deploy_opts.env_vars, options.allocator);
|
|
||||||
defer if (env_variables) |vars| {
|
|
||||||
for (vars) |v| {
|
|
||||||
options.allocator.free(v.key);
|
|
||||||
if (v.value) |val| options.allocator.free(val);
|
|
||||||
}
|
|
||||||
options.allocator.free(vars);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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});
|
|
||||||
|
|
||||||
var create_diagnostics = aws.Diagnostics{
|
|
||||||
// SAFETY: set by sdk on error
|
|
||||||
.response_status = undefined,
|
|
||||||
// SAFETY: set by sdk on error
|
|
||||||
.response_body = undefined,
|
|
||||||
.allocator = options.allocator,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use the shared aws_options but add diagnostics for create call
|
|
||||||
var create_options = options.aws_options;
|
|
||||||
create_options.diagnostics = &create_diagnostics;
|
|
||||||
|
|
||||||
// Track the function ARN from whichever path succeeds
|
|
||||||
var function_arn: ?[]const u8 = null;
|
|
||||||
defer if (function_arn) |arn| options.allocator.free(arn);
|
|
||||||
|
|
||||||
const create_result = aws.Request(services.lambda.create_function).call(.{
|
|
||||||
.function_name = deploy_opts.function_name,
|
|
||||||
.architectures = architectures,
|
|
||||||
.code = .{ .zip_file = base64_data },
|
|
||||||
.handler = "bootstrap",
|
|
||||||
.package_type = "Zip",
|
|
||||||
.runtime = "provided.al2023",
|
|
||||||
.role = role_arn,
|
|
||||||
.environment = if (env_variables) |vars| .{ .variables = vars } else null,
|
|
||||||
}, create_options) catch |err| {
|
|
||||||
defer create_diagnostics.deinit();
|
|
||||||
|
|
||||||
// Function already exists (409 Conflict) - update it instead
|
|
||||||
if (create_diagnostics.response_status == .conflict) {
|
|
||||||
std.log.info("Function already exists, updating: {s}", .{deploy_opts.function_name});
|
|
||||||
|
|
||||||
const update_result = try aws.Request(services.lambda.update_function_code).call(.{
|
|
||||||
.function_name = deploy_opts.function_name,
|
|
||||||
.architectures = architectures,
|
|
||||||
.zip_file = base64_data,
|
|
||||||
}, options.aws_options);
|
|
||||||
defer update_result.deinit();
|
|
||||||
|
|
||||||
try options.stdout.print("Updated function: {s}\n", .{deploy_opts.function_name});
|
|
||||||
if (update_result.response.function_arn) |arn| {
|
|
||||||
try options.stdout.print("ARN: {s}\n", .{arn});
|
|
||||||
function_arn = try options.allocator.dupe(u8, arn);
|
|
||||||
}
|
|
||||||
try options.stdout.flush();
|
|
||||||
|
|
||||||
// Wait for function to be ready before updating configuration
|
|
||||||
try waitForFunctionReady(deploy_opts.function_name, options);
|
|
||||||
|
|
||||||
// Update environment variables if provided
|
|
||||||
if (env_variables) |vars| {
|
|
||||||
try updateFunctionConfiguration(deploy_opts.function_name, vars, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add invoke permission if requested
|
|
||||||
if (deploy_opts.allow_principal) |principal| {
|
|
||||||
try addPermission(deploy_opts.function_name, principal, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write deploy output if requested
|
|
||||||
if (deploy_opts.deploy_output) |output_path| {
|
|
||||||
try writeDeployOutput(output_path, function_arn.?, role_arn, lambda_arch, deploy_opts.env_vars);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
std.log.err(
|
|
||||||
"Lambda CreateFunction failed: {} (HTTP Response code {})",
|
|
||||||
.{ err, create_diagnostics.response_status },
|
|
||||||
);
|
|
||||||
return error.LambdaCreateFunctionFailed;
|
|
||||||
};
|
|
||||||
defer create_result.deinit();
|
|
||||||
|
|
||||||
try options.stdout.print("Created function: {s}\n", .{deploy_opts.function_name});
|
|
||||||
if (create_result.response.function_arn) |arn| {
|
|
||||||
try options.stdout.print("ARN: {s}\n", .{arn});
|
|
||||||
function_arn = try options.allocator.dupe(u8, arn);
|
|
||||||
}
|
|
||||||
try options.stdout.flush();
|
|
||||||
|
|
||||||
// Wait for function to be ready before returning
|
|
||||||
try waitForFunctionReady(deploy_opts.function_name, options);
|
|
||||||
|
|
||||||
// Add invoke permission if requested
|
|
||||||
if (deploy_opts.allow_principal) |principal| {
|
|
||||||
try addPermission(deploy_opts.function_name, principal, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write deploy output if requested
|
|
||||||
if (deploy_opts.deploy_output) |output_path| {
|
|
||||||
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
|
|
||||||
fn buildEnvVariables(
|
|
||||||
env_vars: ?*const std.StringHashMap([]const u8),
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
) !?[]EnvVar {
|
|
||||||
const vars = env_vars orelse return null;
|
|
||||||
if (vars.count() == 0) return null;
|
|
||||||
|
|
||||||
var result = try allocator.alloc(EnvVar, vars.count());
|
|
||||||
errdefer allocator.free(result);
|
|
||||||
|
|
||||||
var idx: usize = 0;
|
|
||||||
var it = vars.iterator();
|
|
||||||
while (it.next()) |entry| {
|
|
||||||
result[idx] = .{
|
|
||||||
.key = try allocator.dupe(u8, entry.key_ptr.*),
|
|
||||||
.value = try allocator.dupe(u8, entry.value_ptr.*),
|
|
||||||
};
|
|
||||||
idx += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update function configuration (environment variables)
|
|
||||||
fn updateFunctionConfiguration(
|
|
||||||
function_name: []const u8,
|
|
||||||
env_variables: []EnvVar,
|
|
||||||
options: RunOptions,
|
|
||||||
) !void {
|
|
||||||
const services = aws.Services(.{.lambda}){};
|
|
||||||
|
|
||||||
std.log.info("Updating function configuration for: {s}", .{function_name});
|
|
||||||
|
|
||||||
const update_config_result = try aws.Request(services.lambda.update_function_configuration).call(.{
|
|
||||||
.function_name = function_name,
|
|
||||||
.environment = .{ .variables = env_variables },
|
|
||||||
}, options.aws_options);
|
|
||||||
defer update_config_result.deinit();
|
|
||||||
|
|
||||||
try options.stdout.print("Updated environment variables\n", .{});
|
|
||||||
try options.stdout.flush();
|
|
||||||
|
|
||||||
// Wait for configuration update to complete
|
|
||||||
try waitForFunctionReady(function_name, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn waitForFunctionReady(function_name: []const u8, options: RunOptions) !void {
|
|
||||||
const services = aws.Services(.{.lambda}){};
|
|
||||||
|
|
||||||
var retries: usize = 30; // Up to ~6 seconds total
|
|
||||||
while (retries > 0) : (retries -= 1) {
|
|
||||||
const result = aws.Request(services.lambda.get_function).call(.{
|
|
||||||
.function_name = function_name,
|
|
||||||
}, options.aws_options) catch |err| {
|
|
||||||
// Function should exist at this point, but retry on transient errors
|
|
||||||
std.log.warn("GetFunction failed during wait: {}", .{err});
|
|
||||||
std.Thread.sleep(200 * std.time.ns_per_ms);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
defer result.deinit();
|
|
||||||
|
|
||||||
// Check if function is ready
|
|
||||||
if (result.response.configuration) |config| {
|
|
||||||
if (config.last_update_status) |status| {
|
|
||||||
if (std.mem.eql(u8, status, "Successful")) {
|
|
||||||
std.log.info("Function is ready", .{});
|
|
||||||
return;
|
|
||||||
} else if (std.mem.eql(u8, status, "Failed")) {
|
|
||||||
return error.FunctionUpdateFailed;
|
|
||||||
}
|
|
||||||
// "InProgress" - keep waiting
|
|
||||||
} else {
|
|
||||||
return; // No status means it's ready
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return; // No configuration means we can't check, assume ready
|
|
||||||
}
|
|
||||||
|
|
||||||
std.Thread.sleep(200 * std.time.ns_per_ms);
|
|
||||||
}
|
|
||||||
|
|
||||||
return error.FunctionNotReady;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add invoke permission for a service principal
|
|
||||||
fn addPermission(
|
|
||||||
function_name: []const u8,
|
|
||||||
principal: []const u8,
|
|
||||||
options: RunOptions,
|
|
||||||
) !void {
|
|
||||||
const services = aws.Services(.{.lambda}){};
|
|
||||||
|
|
||||||
// Generate statement ID from principal: "alexa-appkit.amazon.com" -> "allow-alexa-appkit-amazon-com"
|
|
||||||
// SAFETY: set on write
|
|
||||||
var statement_id_buf: [128]u8 = undefined;
|
|
||||||
var statement_id_len: usize = 0;
|
|
||||||
|
|
||||||
// Add "allow-" prefix
|
|
||||||
const prefix = "allow-";
|
|
||||||
@memcpy(statement_id_buf[0..prefix.len], prefix);
|
|
||||||
statement_id_len = prefix.len;
|
|
||||||
|
|
||||||
// Sanitize principal: replace dots with dashes
|
|
||||||
for (principal) |c| {
|
|
||||||
if (statement_id_len >= statement_id_buf.len - 1) break;
|
|
||||||
statement_id_buf[statement_id_len] = if (c == '.') '-' else c;
|
|
||||||
statement_id_len += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const statement_id = statement_id_buf[0..statement_id_len];
|
|
||||||
|
|
||||||
std.log.info("Adding invoke permission for principal: {s}", .{principal});
|
|
||||||
|
|
||||||
var diagnostics = aws.Diagnostics{
|
|
||||||
// SAFETY: set by sdk on error
|
|
||||||
.response_status = undefined,
|
|
||||||
// SAFETY: set by sdk on error
|
|
||||||
.response_body = undefined,
|
|
||||||
.allocator = options.allocator,
|
|
||||||
};
|
|
||||||
|
|
||||||
var add_perm_options = options.aws_options;
|
|
||||||
add_perm_options.diagnostics = &diagnostics;
|
|
||||||
|
|
||||||
const result = aws.Request(services.lambda.add_permission).call(.{
|
|
||||||
.function_name = function_name,
|
|
||||||
.statement_id = statement_id,
|
|
||||||
.action = "lambda:InvokeFunction",
|
|
||||||
.principal = principal,
|
|
||||||
}, add_perm_options) catch |err| {
|
|
||||||
defer diagnostics.deinit();
|
|
||||||
|
|
||||||
// 409 Conflict means permission already exists - that's fine
|
|
||||||
if (diagnostics.response_status == .conflict) {
|
|
||||||
std.log.info("Permission already exists for: {s}", .{principal});
|
|
||||||
try options.stdout.print("Permission already exists for: {s}\n", .{principal});
|
|
||||||
try options.stdout.flush();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std.log.err(
|
|
||||||
"AddPermission failed: {} (HTTP Response code {})",
|
|
||||||
.{ err, diagnostics.response_status },
|
|
||||||
);
|
|
||||||
return error.AddPermissionFailed;
|
|
||||||
};
|
|
||||||
defer result.deinit();
|
|
||||||
|
|
||||||
try options.stdout.print("Added invoke permission for: {s}\n", .{principal});
|
|
||||||
try options.stdout.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write deployment information to a JSON file
|
|
||||||
fn writeDeployOutput(
|
|
||||||
output_path: []const u8,
|
|
||||||
function_arn: []const u8,
|
|
||||||
role_arn: []const u8,
|
|
||||||
architecture: []const u8,
|
|
||||||
env_vars: ?*const std.StringHashMap([]const u8),
|
|
||||||
) !void {
|
|
||||||
// Parse ARN to extract components
|
|
||||||
// ARN format: arn:{partition}:lambda:{region}:{account_id}:function:{name}
|
|
||||||
var arn_parts = std.mem.splitScalar(u8, function_arn, ':');
|
|
||||||
_ = arn_parts.next(); // arn
|
|
||||||
const partition = arn_parts.next() orelse return error.InvalidArn;
|
|
||||||
_ = arn_parts.next(); // lambda
|
|
||||||
const region = arn_parts.next() orelse return error.InvalidArn;
|
|
||||||
const account_id = arn_parts.next() orelse return error.InvalidArn;
|
|
||||||
_ = arn_parts.next(); // function
|
|
||||||
const function_name = arn_parts.next() orelse return error.InvalidArn;
|
|
||||||
|
|
||||||
const file = try std.fs.cwd().createFile(output_path, .{});
|
|
||||||
defer file.close();
|
|
||||||
|
|
||||||
// SAFETY: set on write
|
|
||||||
var write_buffer: [4096]u8 = undefined;
|
|
||||||
var buffered = file.writer(&write_buffer);
|
|
||||||
const writer = &buffered.interface;
|
|
||||||
|
|
||||||
try writer.print(
|
|
||||||
\\{{
|
|
||||||
\\ "arn": "{s}",
|
|
||||||
\\ "function_name": "{s}",
|
|
||||||
\\ "partition": "{s}",
|
|
||||||
\\ "region": "{s}",
|
|
||||||
\\ "account_id": "{s}",
|
|
||||||
\\ "role_arn": "{s}",
|
|
||||||
\\ "architecture": "{s}",
|
|
||||||
\\ "environment_keys": [
|
|
||||||
, .{ function_arn, function_name, partition, region, account_id, role_arn, architecture });
|
|
||||||
|
|
||||||
// Write environment variable keys
|
|
||||||
if (env_vars) |vars| {
|
|
||||||
var it = vars.keyIterator();
|
|
||||||
var first = true;
|
|
||||||
while (it.next()) |key| {
|
|
||||||
if (!first) {
|
|
||||||
try writer.writeAll(",");
|
|
||||||
}
|
|
||||||
try writer.print("\n \"{s}\"", .{key.*});
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try writer.writeAll(
|
|
||||||
\\
|
|
||||||
\\ ]
|
|
||||||
\\}
|
|
||||||
\\
|
|
||||||
);
|
|
||||||
try writer.flush();
|
|
||||||
|
|
||||||
std.log.info("Wrote deployment info to: {s}", .{output_path});
|
|
||||||
}
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
//! IAM command - creates or retrieves an IAM role for Lambda execution.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const aws = @import("aws");
|
|
||||||
const RunOptions = @import("main.zig").RunOptions;
|
|
||||||
|
|
||||||
pub fn run(args: []const []const u8, options: RunOptions) !void {
|
|
||||||
var role_name: ?[]const u8 = null;
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < args.len) : (i += 1) {
|
|
||||||
const arg = args[i];
|
|
||||||
if (std.mem.eql(u8, arg, "--role-name")) {
|
|
||||||
i += 1;
|
|
||||||
if (i >= args.len) return error.MissingRoleName;
|
|
||||||
role_name = args[i];
|
|
||||||
} else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
|
|
||||||
printHelp(options.stdout);
|
|
||||||
try options.stdout.flush();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
try options.stderr.print("Unknown option: {s}\n", .{arg});
|
|
||||||
try options.stderr.flush();
|
|
||||||
return error.UnknownOption;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role_name == null) {
|
|
||||||
try options.stderr.print("Error: --role-name is required\n", .{});
|
|
||||||
printHelp(options.stderr);
|
|
||||||
try options.stderr.flush();
|
|
||||||
return error.MissingRoleName;
|
|
||||||
}
|
|
||||||
|
|
||||||
const arn = try getOrCreateRole(role_name.?, options);
|
|
||||||
defer options.allocator.free(arn);
|
|
||||||
|
|
||||||
try options.stdout.print("{s}\n", .{arn});
|
|
||||||
try options.stdout.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn printHelp(writer: *std.Io.Writer) void {
|
|
||||||
writer.print(
|
|
||||||
\\Usage: lambda-build iam [options]
|
|
||||||
\\
|
|
||||||
\\Create or retrieve an IAM role for Lambda execution.
|
|
||||||
\\
|
|
||||||
\\Options:
|
|
||||||
\\ --role-name <name> Name of the IAM role (required)
|
|
||||||
\\ --help, -h Show this help message
|
|
||||||
\\
|
|
||||||
\\If the role exists, its ARN is returned. If not, a new role is created
|
|
||||||
\\with the AWSLambdaExecute policy attached.
|
|
||||||
\\
|
|
||||||
, .{}) catch {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get or create an IAM role for Lambda execution
|
|
||||||
/// Returns the role ARN
|
|
||||||
pub fn getOrCreateRole(role_name: []const u8, options: RunOptions) ![]const u8 {
|
|
||||||
const services = aws.Services(.{.iam}){};
|
|
||||||
|
|
||||||
var diagnostics = aws.Diagnostics{
|
|
||||||
// SAFETY: set by sdk on error
|
|
||||||
.response_status = undefined,
|
|
||||||
// SAFETY: set by sdk on error
|
|
||||||
.response_body = undefined,
|
|
||||||
.allocator = options.allocator,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use the shared aws_options but add diagnostics for this call
|
|
||||||
var aws_options = options.aws_options;
|
|
||||||
aws_options.diagnostics = &diagnostics;
|
|
||||||
|
|
||||||
const get_result = aws.Request(services.iam.get_role).call(.{
|
|
||||||
.role_name = role_name,
|
|
||||||
}, aws_options) catch |err| {
|
|
||||||
defer diagnostics.deinit();
|
|
||||||
if (diagnostics.response_status == .not_found) {
|
|
||||||
// Role doesn't exist, create it
|
|
||||||
return try createRole(role_name, options);
|
|
||||||
}
|
|
||||||
std.log.err(
|
|
||||||
"IAM GetRole failed: {} (HTTP Response code {})",
|
|
||||||
.{ err, diagnostics.response_status },
|
|
||||||
);
|
|
||||||
return error.IamGetRoleFailed;
|
|
||||||
};
|
|
||||||
defer get_result.deinit();
|
|
||||||
|
|
||||||
// Role exists, return ARN
|
|
||||||
return try options.allocator.dupe(u8, get_result.response.role.arn);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn createRole(role_name: []const u8, options: RunOptions) ![]const u8 {
|
|
||||||
const services = aws.Services(.{.iam}){};
|
|
||||||
|
|
||||||
const assume_role_policy =
|
|
||||||
\\{
|
|
||||||
\\ "Version": "2012-10-17",
|
|
||||||
\\ "Statement": [
|
|
||||||
\\ {
|
|
||||||
\\ "Sid": "",
|
|
||||||
\\ "Effect": "Allow",
|
|
||||||
\\ "Principal": {
|
|
||||||
\\ "Service": "lambda.amazonaws.com"
|
|
||||||
\\ },
|
|
||||||
\\ "Action": "sts:AssumeRole"
|
|
||||||
\\ }
|
|
||||||
\\ ]
|
|
||||||
\\}
|
|
||||||
;
|
|
||||||
|
|
||||||
std.log.info("Creating IAM role: {s}", .{role_name});
|
|
||||||
|
|
||||||
const create_result = try aws.Request(services.iam.create_role).call(.{
|
|
||||||
.role_name = role_name,
|
|
||||||
.assume_role_policy_document = assume_role_policy,
|
|
||||||
}, options.aws_options);
|
|
||||||
defer create_result.deinit();
|
|
||||||
|
|
||||||
const arn = try options.allocator.dupe(u8, create_result.response.role.arn);
|
|
||||||
|
|
||||||
// Attach the Lambda execution policy
|
|
||||||
std.log.info("Attaching AWSLambdaExecute policy", .{});
|
|
||||||
|
|
||||||
const attach_result = try aws.Request(services.iam.attach_role_policy).call(.{
|
|
||||||
.policy_arn = "arn:aws:iam::aws:policy/AWSLambdaExecute",
|
|
||||||
.role_name = role_name,
|
|
||||||
}, options.aws_options);
|
|
||||||
defer attach_result.deinit();
|
|
||||||
|
|
||||||
// IAM role creation can take a moment to propagate
|
|
||||||
std.log.info("Role created: {s}", .{arn});
|
|
||||||
std.log.info("Note: New roles may take a few seconds to propagate", .{});
|
|
||||||
|
|
||||||
return arn;
|
|
||||||
}
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
//! Invoke command - invokes a Lambda function.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const aws = @import("aws");
|
|
||||||
const RunOptions = @import("main.zig").RunOptions;
|
|
||||||
|
|
||||||
pub fn run(args: []const []const u8, options: RunOptions) !void {
|
|
||||||
var function_name: ?[]const u8 = null;
|
|
||||||
var payload: []const u8 = "{}";
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < args.len) : (i += 1) {
|
|
||||||
const arg = args[i];
|
|
||||||
if (std.mem.eql(u8, arg, "--function-name")) {
|
|
||||||
i += 1;
|
|
||||||
if (i >= args.len) return error.MissingFunctionName;
|
|
||||||
function_name = args[i];
|
|
||||||
} else if (std.mem.eql(u8, arg, "--payload")) {
|
|
||||||
i += 1;
|
|
||||||
if (i >= args.len) return error.MissingPayload;
|
|
||||||
payload = args[i];
|
|
||||||
} else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
|
|
||||||
printHelp(options.stdout);
|
|
||||||
try options.stdout.flush();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
try options.stderr.print("Unknown option: {s}\n", .{arg});
|
|
||||||
try options.stderr.flush();
|
|
||||||
return error.UnknownOption;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (function_name == null) {
|
|
||||||
try options.stderr.print("Error: --function-name is required\n", .{});
|
|
||||||
printHelp(options.stderr);
|
|
||||||
try options.stderr.flush();
|
|
||||||
return error.MissingFunctionName;
|
|
||||||
}
|
|
||||||
|
|
||||||
try invokeFunction(function_name.?, payload, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn printHelp(writer: *std.Io.Writer) void {
|
|
||||||
writer.print(
|
|
||||||
\\Usage: lambda-build invoke [options]
|
|
||||||
\\
|
|
||||||
\\Invoke a Lambda function.
|
|
||||||
\\
|
|
||||||
\\Options:
|
|
||||||
\\ --function-name <name> Name of the Lambda function (required)
|
|
||||||
\\ --payload <json> JSON payload to send (default: empty object)
|
|
||||||
\\ --help, -h Show this help message
|
|
||||||
\\
|
|
||||||
\\The function response is printed to stdout.
|
|
||||||
\\
|
|
||||||
, .{}) catch {};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn invokeFunction(function_name: []const u8, payload: []const u8, options: RunOptions) !void {
|
|
||||||
const services = aws.Services(.{.lambda}){};
|
|
||||||
|
|
||||||
std.log.info("Invoking function: {s}", .{function_name});
|
|
||||||
|
|
||||||
const result = try aws.Request(services.lambda.invoke).call(.{
|
|
||||||
.function_name = function_name,
|
|
||||||
.payload = payload,
|
|
||||||
.log_type = "Tail",
|
|
||||||
.invocation_type = "RequestResponse",
|
|
||||||
}, options.aws_options);
|
|
||||||
defer result.deinit();
|
|
||||||
|
|
||||||
// Print response payload
|
|
||||||
if (result.response.payload) |response_payload| {
|
|
||||||
try options.stdout.print("{s}\n", .{response_payload});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print function error if any
|
|
||||||
if (result.response.function_error) |func_error| {
|
|
||||||
try options.stdout.print("Function error: {s}\n", .{func_error});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print logs if available (base64 decoded)
|
|
||||||
if (result.response.log_result) |log_result| {
|
|
||||||
const decoder = std.base64.standard.Decoder;
|
|
||||||
const decoded_len = try decoder.calcSizeForSlice(log_result);
|
|
||||||
const decoded = try options.allocator.alloc(u8, decoded_len);
|
|
||||||
defer options.allocator.free(decoded);
|
|
||||||
try decoder.decode(decoded, log_result);
|
|
||||||
try options.stdout.print("\n--- Logs ---\n{s}\n", .{decoded});
|
|
||||||
}
|
|
||||||
|
|
||||||
try options.stdout.flush();
|
|
||||||
}
|
|
||||||
|
|
@ -1,155 +0,0 @@
|
||||||
//! Lambda Build CLI
|
|
||||||
//!
|
|
||||||
//! A command-line tool for packaging, deploying, and invoking AWS Lambda functions.
|
|
||||||
//!
|
|
||||||
//! Usage: lambda-build <command> [options]
|
|
||||||
//!
|
|
||||||
//! Commands:
|
|
||||||
//! package Create deployment zip from executable
|
|
||||||
//! iam Create/verify IAM role for Lambda
|
|
||||||
//! deploy Deploy function to AWS Lambda
|
|
||||||
//! invoke Invoke the deployed function
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const aws = @import("aws");
|
|
||||||
const package = @import("package.zig");
|
|
||||||
const iam_cmd = @import("iam.zig");
|
|
||||||
const deploy_cmd = @import("deploy.zig");
|
|
||||||
const invoke_cmd = @import("invoke.zig");
|
|
||||||
|
|
||||||
/// Options passed to all commands
|
|
||||||
pub const RunOptions = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
stdout: *std.Io.Writer,
|
|
||||||
stderr: *std.Io.Writer,
|
|
||||||
region: []const u8,
|
|
||||||
aws_options: aws.Options,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn main() !u8 {
|
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
||||||
defer _ = gpa.deinit();
|
|
||||||
const allocator = gpa.allocator();
|
|
||||||
|
|
||||||
var stdout_buffer: [4096]u8 = undefined;
|
|
||||||
var stderr_buffer: [4096]u8 = undefined;
|
|
||||||
var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
|
|
||||||
var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer);
|
|
||||||
|
|
||||||
run(allocator, &stdout_writer.interface, &stderr_writer.interface) catch |err| {
|
|
||||||
stderr_writer.interface.print("Error: {}\n", .{err}) catch {};
|
|
||||||
try stderr_writer.interface.flush();
|
|
||||||
return 1;
|
|
||||||
};
|
|
||||||
try stderr_writer.interface.flush();
|
|
||||||
try stdout_writer.interface.flush();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(allocator: std.mem.Allocator, stdout: *std.Io.Writer, stderr: *std.Io.Writer) !void {
|
|
||||||
const args = try std.process.argsAlloc(allocator);
|
|
||||||
defer std.process.argsFree(allocator, args);
|
|
||||||
|
|
||||||
if (args.len < 2) {
|
|
||||||
printUsage(stderr);
|
|
||||||
try stderr.flush();
|
|
||||||
return error.MissingCommand;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse global options and find command
|
|
||||||
var cmd_start: usize = 1;
|
|
||||||
var region: []const u8 = "us-east-1";
|
|
||||||
var profile: ?[]const u8 = null;
|
|
||||||
|
|
||||||
while (cmd_start < args.len) {
|
|
||||||
const arg = args[cmd_start];
|
|
||||||
if (std.mem.eql(u8, arg, "--region")) {
|
|
||||||
cmd_start += 1;
|
|
||||||
if (cmd_start >= args.len) return error.MissingRegionValue;
|
|
||||||
region = args[cmd_start];
|
|
||||||
cmd_start += 1;
|
|
||||||
} else if (std.mem.eql(u8, arg, "--profile")) {
|
|
||||||
cmd_start += 1;
|
|
||||||
if (cmd_start >= args.len) return error.MissingProfileValue;
|
|
||||||
profile = args[cmd_start];
|
|
||||||
cmd_start += 1;
|
|
||||||
} else if (std.mem.startsWith(u8, arg, "--")) {
|
|
||||||
// Unknown global option - might be command-specific, let command handle it
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
// Found command
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmd_start >= args.len) {
|
|
||||||
printUsage(stderr);
|
|
||||||
try stderr.flush();
|
|
||||||
return error.MissingCommand;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create AWS client and options once, used by all commands
|
|
||||||
var client = aws.Client.init(allocator, .{});
|
|
||||||
defer client.deinit();
|
|
||||||
|
|
||||||
const aws_options = aws.Options{
|
|
||||||
.client = client,
|
|
||||||
.region = region,
|
|
||||||
.credential_options = .{
|
|
||||||
.profile = .{
|
|
||||||
.profile_name = profile,
|
|
||||||
.prefer_profile_from_file = profile != null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = RunOptions{
|
|
||||||
.allocator = allocator,
|
|
||||||
.stdout = stdout,
|
|
||||||
.stderr = stderr,
|
|
||||||
.region = region,
|
|
||||||
.aws_options = aws_options,
|
|
||||||
};
|
|
||||||
|
|
||||||
const command = args[cmd_start];
|
|
||||||
const cmd_args = args[cmd_start + 1 ..];
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, command, "package")) {
|
|
||||||
try package.run(cmd_args, options);
|
|
||||||
} else if (std.mem.eql(u8, command, "iam")) {
|
|
||||||
try iam_cmd.run(cmd_args, options);
|
|
||||||
} else if (std.mem.eql(u8, command, "deploy")) {
|
|
||||||
try deploy_cmd.run(cmd_args, options);
|
|
||||||
} else if (std.mem.eql(u8, command, "invoke")) {
|
|
||||||
try invoke_cmd.run(cmd_args, options);
|
|
||||||
} else if (std.mem.eql(u8, command, "--help") or std.mem.eql(u8, command, "-h")) {
|
|
||||||
printUsage(stdout);
|
|
||||||
try stdout.flush();
|
|
||||||
} else {
|
|
||||||
stderr.print("Unknown command: {s}\n\n", .{command}) catch {};
|
|
||||||
printUsage(stderr);
|
|
||||||
try stderr.flush();
|
|
||||||
return error.UnknownCommand;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn printUsage(writer: *std.Io.Writer) void {
|
|
||||||
writer.print(
|
|
||||||
\\Usage: lambda-build [global-options] <command> [options]
|
|
||||||
\\
|
|
||||||
\\Lambda deployment CLI tool
|
|
||||||
\\
|
|
||||||
\\Global Options:
|
|
||||||
\\ --region <region> AWS region (default: us-east-1)
|
|
||||||
\\ --profile <profile> AWS profile to use
|
|
||||||
\\
|
|
||||||
\\Commands:
|
|
||||||
\\ package Create deployment zip from executable
|
|
||||||
\\ iam Create/verify IAM role for Lambda
|
|
||||||
\\ deploy Deploy function to AWS Lambda
|
|
||||||
\\ invoke Invoke the deployed function
|
|
||||||
\\
|
|
||||||
\\Run 'lambda-build <command> --help' for command-specific options.
|
|
||||||
\\
|
|
||||||
, .{}) catch {};
|
|
||||||
}
|
|
||||||
|
|
@ -1,265 +0,0 @@
|
||||||
//! Package command - creates a Lambda deployment zip from an executable.
|
|
||||||
//!
|
|
||||||
//! The zip file contains a single file named "bootstrap" (Lambda's expected name
|
|
||||||
//! for custom runtime executables).
|
|
||||||
//!
|
|
||||||
//! Note: Uses "store" (uncompressed) format because Zig 0.15's std.compress.flate.Compress
|
|
||||||
//! has incomplete implementation (drain function panics with TODO). When the compression
|
|
||||||
//! implementation is completed, this should use deflate level 6.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const zip = std.zip;
|
|
||||||
const RunOptions = @import("main.zig").RunOptions;
|
|
||||||
|
|
||||||
pub fn run(args: []const []const u8, options: RunOptions) !void {
|
|
||||||
var exe_path: ?[]const u8 = null;
|
|
||||||
var output_path: ?[]const u8 = null;
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < args.len) : (i += 1) {
|
|
||||||
const arg = args[i];
|
|
||||||
if (std.mem.eql(u8, arg, "--exe")) {
|
|
||||||
i += 1;
|
|
||||||
if (i >= args.len) return error.MissingExePath;
|
|
||||||
exe_path = args[i];
|
|
||||||
} else if (std.mem.eql(u8, arg, "--output") or std.mem.eql(u8, arg, "-o")) {
|
|
||||||
i += 1;
|
|
||||||
if (i >= args.len) return error.MissingOutputPath;
|
|
||||||
output_path = args[i];
|
|
||||||
} else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
|
|
||||||
printHelp(options.stdout);
|
|
||||||
try options.stdout.flush();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
try options.stderr.print("Unknown option: {s}\n", .{arg});
|
|
||||||
try options.stderr.flush();
|
|
||||||
return error.UnknownOption;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exe_path == null) {
|
|
||||||
try options.stderr.print("Error: --exe is required\n", .{});
|
|
||||||
printHelp(options.stderr);
|
|
||||||
try options.stderr.flush();
|
|
||||||
return error.MissingExePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (output_path == null) {
|
|
||||||
try options.stderr.print("Error: --output is required\n", .{});
|
|
||||||
printHelp(options.stderr);
|
|
||||||
try options.stderr.flush();
|
|
||||||
return error.MissingOutputPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
try createLambdaZip(options.allocator, exe_path.?, output_path.?);
|
|
||||||
|
|
||||||
try options.stdout.print("Created {s}\n", .{output_path.?});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn printHelp(writer: *std.Io.Writer) void {
|
|
||||||
writer.print(
|
|
||||||
\\Usage: lambda-build package [options]
|
|
||||||
\\
|
|
||||||
\\Create a Lambda deployment zip from an executable.
|
|
||||||
\\
|
|
||||||
\\Options:
|
|
||||||
\\ --exe <path> Path to the executable (required)
|
|
||||||
\\ --output, -o <path> Output zip file path (required)
|
|
||||||
\\ --help, -h Show this help message
|
|
||||||
\\
|
|
||||||
\\The executable will be packaged as 'bootstrap' in the zip file,
|
|
||||||
\\which is the expected name for Lambda custom runtimes.
|
|
||||||
\\
|
|
||||||
, .{}) catch {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper to write a little-endian u16
|
|
||||||
fn writeU16LE(file: std.fs.File, value: u16) !void {
|
|
||||||
const bytes = std.mem.toBytes(std.mem.nativeToLittle(u16, value));
|
|
||||||
try file.writeAll(&bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper to write a little-endian u32
|
|
||||||
fn writeU32LE(file: std.fs.File, value: u32) !void {
|
|
||||||
const bytes = std.mem.toBytes(std.mem.nativeToLittle(u32, value));
|
|
||||||
try file.writeAll(&bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a Lambda deployment zip file containing a single "bootstrap" executable.
|
|
||||||
/// Currently uses "store" (uncompressed) format because Zig 0.15's std.compress.flate.Compress
|
|
||||||
/// has incomplete implementation.
|
|
||||||
/// TODO: Add deflate compression (level 6) when the Compress implementation is completed.
|
|
||||||
fn createLambdaZip(allocator: std.mem.Allocator, exe_path: []const u8, output_path: []const u8) !void {
|
|
||||||
// Read the executable
|
|
||||||
const exe_file = try std.fs.cwd().openFile(exe_path, .{});
|
|
||||||
defer exe_file.close();
|
|
||||||
|
|
||||||
const exe_stat = try exe_file.stat();
|
|
||||||
const exe_size: u32 = @intCast(exe_stat.size);
|
|
||||||
|
|
||||||
// Allocate buffer and read file contents
|
|
||||||
const exe_data = try allocator.alloc(u8, exe_size);
|
|
||||||
defer allocator.free(exe_data);
|
|
||||||
const bytes_read = try exe_file.readAll(exe_data);
|
|
||||||
if (bytes_read != exe_size) return error.IncompleteRead;
|
|
||||||
|
|
||||||
// Calculate CRC32 of uncompressed data
|
|
||||||
const crc = std.hash.crc.Crc32IsoHdlc.hash(exe_data);
|
|
||||||
|
|
||||||
// Create the output file
|
|
||||||
const out_file = try std.fs.cwd().createFile(output_path, .{});
|
|
||||||
defer out_file.close();
|
|
||||||
|
|
||||||
const filename = "bootstrap";
|
|
||||||
const filename_len: u16 = @intCast(filename.len);
|
|
||||||
|
|
||||||
// Reproducible zip files: use fixed timestamp
|
|
||||||
// September 26, 1995 at midnight (00:00:00)
|
|
||||||
// DOS time format: bits 0-4: seconds/2, bits 5-10: minute, bits 11-15: hour
|
|
||||||
// DOS date format: bits 0-4: day, bits 5-8: month, bits 9-15: year-1980
|
|
||||||
//
|
|
||||||
// Note: We use a fixed timestamp for reproducible builds.
|
|
||||||
//
|
|
||||||
// If current time is needed in the future:
|
|
||||||
// const now = std.time.timestamp();
|
|
||||||
// const epoch_secs: std.time.epoch.EpochSeconds = .{ .secs = @intCast(now) };
|
|
||||||
// const day_secs = epoch_secs.getDaySeconds();
|
|
||||||
// const year_day = epoch_secs.getEpochDay().calculateYearDay();
|
|
||||||
// const mod_time: u16 = @as(u16, day_secs.getHoursIntoDay()) << 11 |
|
|
||||||
// @as(u16, day_secs.getMinutesIntoHour()) << 5 |
|
|
||||||
// @as(u16, day_secs.getSecondsIntoMinute() / 2);
|
|
||||||
// const month_day = year_day.calculateMonthDay();
|
|
||||||
// const mod_date: u16 = @as(u16, year_day.year -% 1980) << 9 |
|
|
||||||
// @as(u16, @intFromEnum(month_day.month)) << 5 |
|
|
||||||
// @as(u16, month_day.day_index + 1);
|
|
||||||
|
|
||||||
// 1995-09-26 midnight for reproducible builds
|
|
||||||
const mod_time: u16 = 0x0000; // 00:00:00
|
|
||||||
const mod_date: u16 = (15 << 9) | (9 << 5) | 26; // 1995-09-26 (year 15 = 1995-1980)
|
|
||||||
|
|
||||||
// Local file header
|
|
||||||
try out_file.writeAll(&zip.local_file_header_sig);
|
|
||||||
try writeU16LE(out_file, 10); // version needed (1.0 for store)
|
|
||||||
try writeU16LE(out_file, 0); // general purpose flags
|
|
||||||
try writeU16LE(out_file, @intFromEnum(zip.CompressionMethod.store)); // store (no compression)
|
|
||||||
try writeU16LE(out_file, mod_time);
|
|
||||||
try writeU16LE(out_file, mod_date);
|
|
||||||
try writeU32LE(out_file, crc);
|
|
||||||
try writeU32LE(out_file, exe_size); // compressed size = uncompressed for store
|
|
||||||
try writeU32LE(out_file, exe_size); // uncompressed size
|
|
||||||
try writeU16LE(out_file, filename_len);
|
|
||||||
try writeU16LE(out_file, 0); // extra field length
|
|
||||||
try out_file.writeAll(filename);
|
|
||||||
|
|
||||||
// File data (uncompressed)
|
|
||||||
const local_header_end = 30 + filename_len;
|
|
||||||
try out_file.writeAll(exe_data);
|
|
||||||
|
|
||||||
// Central directory file header
|
|
||||||
const cd_offset = local_header_end + exe_size;
|
|
||||||
try out_file.writeAll(&zip.central_file_header_sig);
|
|
||||||
try writeU16LE(out_file, 0x031e); // version made by (Unix, 3.0)
|
|
||||||
try writeU16LE(out_file, 10); // version needed (1.0 for store)
|
|
||||||
try writeU16LE(out_file, 0); // general purpose flags
|
|
||||||
try writeU16LE(out_file, @intFromEnum(zip.CompressionMethod.store)); // store
|
|
||||||
try writeU16LE(out_file, mod_time);
|
|
||||||
try writeU16LE(out_file, mod_date);
|
|
||||||
try writeU32LE(out_file, crc);
|
|
||||||
try writeU32LE(out_file, exe_size); // compressed size
|
|
||||||
try writeU32LE(out_file, exe_size); // uncompressed size
|
|
||||||
try writeU16LE(out_file, filename_len);
|
|
||||||
try writeU16LE(out_file, 0); // extra field length
|
|
||||||
try writeU16LE(out_file, 0); // file comment length
|
|
||||||
try writeU16LE(out_file, 0); // disk number start
|
|
||||||
try writeU16LE(out_file, 0); // internal file attributes
|
|
||||||
try writeU32LE(out_file, 0o100755 << 16); // external file attributes (Unix executable)
|
|
||||||
try writeU32LE(out_file, 0); // relative offset of local header
|
|
||||||
|
|
||||||
try out_file.writeAll(filename);
|
|
||||||
|
|
||||||
// End of central directory record
|
|
||||||
const cd_size: u32 = 46 + filename_len;
|
|
||||||
try out_file.writeAll(&zip.end_record_sig);
|
|
||||||
try writeU16LE(out_file, 0); // disk number
|
|
||||||
try writeU16LE(out_file, 0); // disk number with CD
|
|
||||||
try writeU16LE(out_file, 1); // number of entries on disk
|
|
||||||
try writeU16LE(out_file, 1); // total number of entries
|
|
||||||
try writeU32LE(out_file, cd_size); // size of central directory
|
|
||||||
try writeU32LE(out_file, cd_offset); // offset of central directory
|
|
||||||
try writeU16LE(out_file, 0); // comment length
|
|
||||||
}
|
|
||||||
|
|
||||||
test "create zip with test data" {
|
|
||||||
const allocator = std.testing.allocator;
|
|
||||||
|
|
||||||
// Create a temporary test file
|
|
||||||
var tmp_dir = std.testing.tmpDir(.{});
|
|
||||||
defer tmp_dir.cleanup();
|
|
||||||
|
|
||||||
const test_content = "#!/bin/sh\necho hello";
|
|
||||||
const test_exe = try tmp_dir.dir.createFile("test_exe", .{});
|
|
||||||
try test_exe.writeAll(test_content);
|
|
||||||
test_exe.close();
|
|
||||||
|
|
||||||
const exe_path = try tmp_dir.dir.realpathAlloc(allocator, "test_exe");
|
|
||||||
defer allocator.free(exe_path);
|
|
||||||
|
|
||||||
const output_path = try tmp_dir.dir.realpathAlloc(allocator, ".");
|
|
||||||
defer allocator.free(output_path);
|
|
||||||
|
|
||||||
const full_output = try std.fs.path.join(allocator, &.{ output_path, "test.zip" });
|
|
||||||
defer allocator.free(full_output);
|
|
||||||
|
|
||||||
try createLambdaZip(allocator, exe_path, full_output);
|
|
||||||
|
|
||||||
// Verify the zip file can be read by std.zip
|
|
||||||
const zip_file = try std.fs.cwd().openFile(full_output, .{});
|
|
||||||
defer zip_file.close();
|
|
||||||
|
|
||||||
var read_buffer: [4096]u8 = undefined;
|
|
||||||
var file_reader = zip_file.reader(&read_buffer);
|
|
||||||
|
|
||||||
var iter = try zip.Iterator.init(&file_reader);
|
|
||||||
|
|
||||||
// Should have exactly one entry
|
|
||||||
const entry = try iter.next();
|
|
||||||
try std.testing.expect(entry != null);
|
|
||||||
|
|
||||||
const e = entry.?;
|
|
||||||
|
|
||||||
// Verify filename length is 9 ("bootstrap")
|
|
||||||
try std.testing.expectEqual(@as(u32, 9), e.filename_len);
|
|
||||||
|
|
||||||
// Verify compression method is store
|
|
||||||
try std.testing.expectEqual(zip.CompressionMethod.store, e.compression_method);
|
|
||||||
|
|
||||||
// Verify sizes match test content
|
|
||||||
try std.testing.expectEqual(@as(u64, test_content.len), e.uncompressed_size);
|
|
||||||
try std.testing.expectEqual(@as(u64, test_content.len), e.compressed_size);
|
|
||||||
|
|
||||||
// Verify CRC32 matches
|
|
||||||
const expected_crc = std.hash.crc.Crc32IsoHdlc.hash(test_content);
|
|
||||||
try std.testing.expectEqual(expected_crc, e.crc32);
|
|
||||||
|
|
||||||
// Verify no more entries
|
|
||||||
const next_entry = try iter.next();
|
|
||||||
try std.testing.expect(next_entry == null);
|
|
||||||
|
|
||||||
// Extract and verify contents
|
|
||||||
var extract_dir = std.testing.tmpDir(.{});
|
|
||||||
defer extract_dir.cleanup();
|
|
||||||
|
|
||||||
// Reset file reader position
|
|
||||||
try file_reader.seekTo(0);
|
|
||||||
|
|
||||||
var filename_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
||||||
try e.extract(&file_reader, .{}, &filename_buf, extract_dir.dir);
|
|
||||||
|
|
||||||
// Read extracted file and verify contents
|
|
||||||
const extracted = try extract_dir.dir.openFile("bootstrap", .{});
|
|
||||||
defer extracted.close();
|
|
||||||
|
|
||||||
var extracted_content: [1024]u8 = undefined;
|
|
||||||
const bytes_read = try extracted.readAll(&extracted_content);
|
|
||||||
try std.testing.expectEqualStrings(test_content, extracted_content[0..bytes_read]);
|
|
||||||
}
|
|
||||||
Loading…
Add table
Reference in a new issue