out with AWS cli/in with the SDK
Some checks failed
Generic zig build / build (push) Failing after 44s
Some checks failed
Generic zig build / build (push) Failing after 44s
This commit is contained in:
parent
ef5b793882
commit
f86bafc533
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
zig-cache/
|
||||
zig-out/
|
||||
.zig-cache
|
||||
|
|
38
README.md
38
README.md
|
@ -1,37 +1,37 @@
|
|||
lambda-zig: A Custom Runtime for AWS Lambda
|
||||
===========================================
|
||||
|
||||
This is a sample custom runtime built in zig (0.12). Simple projects will execute
|
||||
This is a sample custom runtime built in zig (0.13). Simple projects will execute
|
||||
in <1ms, with a cold start init time of approximately 11ms.
|
||||
|
||||
Some custom build steps have been added to build.zig, which will only currently appear if compiling from a linux operating system:
|
||||
|
||||
* `zig build iam`: Deploy and record a default IAM role for the lambda function
|
||||
* `zig build package`: Package the lambda function for upload
|
||||
* `zig build deploy`: Deploy the lambda function
|
||||
* `zig build remoterun`: Run the lambda function
|
||||
* `zig build awslambda_iam`: Deploy and record a default IAM role for the lambda function
|
||||
* `zig build awslambda_package`: Package the lambda function for upload
|
||||
* `zig build awslambda_deploy`: Deploy the lambda function
|
||||
* `zig build awslambda_run`: Run the lambda function
|
||||
|
||||
Custom options:
|
||||
|
||||
* **function-name**: set the name of the AWS Lambda function
|
||||
* **payload**: Use this to set the payload of the function when run using `zig build remoterun`
|
||||
* **payload**: Use this to set the payload of the function when run using `zig build awslambda_run`
|
||||
* **region**: Use this to set the region for the function deployment/run
|
||||
* **function-role**: Name of the role to use for the function. The system will
|
||||
look up the arn from this name, and create if it does not exist
|
||||
* **function-arn**: Role arn to use with the function. This must exist
|
||||
|
||||
Additionally, a custom IAM role can be used for the function by appending ``-- --role myawesomerole``
|
||||
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.
|
||||
|
||||
The AWS Lambda function is compiled as a linux ARM64 executable. Since the build.zig
|
||||
calls out to the shell for AWS operations, you will need the AWS CLI. v2.2.43 has been tested.
|
||||
The AWS Lambda function can be compiled as a linux x86_64 or linux aarch64
|
||||
executable. The build script will set the architecture appropriately
|
||||
|
||||
Caveats:
|
||||
|
||||
* Unhandled invocation errors seem to be causing timeouts
|
||||
* zig build options only appear if compiling using linux, although it should be trivial
|
||||
to make it work on other Unix-like operating systems (e.g. macos, freebsd). In fact,
|
||||
it will likely work with just a change to the operating system check
|
||||
* There are a **ton** of TODO's in this code. Current state is more of a proof of
|
||||
concept. PRs are welcome!
|
||||
* Building on Windows will not yet work, as the package step still uses
|
||||
system commands due to the need to create a zip file, and the current lack
|
||||
of zip file creation capabilities in the standard library (you can read, but
|
||||
not write, zip files with the standard library). A TODO exists with more
|
||||
information should you wish to file a PR.
|
||||
* Caching is not yet implemented in the package or deployment steps, so the
|
||||
function will be deployed on every build
|
||||
|
||||
A sample project using this runtime can be found at https://git.lerch.org/lobo/lambda-zig-sample
|
||||
|
||||
|
|
|
@ -7,6 +7,12 @@
|
|||
// This field is optional.
|
||||
// This is currently advisory only; Zig does not yet do anything
|
||||
// with this value.
|
||||
.dependencies = .{
|
||||
.aws = .{
|
||||
.url = "https://git.lerch.org/api/packages/lobo/generic/aws-sdk-with-models/908c9d2d429b1f38c835363db566aa17bf1742fd/908c9d2d429b1f38c835363db566aa17bf1742fd-with-models.tar.gz",
|
||||
.hash = "122022770a177afb2ee46632f88ad5468a5dea8df22170d1dea5163890b0a881399d",
|
||||
},
|
||||
},
|
||||
.minimum_zig_version = "0.12.0",
|
||||
|
||||
// Specifies the set of files and directories that are included in this package.
|
||||
|
@ -20,6 +26,7 @@
|
|||
"build.zig.zon",
|
||||
"lambdabuild.zig",
|
||||
"src",
|
||||
"lambdabuild",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
},
|
||||
|
|
233
lambdabuild.zig
233
lambdabuild.zig
|
@ -1,5 +1,9 @@
|
|||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Package = @import("lambdabuild/Package.zig");
|
||||
const Iam = @import("lambdabuild/Iam.zig");
|
||||
const Deploy = @import("lambdabuild/Deploy.zig");
|
||||
const Invoke = @import("lambdabuild/Invoke.zig");
|
||||
|
||||
fn fileExists(file_name: []const u8) bool {
|
||||
const file = std.fs.openFileAbsolute(file_name, .{}) catch return false;
|
||||
|
@ -40,159 +44,122 @@ pub fn configureBuild(b: *std.Build, exe: *std.Build.Step.Compile, function_name
|
|||
// TODO: support other host OSs
|
||||
if (builtin.os.tag != .linux) return;
|
||||
|
||||
// Package step
|
||||
const package_step = b.step("awslambda_package", "Package the function");
|
||||
const function_zip = b.getInstallPath(.bin, "function.zip");
|
||||
@import("aws").aws.globalLogControl(.info, .warn, .info, false);
|
||||
const package_step = Package.create(b, .{ .exe = exe });
|
||||
|
||||
// TODO: Avoid use of system-installed zip, maybe using something like
|
||||
// https://github.com/hdorio/hwzip.zig/blob/master/src/hwzip.zig
|
||||
const zip = if (std.mem.eql(u8, "bootstrap", exe.out_filename))
|
||||
try std.fmt.allocPrint(b.allocator,
|
||||
\\zip -qj9 {s} {s}
|
||||
, .{
|
||||
function_zip,
|
||||
b.getInstallPath(.bin, "bootstrap"),
|
||||
})
|
||||
else
|
||||
// We need to copy stuff around
|
||||
try std.fmt.allocPrint(b.allocator,
|
||||
\\cp {s} {s} && \
|
||||
\\zip -qj9 {s} {s} && \
|
||||
\\rm {s}
|
||||
, .{
|
||||
b.getInstallPath(.bin, exe.out_filename),
|
||||
b.getInstallPath(.bin, "bootstrap"),
|
||||
function_zip,
|
||||
b.getInstallPath(.bin, "bootstrap"),
|
||||
b.getInstallPath(.bin, "bootstrap"),
|
||||
});
|
||||
// std.debug.print("\nzip cmdline: {s}", .{zip});
|
||||
defer b.allocator.free(zip);
|
||||
var zip_cmd = b.addSystemCommand(&.{ "/bin/sh", "-c", zip });
|
||||
zip_cmd.step.dependOn(b.getInstallStep());
|
||||
package_step.dependOn(&zip_cmd.step);
|
||||
const step = b.step("awslambda_package", "Package the function");
|
||||
step.dependOn(&package_step.step);
|
||||
package_step.step.dependOn(b.getInstallStep());
|
||||
|
||||
// Deployment
|
||||
const deploy_step = b.step("awslambda_deploy", "Deploy the function");
|
||||
// Doing this will require that the aws dependency be added to the downstream
|
||||
// build.zig.zon
|
||||
// const lambdabuild = b.addExecutable(.{
|
||||
// .name = "lambdabuild",
|
||||
// .root_source_file = .{
|
||||
// // we use cwd_relative here because we need to compile this relative
|
||||
// // to whatever directory this file happens to be. That is likely
|
||||
// // in a cache directory, not the base of the build.
|
||||
// .cwd_relative = try std.fs.path.join(b.allocator, &[_][]const u8{
|
||||
// std.fs.path.dirname(@src().file).?,
|
||||
// "lambdabuild/src/main.zig",
|
||||
// }),
|
||||
// },
|
||||
// .target = b.host,
|
||||
// });
|
||||
// const aws_dep = b.dependency("aws", .{
|
||||
// .target = b.host,
|
||||
// .optimize = lambdabuild.root_module.optimize orelse .Debug,
|
||||
// });
|
||||
// const aws_module = aws_dep.module("aws");
|
||||
// lambdabuild.root_module.addImport("aws", aws_module);
|
||||
//
|
||||
|
||||
const iam_role_name = b.option(
|
||||
[]const u8,
|
||||
"function-role",
|
||||
"IAM role name for function (will create if it does not exist) [lambda_basic_execution]",
|
||||
) orelse "lambda_basic_execution";
|
||||
) orelse "lambda_basic_execution_blah2";
|
||||
|
||||
const iam_role_arn = b.option(
|
||||
[]const u8,
|
||||
"function-arn",
|
||||
"Preexisting IAM role arn for function",
|
||||
);
|
||||
|
||||
const iam_step = b.step("awslambda_iam", "Create/Get IAM role for function");
|
||||
deploy_step.dependOn(iam_step); // iam_step will either be a noop or all the stuff below
|
||||
const iam_role_param: []u8 = blk: {
|
||||
if (iam_role_arn != null)
|
||||
break :blk try std.fmt.allocPrint(b.allocator, "--role {s}", .{iam_role_arn.?});
|
||||
|
||||
if (iam_role_name.len == 0)
|
||||
@panic("Either function-role or function-arn must be specified. function-arn will allow deployment without creating a role");
|
||||
|
||||
// Now we have an iam role name to use, but no iam role arn. Let's go hunting
|
||||
// Once this is done once, we'll have a file with the arn in "cache"
|
||||
// The iam arn will reside in an 'iam_role' file in the bin directory
|
||||
|
||||
// Build system command to create the role if necessary and get the role arn
|
||||
const iam_role_file = b.getInstallPath(.bin, "iam_role");
|
||||
|
||||
if (!fileExists(iam_role_file)) {
|
||||
// std.debug.print("file does not exist", .{});
|
||||
// Our cache file does not exist on disk, so we'll create/get the role
|
||||
// arn using the AWS CLI and dump to disk here
|
||||
const ifstatement_fmt =
|
||||
\\ if aws iam get-role --role-name {s} 2>&1 |grep -q NoSuchEntity; then aws iam create-role --output text --query Role.Arn --role-name {s} --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 > {s}
|
||||
;
|
||||
const ifstatement = try std.fmt.allocPrint(
|
||||
b.allocator,
|
||||
ifstatement_fmt,
|
||||
.{ iam_role_name, iam_role_name, iam_role_file },
|
||||
);
|
||||
iam_step.dependOn(&b.addSystemCommand(&.{ "/bin/sh", "-c", ifstatement }).step);
|
||||
}
|
||||
|
||||
break :blk try std.fmt.allocPrint(b.allocator, "--role \"$(cat {s})\"", .{iam_role_file});
|
||||
};
|
||||
const function_name_file = b.getInstallPath(.bin, 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
|
||||
// TODO: This should determine compilation target and use x86_64 if needed
|
||||
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_param, 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,
|
||||
b.getInstallPath(.bin, exe.out_filename),
|
||||
function_name_file,
|
||||
function_name,
|
||||
not_found_fmt,
|
||||
found_fmt,
|
||||
const iam = Iam.create(b, .{
|
||||
.role_name = iam_role_name,
|
||||
.role_arn = iam_role_arn,
|
||||
});
|
||||
const iam_step = b.step("awslambda_iam", "Create/Get IAM role for function");
|
||||
iam_step.dependOn(&iam.step);
|
||||
|
||||
defer b.allocator.free(cmd);
|
||||
const region = b.option([]const u8, "region", "Region to use [default is autodetect from environment/config]") orelse try findRegionFromSystem(b.allocator);
|
||||
|
||||
// std.debug.print("{s}\n", .{cmd});
|
||||
deploy_step.dependOn(package_step);
|
||||
deploy_step.dependOn(&b.addSystemCommand(&.{ "/bin/sh", "-c", cmd }).step);
|
||||
// Deployment
|
||||
const deploy = Deploy.create(b, .{
|
||||
.name = function_name,
|
||||
.package = package_step.packagedFileLazyPath(),
|
||||
.arch = exe.root_module.resolved_target.?.result.cpu.arch,
|
||||
.iam_step = iam,
|
||||
.region = region,
|
||||
});
|
||||
deploy.step.dependOn(&package_step.step);
|
||||
|
||||
const deploy_step = b.step("awslambda_deploy", "Deploy the function");
|
||||
deploy_step.dependOn(&deploy.step);
|
||||
|
||||
const payload = b.option([]const u8, "payload", "Lambda payload [{\"foo\":\"bar\", \"baz\": \"qux\"}]") orelse
|
||||
\\ {"foo": "bar", "baz": "qux"}"
|
||||
;
|
||||
|
||||
const run_script =
|
||||
\\ f=$(mktemp) && \
|
||||
\\ logs=$(aws lambda invoke \
|
||||
\\ --cli-binary-format raw-in-base64-out \
|
||||
\\ --invocation-type RequestResponse \
|
||||
\\ --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);
|
||||
}
|
||||
|
||||
const invoke = Invoke.create(b, .{
|
||||
.name = function_name,
|
||||
.payload = payload,
|
||||
.region = region,
|
||||
});
|
||||
invoke.step.dependOn(&deploy.step);
|
||||
const run_step = b.step("awslambda_run", "Run the app in AWS lambda");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
run_step.dependOn(&invoke.step);
|
||||
}
|
||||
|
||||
// AWS_CONFIG_FILE (default is ~/.aws/config
|
||||
// AWS_DEFAULT_REGION
|
||||
fn findRegionFromSystem(allocator: std.mem.Allocator) ![]const u8 {
|
||||
const env_map = try std.process.getEnvMap(allocator);
|
||||
if (env_map.get("AWS_DEFAULT_REGION")) |r| return r;
|
||||
const config_file_path = env_map.get("AWS_CONFIG_FILE") orelse
|
||||
try std.fs.path.join(allocator, &[_][]const u8{
|
||||
env_map.get("HOME") orelse env_map.get("USERPROFILE").?,
|
||||
".aws",
|
||||
"config",
|
||||
});
|
||||
const config_file = try std.fs.openFileAbsolute(config_file_path, .{});
|
||||
defer config_file.close();
|
||||
const config_bytes = try config_file.readToEndAlloc(allocator, 1024 * 1024);
|
||||
const profile = env_map.get("AWS_PROFILE") orelse "default";
|
||||
var line_iterator = std.mem.split(u8, config_bytes, "\n");
|
||||
var in_profile = false;
|
||||
while (line_iterator.next()) |line| {
|
||||
const trimmed = std.mem.trim(u8, line, " \t\r");
|
||||
if (trimmed.len == 0 or trimmed[0] == '#') continue;
|
||||
if (!in_profile) {
|
||||
if (trimmed[0] == '[' and trimmed[trimmed.len - 1] == ']') {
|
||||
// this is a profile directive!
|
||||
// std.debug.print("profile: {s}, in file: {s}\n", .{ profile, trimmed[1 .. trimmed.len - 1] });
|
||||
if (std.mem.eql(u8, profile, trimmed[1 .. trimmed.len - 1])) {
|
||||
in_profile = true;
|
||||
}
|
||||
}
|
||||
continue; // we're only looking for a profile at this point
|
||||
}
|
||||
// look for our region directive
|
||||
if (trimmed[0] == '[' and trimmed[trimmed.len - 1] == ']')
|
||||
return error.RegionNotFound; // we've hit another profile without getting our region
|
||||
if (!std.mem.startsWith(u8, trimmed, "region")) continue;
|
||||
var equalityiterator = std.mem.split(u8, trimmed, "=");
|
||||
_ = equalityiterator.next() orelse return error.RegionNotFound;
|
||||
const raw_val = equalityiterator.next() orelse return error.RegionNotFound;
|
||||
return try allocator.dupe(u8, std.mem.trimLeft(u8, raw_val, " \t"));
|
||||
}
|
||||
return error.RegionNotFound;
|
||||
}
|
||||
|
|
161
lambdabuild/Deploy.zig
Normal file
161
lambdabuild/Deploy.zig
Normal file
|
@ -0,0 +1,161 @@
|
|||
const std = @import("std");
|
||||
const aws = @import("aws").aws;
|
||||
|
||||
const Deploy = @This();
|
||||
|
||||
step: std.Build.Step,
|
||||
options: Options,
|
||||
|
||||
const base_id: std.Build.Step.Id = .custom;
|
||||
|
||||
pub const Options = struct {
|
||||
/// Function name to be used for the function
|
||||
name: []const u8,
|
||||
|
||||
/// LazyPath for the function package (zip file)
|
||||
package: std.Build.LazyPath,
|
||||
|
||||
/// Architecture for Lambda function
|
||||
arch: std.Target.Cpu.Arch,
|
||||
|
||||
/// Iam step. This will be a dependency of the deployment
|
||||
iam_step: *@import("Iam.zig"),
|
||||
|
||||
/// Region for deployment
|
||||
region: []const u8,
|
||||
};
|
||||
|
||||
pub fn create(owner: *std.Build, options: Options) *Deploy {
|
||||
const name = owner.dupe(options.name);
|
||||
const step_name = owner.fmt("{s} {s}{s}", .{
|
||||
"aws lambda",
|
||||
"deploy",
|
||||
name,
|
||||
});
|
||||
const self = owner.allocator.create(Deploy) catch @panic("OOM");
|
||||
self.* = .{
|
||||
.step = std.Build.Step.init(.{
|
||||
.id = base_id,
|
||||
.name = step_name,
|
||||
.owner = owner,
|
||||
.makeFn = make,
|
||||
}),
|
||||
.options = options,
|
||||
};
|
||||
|
||||
self.step.dependOn(&options.iam_step.step);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// gets the last time we deployed this function from the name in cache.
|
||||
/// If not in cache, null is returned. Note that cache is not account specific,
|
||||
/// so if you're banging around multiple accounts, you'll want to use different
|
||||
/// local zig caches for each
|
||||
fn getlastDeployedTime(step: *std.Build.Step, name: []const u8) !?[]const u8 {
|
||||
try step.owner.cache_root.handle.makePath("iam");
|
||||
// we should be able to use the role name, as only the following characters
|
||||
// are allowed: _+=,.@-.
|
||||
const cache_file = try std.fmt.allocPrint(
|
||||
step.owner.allocator,
|
||||
"deploy{s}{s}",
|
||||
.{ std.fs.path.sep_str, name },
|
||||
);
|
||||
const buff = try step.owner.allocator.alloc(u8, 64);
|
||||
const time = step.owner.cache_root.handle.readFile(cache_file, buff) catch return null;
|
||||
return time;
|
||||
}
|
||||
|
||||
fn make(step: *std.Build.Step, node: std.Progress.Node) anyerror!void {
|
||||
_ = node;
|
||||
const self: *Deploy = @fieldParentPtr("step", step);
|
||||
|
||||
if (self.options.arch != .aarch64 and self.options.arch != .x86_64)
|
||||
return step.fail("AWS Lambda can only deploy aarch64 and x86_64 functions ({} not allowed)", .{self.options.arch});
|
||||
|
||||
// TODO: Work out cache. HOWEVER...this cannot be done until the caching
|
||||
// for the Deploy command works properly. Right now, it regenerates
|
||||
// the zip file every time
|
||||
// if (try getIamArnFromName(step, self.options.role_name)) |_| {
|
||||
// step.result_cached = true;
|
||||
// return; // exists in cache - nothing to do
|
||||
// }
|
||||
|
||||
var client = aws.Client.init(self.step.owner.allocator, .{});
|
||||
defer client.deinit();
|
||||
const services = aws.Services(.{.lambda}){};
|
||||
const function = blk: {
|
||||
var diagnostics = aws.Diagnostics{
|
||||
.http_code = undefined,
|
||||
.response_body = undefined,
|
||||
.allocator = self.step.owner.allocator,
|
||||
};
|
||||
const options = aws.Options{
|
||||
.client = client,
|
||||
.diagnostics = &diagnostics,
|
||||
.region = self.options.region,
|
||||
};
|
||||
|
||||
aws.globalLogControl(.info, .warn, .info, true);
|
||||
defer aws.globalLogControl(.info, .warn, .info, false);
|
||||
const call = aws.Request(services.lambda.get_function).call(.{
|
||||
.function_name = self.options.name,
|
||||
}, options) catch |e| {
|
||||
// There seems an issue here, but realistically, we have an arena
|
||||
// so there's no leak leaving this out
|
||||
defer diagnostics.deinit();
|
||||
if (diagnostics.http_code == 404) break :blk null;
|
||||
return step.fail(
|
||||
"Unknown error {} from Lambda GetFunction. HTTP code {}, message: {s}",
|
||||
.{ e, diagnostics.http_code, diagnostics.response_body },
|
||||
);
|
||||
};
|
||||
defer call.deinit();
|
||||
|
||||
// TODO: Write call.response.configuration.last_modified to cache
|
||||
|
||||
// std.debug.print("Function found. Last modified: {s}, revision id: {s}\n", .{ call.response.configuration.?.last_modified.?, call.response.configuration.?.revision_id.? });
|
||||
break :blk .{
|
||||
.last_modified = try step.owner.allocator.dupe(u8, call.response.configuration.?.last_modified.?),
|
||||
.revision_id = try step.owner.allocator.dupe(u8, call.response.configuration.?.revision_id.?),
|
||||
};
|
||||
};
|
||||
|
||||
const encoder = std.base64.standard.Encoder;
|
||||
const file = try std.fs.openFileAbsolute(self.options.package.getPath2(step.owner, step), .{});
|
||||
defer file.close();
|
||||
const bytes = try file.readToEndAlloc(step.owner.allocator, 100 * 1024 * 1024);
|
||||
const base64_buf = try step.owner.allocator.alloc(u8, encoder.calcSize(bytes.len));
|
||||
const base64_bytes = encoder.encode(base64_buf, bytes);
|
||||
const options = aws.Options{
|
||||
.client = client,
|
||||
.region = self.options.region,
|
||||
};
|
||||
const arm64_arch = [_][]const u8{"arm64"};
|
||||
const x86_64_arch = [_][]const u8{"x86_64"};
|
||||
const architectures = (if (self.options.arch == .aarch64) arm64_arch else x86_64_arch);
|
||||
const arches: [][]const u8 = @constCast(architectures[0..]);
|
||||
if (function) |f| {
|
||||
// TODO: make sure our zipfile newer than the lambda function
|
||||
const update_call = try aws.Request(services.lambda.update_function_code).call(.{
|
||||
.function_name = self.options.name,
|
||||
.architectures = arches,
|
||||
.revision_id = f.revision_id,
|
||||
.zip_file = base64_bytes,
|
||||
}, options);
|
||||
defer update_call.deinit();
|
||||
// TODO: Write call.response.last_modified to cache
|
||||
// TODO: Write call.response.revision_id to cache?
|
||||
} else {
|
||||
// New function - we need to create from scratch
|
||||
const create_call = try aws.Request(services.lambda.create_function).call(.{
|
||||
.function_name = self.options.name,
|
||||
.architectures = arches,
|
||||
.code = .{ .zip_file = base64_bytes },
|
||||
.handler = "not_applicable",
|
||||
.package_type = "Zip",
|
||||
.runtime = "provided.al2",
|
||||
.role = self.options.iam_step.resolved_arn,
|
||||
}, options);
|
||||
defer create_call.deinit();
|
||||
}
|
||||
}
|
146
lambdabuild/Iam.zig
Normal file
146
lambdabuild/Iam.zig
Normal file
|
@ -0,0 +1,146 @@
|
|||
const std = @import("std");
|
||||
const aws = @import("aws").aws;
|
||||
|
||||
const Iam = @This();
|
||||
|
||||
step: std.Build.Step,
|
||||
options: Options,
|
||||
/// resolved_arn will be set only after make is run
|
||||
resolved_arn: []const u8 = undefined,
|
||||
|
||||
arn_buf: [2048]u8 = undefined, // https://docs.aws.amazon.com/IAM/latest/APIReference/API_Role.html has 2k limit
|
||||
const base_id: std.Build.Step.Id = .custom;
|
||||
|
||||
pub const Options = struct {
|
||||
name: []const u8 = "",
|
||||
role_name: []const u8,
|
||||
role_arn: ?[]const u8,
|
||||
};
|
||||
|
||||
pub fn create(owner: *std.Build, options: Options) *Iam {
|
||||
const name = owner.dupe(options.name);
|
||||
const step_name = owner.fmt("{s} {s}{s}", .{
|
||||
"aws lambda",
|
||||
"iam",
|
||||
name,
|
||||
});
|
||||
const self = owner.allocator.create(Iam) catch @panic("OOM");
|
||||
self.* = .{
|
||||
.step = std.Build.Step.init(.{
|
||||
.id = base_id,
|
||||
.name = step_name,
|
||||
.owner = owner,
|
||||
.makeFn = make,
|
||||
}),
|
||||
.options = options,
|
||||
};
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/// gets an IamArn from the name in cache. If not in cache, null is returned
|
||||
/// Note that cache is not account specific, so if you're banging around multiple
|
||||
/// accounts, you'll want to use different local zig caches for each
|
||||
pub fn getIamArnFromName(step: *std.Build.Step, name: []const u8) !?[]const u8 {
|
||||
try step.owner.cache_root.handle.makePath("iam");
|
||||
// we should be able to use the role name, as only the following characters
|
||||
// are allowed: _+=,.@-.
|
||||
const iam_file = try std.fmt.allocPrint(
|
||||
step.owner.allocator,
|
||||
"iam{s}{s}",
|
||||
.{ std.fs.path.sep_str, name },
|
||||
);
|
||||
const buff = try step.owner.allocator.alloc(u8, 64);
|
||||
const arn = step.owner.cache_root.handle.readFile(iam_file, buff) catch return null;
|
||||
return arn;
|
||||
}
|
||||
|
||||
fn make(step: *std.Build.Step, node: std.Progress.Node) anyerror!void {
|
||||
_ = node;
|
||||
const self: *Iam = @fieldParentPtr("step", step);
|
||||
|
||||
if (try getIamArnFromName(step, self.options.role_name)) |a| {
|
||||
step.result_cached = true;
|
||||
@memcpy(self.arn_buf[0..a.len], a);
|
||||
self.resolved_arn = self.arn_buf[0..a.len];
|
||||
return; // exists in cache - nothing to do
|
||||
}
|
||||
|
||||
var client = aws.Client.init(self.step.owner.allocator, .{});
|
||||
defer client.deinit();
|
||||
const services = aws.Services(.{.iam}){};
|
||||
|
||||
var arn = blk: {
|
||||
var diagnostics = aws.Diagnostics{
|
||||
.http_code = undefined,
|
||||
.response_body = undefined,
|
||||
.allocator = self.step.owner.allocator,
|
||||
};
|
||||
const options = aws.Options{
|
||||
.client = client,
|
||||
.diagnostics = &diagnostics,
|
||||
};
|
||||
|
||||
const call = aws.Request(services.iam.get_role).call(.{
|
||||
.role_name = self.options.role_name, // TODO: if we have a role_arn, we should use it and skip
|
||||
}, options) catch |e| {
|
||||
defer diagnostics.deinit();
|
||||
if (diagnostics.http_code == 404) break :blk null;
|
||||
return step.fail(
|
||||
"Unknown error {} from IAM GetRole. HTTP code {}, message: {s}",
|
||||
.{ e, diagnostics.http_code, diagnostics.response_body },
|
||||
);
|
||||
};
|
||||
defer call.deinit();
|
||||
|
||||
break :blk try step.owner.allocator.dupe(u8, call.response.role.arn);
|
||||
};
|
||||
// Now ARN will either be null (does not exist), or a value
|
||||
|
||||
if (arn == null) {
|
||||
// we need to create the role before proceeding
|
||||
const options = aws.Options{
|
||||
.client = client,
|
||||
};
|
||||
|
||||
const create_call = try aws.Request(services.iam.create_role).call(.{
|
||||
.role_name = self.options.role_name,
|
||||
.assume_role_policy_document =
|
||||
\\{
|
||||
\\ "Version": "2012-10-17",
|
||||
\\ "Statement": [
|
||||
\\ {
|
||||
\\ "Sid": "",
|
||||
\\ "Effect": "Allow",
|
||||
\\ "Principal": {
|
||||
\\ "Service": "lambda.amazonaws.com"
|
||||
\\ },
|
||||
\\ "Action": "sts:AssumeRole"
|
||||
\\ }
|
||||
\\ ]
|
||||
\\}
|
||||
,
|
||||
}, options);
|
||||
defer create_call.deinit();
|
||||
arn = try step.owner.allocator.dupe(u8, create_call.response.role.arn);
|
||||
const attach_call = try aws.Request(services.iam.attach_role_policy).call(.{
|
||||
.policy_arn = "arn:aws:iam::aws:policy/AWSLambdaExecute",
|
||||
.role_name = self.options.role_name,
|
||||
}, options);
|
||||
defer attach_call.deinit();
|
||||
}
|
||||
|
||||
@memcpy(self.arn_buf[0..arn.?.len], arn.?);
|
||||
self.resolved_arn = self.arn_buf[0..arn.?.len];
|
||||
|
||||
// NOTE: This must match getIamArnFromName
|
||||
const iam_file = try std.fmt.allocPrint(
|
||||
step.owner.allocator,
|
||||
"iam{s}{s}",
|
||||
.{ std.fs.path.sep_str, self.options.role_name },
|
||||
);
|
||||
try step.owner.cache_root.handle.writeFile(.{
|
||||
.sub_path = iam_file,
|
||||
.data = arn.?,
|
||||
});
|
||||
}
|
63
lambdabuild/Invoke.zig
Normal file
63
lambdabuild/Invoke.zig
Normal file
|
@ -0,0 +1,63 @@
|
|||
const std = @import("std");
|
||||
const aws = @import("aws").aws;
|
||||
|
||||
const Invoke = @This();
|
||||
|
||||
step: std.Build.Step,
|
||||
options: Options,
|
||||
|
||||
const base_id: std.Build.Step.Id = .custom;
|
||||
|
||||
pub const Options = struct {
|
||||
/// Function name to invoke
|
||||
name: []const u8,
|
||||
|
||||
/// Payload to send to the function
|
||||
payload: []const u8,
|
||||
|
||||
/// Region for deployment
|
||||
region: []const u8,
|
||||
};
|
||||
|
||||
pub fn create(owner: *std.Build, options: Options) *Invoke {
|
||||
const name = owner.dupe(options.name);
|
||||
const step_name = owner.fmt("{s} {s}{s}", .{
|
||||
"aws lambda",
|
||||
"invoke",
|
||||
name,
|
||||
});
|
||||
const self = owner.allocator.create(Invoke) catch @panic("OOM");
|
||||
self.* = .{
|
||||
.step = std.Build.Step.init(.{
|
||||
.id = base_id,
|
||||
.name = step_name,
|
||||
.owner = owner,
|
||||
.makeFn = make,
|
||||
}),
|
||||
.options = options,
|
||||
};
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
fn make(step: *std.Build.Step, node: std.Progress.Node) anyerror!void {
|
||||
_ = node;
|
||||
const self: *Invoke = @fieldParentPtr("step", step);
|
||||
|
||||
var client = aws.Client.init(self.step.owner.allocator, .{});
|
||||
defer client.deinit();
|
||||
const services = aws.Services(.{.lambda}){};
|
||||
|
||||
const options = aws.Options{
|
||||
.client = client,
|
||||
.region = self.options.region,
|
||||
};
|
||||
const call = try aws.Request(services.lambda.invoke).call(.{
|
||||
.function_name = self.options.name,
|
||||
.payload = self.options.payload,
|
||||
.log_type = "Tail",
|
||||
.invocation_type = "RequestResponse",
|
||||
}, options);
|
||||
defer call.deinit();
|
||||
std.debug.print("{?s}\n", .{call.response.payload});
|
||||
}
|
94
lambdabuild/Package.zig
Normal file
94
lambdabuild/Package.zig
Normal file
|
@ -0,0 +1,94 @@
|
|||
const std = @import("std");
|
||||
|
||||
const Package = @This();
|
||||
|
||||
step: std.Build.Step,
|
||||
lambda_zipfile: []const u8,
|
||||
|
||||
const base_id: std.Build.Step.Id = .install_file;
|
||||
|
||||
pub const Options = struct {
|
||||
name: []const u8 = "",
|
||||
exe: *std.Build.Step.Compile,
|
||||
zipfile_name: []const u8 = "function.zip",
|
||||
};
|
||||
|
||||
pub fn create(owner: *std.Build, options: Options) *Package {
|
||||
const name = owner.dupe(options.name);
|
||||
const step_name = owner.fmt("{s} {s}{s}", .{
|
||||
"aws lambda",
|
||||
"package",
|
||||
name,
|
||||
});
|
||||
const package = owner.allocator.create(Package) catch @panic("OOM");
|
||||
package.* = .{
|
||||
.step = std.Build.Step.init(.{
|
||||
.id = base_id,
|
||||
.name = step_name,
|
||||
.owner = owner,
|
||||
.makeFn = make,
|
||||
}),
|
||||
.lambda_zipfile = options.zipfile_name,
|
||||
};
|
||||
|
||||
// TODO: For Windows, tar.exe can actually do zip files. tar -a -cf function.zip file1 [file2...]
|
||||
// https://superuser.com/questions/201371/create-zip-folder-from-the-command-line-windows#comment2725283_898508
|
||||
//
|
||||
// We'll want two system commands here. One for the exe itself, and one for
|
||||
// other files (TODO: what does this latter one look like? maybe it's an option?)
|
||||
var zip_cmd = owner.addSystemCommand(&.{ "zip", "-qj9X" });
|
||||
zip_cmd.has_side_effects = true; // TODO: move these to makeFn as we have little cache control here...
|
||||
zip_cmd.setCwd(.{ .src_path = .{
|
||||
.owner = owner,
|
||||
.sub_path = owner.getInstallPath(.prefix, "."),
|
||||
} });
|
||||
const zipfile = zip_cmd.addOutputFileArg(options.zipfile_name);
|
||||
zip_cmd.addArg(owner.getInstallPath(.bin, "bootstrap"));
|
||||
// std.debug.print("\nzip cmdline: {s}", .{zip});
|
||||
if (!std.mem.eql(u8, "bootstrap", options.exe.out_filename)) {
|
||||
// We need to copy stuff around
|
||||
// TODO: should this be installing bootstrap binary in .bin directory?
|
||||
const cp_cmd = owner.addSystemCommand(&.{ "cp", owner.getInstallPath(.bin, options.exe.out_filename) });
|
||||
cp_cmd.has_side_effects = true;
|
||||
const copy_output = cp_cmd.addOutputFileArg("bootstrap");
|
||||
const install_copy = owner.addInstallFileWithDir(copy_output, .bin, "bootstrap");
|
||||
cp_cmd.step.dependOn(owner.getInstallStep());
|
||||
zip_cmd.step.dependOn(&install_copy.step);
|
||||
// might as well leave this bootstrap around for caching purposes
|
||||
// const rm_cmd = owner.addSystemCommand(&.{ "rm", owner.getInstallPath(.bin, "bootstrap"), });
|
||||
}
|
||||
const install_zipfile = owner.addInstallFileWithDir(zipfile, .prefix, options.zipfile_name);
|
||||
install_zipfile.step.dependOn(&zip_cmd.step);
|
||||
package.step.dependOn(&install_zipfile.step);
|
||||
return package;
|
||||
}
|
||||
|
||||
pub fn packagedFilePath(self: Package) []const u8 {
|
||||
return self.step.owner.getInstallPath(.prefix, self.options.zipfile_name);
|
||||
}
|
||||
pub fn packagedFileLazyPath(self: Package) std.Build.LazyPath {
|
||||
return .{ .src_path = .{
|
||||
.owner = self.step.owner,
|
||||
.sub_path = self.step.owner.getInstallPath(.prefix, self.lambda_zipfile),
|
||||
} };
|
||||
}
|
||||
|
||||
fn make(step: *std.Build.Step, node: std.Progress.Node) anyerror!void {
|
||||
// Make here doesn't actually do anything. But we want to set up this
|
||||
// step this way, so that when (if) zig stdlib gains the abiltity to write
|
||||
// zip files in addition to reading them, we can skip all the system commands
|
||||
// and just do all the things here instead
|
||||
//
|
||||
//
|
||||
// TODO: The caching plan will be:
|
||||
//
|
||||
// get a hash of the bootstrap and whatever other files we put into the zip
|
||||
// file (because a zip is not really reproducible). If the cache directory
|
||||
// has the hash as its latest hash, we have nothing to do, so we can exit
|
||||
// at that point
|
||||
//
|
||||
// Otherwise, store that hash in our cache, and copy our bootstrap, zip
|
||||
// things up and install the file into zig-out
|
||||
_ = node;
|
||||
_ = step;
|
||||
}
|
BIN
lambdabuild/function.zip
Normal file
BIN
lambdabuild/function.zip
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user