forked from lobo/lambda-zig
		
	out with AWS cli/in with the SDK
This commit is contained in:
		
							parent
							
								
									ef5b793882
								
							
						
					
					
						commit
						f86bafc533
					
				
					 9 changed files with 591 additions and 152 deletions
				
			
		
							
								
								
									
										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…
	
	Add table
		
		Reference in a new issue