413 lines
19 KiB
Zig
413 lines
19 KiB
Zig
const std = @import("std");
|
|
const aws = @import("aws.zig");
|
|
const json = @import("json.zig");
|
|
|
|
var verbose: u8 = 0;
|
|
|
|
pub fn log(
|
|
comptime level: std.log.Level,
|
|
comptime scope: @TypeOf(.EnumLiteral),
|
|
comptime format: []const u8,
|
|
args: anytype,
|
|
) void {
|
|
// Ignore aws_signing messages
|
|
if (verbose < 3 and scope == .aws_signing and @intFromEnum(level) >= @intFromEnum(std.log.Level.debug))
|
|
return;
|
|
// Ignore aws_credentials messages
|
|
if (verbose < 3 and scope == .aws_credentials and @intFromEnum(level) >= @intFromEnum(std.log.Level.debug))
|
|
return;
|
|
// Ignore xml_shaper messages
|
|
if (verbose < 3 and scope == .xml_shaper and @intFromEnum(level) >= @intFromEnum(std.log.Level.debug))
|
|
return;
|
|
// Ignore date messages
|
|
if (verbose < 3 and scope == .date and @intFromEnum(level) >= @intFromEnum(std.log.Level.debug))
|
|
return;
|
|
// Ignore awshttp messages
|
|
if (verbose < 2 and scope == .awshttp and @intFromEnum(level) >= @intFromEnum(std.log.Level.debug))
|
|
return;
|
|
|
|
if (verbose < 1 and scope == .aws and @intFromEnum(level) >= @intFromEnum(std.log.Level.debug))
|
|
return;
|
|
const scope_prefix = "(" ++ @tagName(scope) ++ "): ";
|
|
const prefix = "[" ++ @tagName(level) ++ "] " ++ scope_prefix;
|
|
|
|
// Print the message to stderr, silently ignoring any errors
|
|
std.debug.lockStdErr();
|
|
defer std.debug.unlockStdErr();
|
|
const stderr = std.io.getStdErr().writer();
|
|
nosuspend stderr.print(prefix ++ format ++ "\n", args) catch return;
|
|
}
|
|
|
|
pub const std_options = std.Options{
|
|
.logFn = log,
|
|
};
|
|
const Tests = enum {
|
|
query_no_input,
|
|
query_with_input,
|
|
ec2_query_no_input,
|
|
ec2_query_with_input,
|
|
json_1_0_query_with_input,
|
|
json_1_0_query_no_input,
|
|
json_1_1_query_with_input,
|
|
json_1_1_query_no_input,
|
|
rest_json_1_query_no_input,
|
|
rest_json_1_query_with_input,
|
|
rest_json_1_work_with_lambda,
|
|
rest_xml_no_input,
|
|
rest_xml_anything_but_s3,
|
|
rest_xml_work_with_s3,
|
|
};
|
|
|
|
pub fn main() anyerror!void {
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
defer _ = gpa.deinit();
|
|
const allocator = gpa.allocator();
|
|
var tests = std.ArrayList(Tests).init(allocator);
|
|
defer tests.deinit();
|
|
var args = try std.process.argsWithAllocator(allocator);
|
|
defer args.deinit();
|
|
const stdout_raw = std.io.getStdOut().writer();
|
|
var bw = std.io.bufferedWriter(stdout_raw);
|
|
defer bw.flush() catch unreachable;
|
|
const stdout = bw.writer();
|
|
var arg0: ?[]const u8 = null;
|
|
var proxy: ?std.http.Client.Proxy = null;
|
|
while (args.next()) |arg| {
|
|
if (arg0 == null) arg0 = arg;
|
|
if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
|
|
try stdout.print(
|
|
\\usage: {?s} [-h|--help] [-v][-v][-v] [-x|--proxy <proxy url>] [tests...]
|
|
\\
|
|
\\Where tests are one of the following:
|
|
\\
|
|
, .{arg0});
|
|
inline for (std.meta.fields(Tests)) |enumfield| {
|
|
try stdout.print("* {s}\n", .{enumfield.name});
|
|
}
|
|
return;
|
|
}
|
|
if (std.mem.eql(u8, "-x", arg) or std.mem.eql(u8, "--proxy", arg)) {
|
|
proxy = try proxyFromString(args.next().?); // parse stuff
|
|
continue;
|
|
}
|
|
if (std.mem.startsWith(u8, arg, "-v")) {
|
|
for (arg[1..]) |c| {
|
|
if (c != 'v') return error.InvalidArgument;
|
|
verbose += 1;
|
|
}
|
|
continue;
|
|
}
|
|
inline for (@typeInfo(Tests).@"enum".fields) |f| {
|
|
if (std.mem.eql(u8, f.name, arg)) {
|
|
try tests.append(@field(Tests, f.name));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (tests.items.len == 0) {
|
|
inline for (@typeInfo(Tests).@"enum".fields) |f|
|
|
try tests.append(@field(Tests, f.name));
|
|
}
|
|
|
|
std.log.info("Start\n", .{});
|
|
const client_options = aws.ClientOptions{ .proxy = proxy };
|
|
var client = aws.Client.init(allocator, client_options);
|
|
const options = aws.Options{
|
|
.region = "us-west-2",
|
|
.client = client,
|
|
};
|
|
defer client.deinit();
|
|
|
|
// As of 2023-08-28, only ECS from this list supports TLS v1.3
|
|
// AWS commitment is to enable all services by 2023-12-31
|
|
const services = aws.Services(.{ .sts, .ec2, .dynamo_db, .ecs, .lambda, .sqs, .s3, .cloudfront }){};
|
|
|
|
for (tests.items) |t| {
|
|
std.log.info("===== Start Test: {s} =====", .{@tagName(t)});
|
|
switch (t) {
|
|
.query_no_input => {
|
|
const call = try aws.Request(services.sts.get_caller_identity).call(.{}, options);
|
|
// const call = try client.call(services.sts.get_caller_identity.Request{}, options);
|
|
defer call.deinit();
|
|
std.log.info("arn: {s}", .{call.response.arn.?});
|
|
std.log.info("id: {s}", .{call.response.user_id.?});
|
|
std.log.info("account: {s}", .{call.response.account.?});
|
|
std.log.info("requestId: {s}", .{call.response_metadata.request_id});
|
|
},
|
|
.query_with_input => {
|
|
const call = try client.call(services.sqs.list_queues.Request{
|
|
.queue_name_prefix = "s",
|
|
}, options);
|
|
defer call.deinit();
|
|
std.log.info("request id: {s}", .{call.response_metadata.request_id});
|
|
std.log.info("account has queues with prefix 's': {}", .{call.response.queue_urls != null});
|
|
},
|
|
.json_1_0_query_with_input => {
|
|
const call = try client.call(services.dynamo_db.list_tables.Request{
|
|
.limit = 1,
|
|
}, options);
|
|
defer call.deinit();
|
|
std.log.info("request id: {s}", .{call.response_metadata.request_id});
|
|
std.log.info("account has tables: {}", .{call.response.table_names.?.len > 0});
|
|
},
|
|
.json_1_0_query_no_input => {
|
|
const call = try client.call(services.dynamo_db.describe_limits.Request{}, options);
|
|
defer call.deinit();
|
|
std.log.info("account read capacity limit: {?d}", .{call.response.account_max_read_capacity_units});
|
|
},
|
|
.json_1_1_query_with_input => {
|
|
const call = try client.call(services.ecs.list_clusters.Request{
|
|
.max_results = 1,
|
|
}, options);
|
|
defer call.deinit();
|
|
std.log.info("request id: {s}", .{call.response_metadata.request_id});
|
|
std.log.info("account has clusters: {}", .{call.response.cluster_arns.?.len > 0});
|
|
},
|
|
.json_1_1_query_no_input => {
|
|
const call = try client.call(services.ecs.list_clusters.Request{}, options);
|
|
defer call.deinit();
|
|
std.log.info("request id: {s}", .{call.response_metadata.request_id});
|
|
std.log.info("account has clusters: {}", .{call.response.cluster_arns.?.len > 0});
|
|
},
|
|
.rest_json_1_query_with_input => {
|
|
const call = try client.call(services.lambda.list_functions.Request{
|
|
.max_items = 1,
|
|
}, options);
|
|
defer call.deinit();
|
|
std.log.info("request id: {s}", .{call.response_metadata.request_id});
|
|
std.log.info("account has functions: {}", .{call.response.functions.?.len > 0});
|
|
},
|
|
.rest_json_1_query_no_input => {
|
|
const call = try client.call(services.lambda.list_functions.Request{}, options);
|
|
defer call.deinit();
|
|
std.log.info("request id: {s}", .{call.response_metadata.request_id});
|
|
std.log.info("account has functions: {}", .{call.response.functions.?.len > 0});
|
|
},
|
|
.rest_json_1_work_with_lambda => {
|
|
const call = try client.call(services.lambda.list_functions.Request{}, options);
|
|
defer call.deinit();
|
|
std.log.info("list request id: {s}", .{call.response_metadata.request_id});
|
|
if (call.response.functions) |fns| {
|
|
if (fns.len > 0) {
|
|
const func = fns[0];
|
|
const arn = func.function_arn.?;
|
|
// This is a bit ugly. Maybe a helper function in the library would help?
|
|
var tags = try std.ArrayList(@typeInfo(try typeForField(services.lambda.tag_resource.Request, "tags")).pointer.child).initCapacity(allocator, 1);
|
|
defer tags.deinit();
|
|
tags.appendAssumeCapacity(.{ .key = "Foo", .value = "Bar" });
|
|
const req = services.lambda.tag_resource.Request{ .resource = arn, .tags = tags.items };
|
|
const addtag = try aws.Request(services.lambda.tag_resource).call(req, options);
|
|
// TODO: This is failing due to double-encoding (see zig issue 17015)
|
|
defer addtag.deinit();
|
|
// const addtag = try client.call(services.lambda.tag_resource.Request{ .resource = arn, .tags = &.{.{ .key = "Foo", .value = "Bar" }} }, options);
|
|
std.log.info("add tag request id: {s}", .{addtag.response_metadata.request_id});
|
|
var keys = [_][]const u8{"Foo"}; // Would love to have a way to express this without burning a var here
|
|
const deletetag = try aws.Request(services.lambda.untag_resource).call(.{ .tag_keys = keys[0..], .resource = arn }, options);
|
|
defer deletetag.deinit();
|
|
std.log.info("delete tag request id: {s}", .{deletetag.response_metadata.request_id});
|
|
} else {
|
|
std.log.err("no functions to work with", .{});
|
|
}
|
|
} else {
|
|
std.log.err("no functions to work with", .{});
|
|
}
|
|
},
|
|
.ec2_query_no_input => {
|
|
// Describe regions is a simpler request and easier to debug
|
|
const result = try client.call(services.ec2.describe_regions.Request{}, options);
|
|
defer result.deinit();
|
|
std.log.info("request id: {s}", .{result.response_metadata.request_id});
|
|
std.log.info("region count: {d}", .{result.response.regions.?.len});
|
|
},
|
|
.ec2_query_with_input => {
|
|
// Describe instances is more interesting
|
|
const result = try client.call(services.ec2.describe_instances.Request{ .max_results = 6 }, options);
|
|
defer result.deinit();
|
|
std.log.info("reservation count: {d}", .{result.response.reservations.?.len});
|
|
var items: usize = 0;
|
|
for (result.response.reservations.?) |reservation| {
|
|
items += reservation.instances.?.len;
|
|
}
|
|
std.log.info("items count: {d}", .{items});
|
|
var next = result.response.next_token;
|
|
while (next) |next_token| {
|
|
std.log.info("more results available: fetching again", .{});
|
|
|
|
const more = try aws.Request(services.ec2.describe_instances)
|
|
.call(.{ .next_token = next_token, .max_results = 6 }, options);
|
|
defer more.deinit();
|
|
// we could have exactly 6, which means we have a next token(?!) but not
|
|
// any actual additional data
|
|
if (more.response.reservations == null) break;
|
|
std.log.info("reservation count: {d}", .{more.response.reservations.?.len});
|
|
var batch_items: usize = 0;
|
|
for (more.response.reservations.?) |reservation| {
|
|
batch_items += reservation.instances.?.len;
|
|
}
|
|
std.log.info("items count: {d}", .{batch_items});
|
|
items += batch_items;
|
|
std.log.info("total items count: {d}", .{items});
|
|
next = more.response.next_token;
|
|
}
|
|
},
|
|
// ^^ under test. vv still in progress
|
|
.rest_xml_no_input => {
|
|
const result = try client.call(services.s3.list_buckets.Request{}, options);
|
|
defer result.deinit();
|
|
std.log.info("request id: {s}", .{result.response_metadata.request_id});
|
|
std.log.info("bucket count: {d}", .{result.response.buckets.?.len});
|
|
},
|
|
.rest_xml_anything_but_s3 => {
|
|
const result = try client.call(services.cloudfront.list_key_groups.Request{}, options);
|
|
defer result.deinit();
|
|
std.log.info("request id: {s}", .{result.response_metadata.request_id});
|
|
const list = result.response.key_group_list.?;
|
|
std.log.info("key group list max: {?d}", .{list.max_items});
|
|
std.log.info("key group quantity: {d}", .{list.quantity});
|
|
},
|
|
.rest_xml_work_with_s3 => {
|
|
const key = "i/am/a/teapot/foo";
|
|
// // const key = "foo";
|
|
//
|
|
const bucket = blk: {
|
|
const result = try client.call(services.s3.list_buckets.Request{}, options);
|
|
defer result.deinit();
|
|
const bucket = result.response.buckets.?[result.response.buckets.?.len - 1];
|
|
std.log.info("ListBuckets request id: {s}", .{result.response_metadata.request_id});
|
|
std.log.info("bucket name: {s}", .{bucket.name.?});
|
|
break :blk try allocator.dupe(u8, bucket.name.?);
|
|
};
|
|
defer allocator.free(bucket);
|
|
const location = blk: {
|
|
const result = try aws.Request(services.s3.get_bucket_location).call(.{
|
|
.bucket = bucket,
|
|
}, options);
|
|
defer result.deinit();
|
|
const location = result.response.location_constraint.?;
|
|
std.log.info("GetBucketLocation request id: {s}", .{result.response_metadata.request_id});
|
|
std.log.info("location: {s}", .{location});
|
|
break :blk try allocator.dupe(u8, location);
|
|
};
|
|
defer allocator.free(location);
|
|
|
|
const s3opts = aws.Options{
|
|
.region = location,
|
|
.client = client,
|
|
};
|
|
{
|
|
const result = try aws.Request(services.s3.put_object).call(.{
|
|
.bucket = bucket,
|
|
.key = key,
|
|
.content_type = "text/plain",
|
|
.body = "bar",
|
|
.storage_class = "STANDARD",
|
|
}, s3opts);
|
|
std.log.info("PutObject Request id: {s}", .{result.response_metadata.request_id});
|
|
std.log.info("PutObject etag: {s}", .{result.response.e_tag.?});
|
|
defer result.deinit();
|
|
}
|
|
{
|
|
// Note that boto appears to redirect by default, but java
|
|
// does not. We will not
|
|
const result = try aws.Request(services.s3.get_object).call(.{
|
|
.bucket = bucket,
|
|
.key = key,
|
|
}, s3opts);
|
|
std.log.info("GetObject Request id: {s}", .{result.response_metadata.request_id});
|
|
std.log.info("GetObject Body: {s}", .{result.response.body.?});
|
|
std.log.info("GetObject etag: {s}", .{result.response.e_tag.?});
|
|
std.log.info("GetObject last modified (seconds since epoch): {d}", .{result.response.last_modified.?});
|
|
defer result.deinit();
|
|
}
|
|
{
|
|
const result = try aws.Request(services.s3.delete_object).call(.{
|
|
.bucket = bucket,
|
|
.key = key,
|
|
}, s3opts);
|
|
std.log.info("DeleteObject Request id: {s}", .{result.response_metadata.request_id});
|
|
defer result.deinit();
|
|
}
|
|
{
|
|
const result = try aws.Request(services.s3.list_objects).call(.{
|
|
.bucket = bucket,
|
|
}, s3opts);
|
|
std.log.info("ListObject Request id: {s}", .{result.response_metadata.request_id});
|
|
std.log.info("Object count: {d}", .{result.response.contents.?.len});
|
|
defer result.deinit();
|
|
}
|
|
},
|
|
}
|
|
std.log.info("===== End Test: {s} =====\n", .{@tagName(t)});
|
|
}
|
|
|
|
// if (test_twice) {
|
|
// std.time.sleep(1000 * std.time.ns_per_ms);
|
|
// std.log.info("second request", .{});
|
|
//
|
|
// var client2 = aws.Aws.init(allocator);
|
|
// defer client2.deinit();
|
|
// const resp2 = try client2.call(services.sts.get_caller_identity.Request{}, options); // catch here and try alloc?
|
|
// defer resp2.deinit();
|
|
// }
|
|
|
|
std.log.info("===== Tests complete =====", .{});
|
|
}
|
|
|
|
fn proxyFromString(string: []const u8) !std.http.Client.Proxy {
|
|
var rc = std.http.Client.Proxy{
|
|
.protocol = undefined,
|
|
.host = undefined,
|
|
.authorization = null,
|
|
.port = undefined,
|
|
.supports_connect = true, // TODO: Is this a good default?
|
|
};
|
|
var remaining: []const u8 = string;
|
|
if (std.mem.startsWith(u8, string, "http://")) {
|
|
remaining = remaining["http://".len..];
|
|
rc.protocol = .plain;
|
|
rc.port = 80;
|
|
} else if (std.mem.startsWith(u8, string, "https://")) {
|
|
remaining = remaining["https://".len..];
|
|
rc.port = 443;
|
|
rc.protocol = .tls;
|
|
} else return error.InvalidScheme;
|
|
var split_iterator = std.mem.splitScalar(u8, remaining, ':');
|
|
rc.host = std.mem.trimRight(u8, split_iterator.first(), "/");
|
|
if (split_iterator.next()) |port|
|
|
rc.port = try std.fmt.parseInt(u16, port, 10);
|
|
return rc;
|
|
}
|
|
fn typeForField(comptime T: type, comptime field_name: []const u8) !type {
|
|
const ti = @typeInfo(T);
|
|
switch (ti) {
|
|
.@"struct" => {
|
|
inline for (ti.@"struct".fields) |field| {
|
|
if (std.mem.eql(u8, field.name, field_name))
|
|
return field.type;
|
|
}
|
|
},
|
|
else => return error.TypeIsNotAStruct, // should not hit this
|
|
}
|
|
return error.FieldNotFound;
|
|
}
|
|
|
|
// TODO: Move into json.zig
|
|
pub fn jsonFun() !void {
|
|
// Standard behavior
|
|
const payload =
|
|
\\{"GetCallerIdentityResponse":{"GetCallerIdentityResult":{"Account":"0123456789","Arn":"arn:aws:iam::0123456789:user/test","UserId":"MYUSERID"},"ResponseMetadata":{"RequestId":"3b80a99b-7df8-4bcb-96ee-b2759878a5f2"}}}
|
|
;
|
|
const Ret3 = struct {
|
|
getCallerIdentityResponse: struct { getCallerIdentityResult: struct { account: []u8, arn: []u8, user_id: []u8 }, responseMetadata: struct { requestId: []u8 } },
|
|
};
|
|
var stream3 = json.TokenStream.init(payload);
|
|
const res3 = json.parse(Ret3, &stream3, .{
|
|
.allocator = std.heap.c_allocator,
|
|
.allow_camel_case_conversion = true, // new option
|
|
.allow_snake_case_conversion = true, // new option
|
|
.allow_unknown_fields = true, // new option
|
|
}) catch unreachable;
|
|
std.log.info("{}", .{res3});
|
|
std.log.info("{any}", .{res3.getCallerIdentityResponse.getCallerIdentityResult.user_id});
|
|
}
|