add profile to alexa permissions tool
This commit is contained in:
parent
d2e399f7be
commit
b13dee642f
3 changed files with 105 additions and 26 deletions
20
build.zig
20
build.zig
|
|
@ -50,6 +50,20 @@ pub fn build(b: *std.Build) !void {
|
||||||
// Function name defaults to exe.name ("house-control")
|
// Function name defaults to exe.name ("house-control")
|
||||||
const lambda = try lambda_zig.configureBuild(b, lambda_zig_dep, exe, .{});
|
const lambda = try lambda_zig.configureBuild(b, lambda_zig_dep, exe, .{});
|
||||||
|
|
||||||
|
// Get AWS profile option (already declared by lambda-zig)
|
||||||
|
const profile = b.user_input_options.get("profile");
|
||||||
|
const profile_str: ?[]const u8 = if (profile) |p| switch (p.value) {
|
||||||
|
.scalar => |s| s,
|
||||||
|
else => null,
|
||||||
|
} else null;
|
||||||
|
|
||||||
|
// Get AWS region option (already declared by lambda-zig)
|
||||||
|
const region = b.user_input_options.get("region");
|
||||||
|
const region_str: ?[]const u8 = if (region) |r| switch (r.value) {
|
||||||
|
.scalar => |s| s,
|
||||||
|
else => null,
|
||||||
|
} else null;
|
||||||
|
|
||||||
// Build the gen-skill-json tool (runs on host)
|
// Build the gen-skill-json tool (runs on host)
|
||||||
const gen_skill_json_module = b.createModule(.{
|
const gen_skill_json_module = b.createModule(.{
|
||||||
.root_source_file = b.path("tools/gen-skill-json.zig"),
|
.root_source_file = b.path("tools/gen-skill-json.zig"),
|
||||||
|
|
@ -112,6 +126,12 @@ pub fn build(b: *std.Build) !void {
|
||||||
const add_alexa_perm_cmd = b.addRunArtifact(add_alexa_perm_exe);
|
const add_alexa_perm_cmd = b.addRunArtifact(add_alexa_perm_exe);
|
||||||
add_alexa_perm_cmd.addFileArg(lambda.deploy_output);
|
add_alexa_perm_cmd.addFileArg(lambda.deploy_output);
|
||||||
add_alexa_perm_cmd.addFileArg(b.path(".ask/ask-states.json"));
|
add_alexa_perm_cmd.addFileArg(b.path(".ask/ask-states.json"));
|
||||||
|
if (profile_str) |p| {
|
||||||
|
add_alexa_perm_cmd.addArgs(&.{ "--profile", p });
|
||||||
|
}
|
||||||
|
if (region_str) |r| {
|
||||||
|
add_alexa_perm_cmd.addArgs(&.{ "--region", r });
|
||||||
|
}
|
||||||
// Must run after ASK deploy (which creates/updates skill ID) and Lambda deploy
|
// Must run after ASK deploy (which creates/updates skill ID) and Lambda deploy
|
||||||
add_alexa_perm_cmd.step.dependOn(&ask_deploy_cmd.step);
|
add_alexa_perm_cmd.step.dependOn(&ask_deploy_cmd.step);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,36 @@
|
||||||
//! Timezone utilities: TZif parsing and local timezone resolution.
|
//! Timezone utilities: TZif parsing and local timezone resolution.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const log = std.log.scoped(.timezone);
|
const log = std.log.scoped(.timezone);
|
||||||
|
|
||||||
|
// Suppress warnings during tests
|
||||||
|
fn warn(comptime fmt: []const u8, args: anytype) void {
|
||||||
|
if (!builtin.is_test) {
|
||||||
|
log.warn(fmt, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get UTC offset in seconds for a timezone name.
|
/// Get UTC offset in seconds for a timezone name.
|
||||||
/// Reads /usr/share/zoneinfo/{name} and parses TZif format.
|
/// Reads /usr/share/zoneinfo/{name} and parses TZif format.
|
||||||
/// Returns null on any error (file not found, parse error, etc.)
|
/// Returns null on any error (file not found, parse error, etc.)
|
||||||
pub fn getUtcOffset(allocator: Allocator, timezone_name: []const u8) ?i32 {
|
pub fn getUtcOffset(allocator: Allocator, timezone_name: []const u8) ?i32 {
|
||||||
const path = std.fmt.allocPrint(allocator, "/usr/share/zoneinfo/{s}", .{timezone_name}) catch {
|
const path = std.fmt.allocPrint(allocator, "/usr/share/zoneinfo/{s}", .{timezone_name}) catch {
|
||||||
log.warn("Failed to allocate timezone path", .{});
|
warn("Failed to allocate timezone path", .{});
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
defer allocator.free(path);
|
defer allocator.free(path);
|
||||||
|
|
||||||
const file = std.fs.openFileAbsolute(path, .{}) catch {
|
const file = std.fs.openFileAbsolute(path, .{}) catch {
|
||||||
log.warn("Failed to open timezone file: {s}", .{path});
|
warn("Failed to open timezone file: {s}", .{path});
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
defer file.close();
|
defer file.close();
|
||||||
|
|
||||||
const data = file.readToEndAlloc(allocator, 64 * 1024) catch {
|
const data = file.readToEndAlloc(allocator, 64 * 1024) catch {
|
||||||
log.warn("Failed to read timezone file: {s}", .{path});
|
warn("Failed to read timezone file: {s}", .{path});
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
defer allocator.free(data);
|
defer allocator.free(data);
|
||||||
|
|
@ -51,13 +59,13 @@ pub fn getLocalTimezone(allocator: Allocator) ?[]const u8 {
|
||||||
|
|
||||||
// Fall back to /etc/timezone
|
// Fall back to /etc/timezone
|
||||||
const file = std.fs.openFileAbsolute("/etc/timezone", .{}) catch {
|
const file = std.fs.openFileAbsolute("/etc/timezone", .{}) catch {
|
||||||
log.warn("No TZ env var and failed to open /etc/timezone", .{});
|
warn("No TZ env var and failed to open /etc/timezone", .{});
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
defer file.close();
|
defer file.close();
|
||||||
|
|
||||||
const content = file.readToEndAlloc(allocator, 256) catch {
|
const content = file.readToEndAlloc(allocator, 256) catch {
|
||||||
log.warn("Failed to read /etc/timezone", .{});
|
warn("Failed to read /etc/timezone", .{});
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -80,13 +88,13 @@ pub fn getLocalTimezone(allocator: Allocator) ?[]const u8 {
|
||||||
fn parseTzif(data: []const u8) ?i32 {
|
fn parseTzif(data: []const u8) ?i32 {
|
||||||
// TZif header is 44 bytes minimum
|
// TZif header is 44 bytes minimum
|
||||||
if (data.len < 44) {
|
if (data.len < 44) {
|
||||||
log.warn("TZif file too small: {d} bytes", .{data.len});
|
warn("TZif file too small: {d} bytes", .{data.len});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check magic number "TZif"
|
// Check magic number "TZif"
|
||||||
if (!std.mem.eql(u8, data[0..4], "TZif")) {
|
if (!std.mem.eql(u8, data[0..4], "TZif")) {
|
||||||
log.warn("Invalid TZif magic number", .{});
|
warn("Invalid TZif magic number", .{});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,7 +119,7 @@ fn parseTzif(data: []const u8) ?i32 {
|
||||||
const tzh_charcnt = std.mem.readInt(u32, data[40..44], .big);
|
const tzh_charcnt = std.mem.readInt(u32, data[40..44], .big);
|
||||||
|
|
||||||
if (tzh_typecnt == 0) {
|
if (tzh_typecnt == 0) {
|
||||||
log.warn("TZif file has no time types", .{});
|
warn("TZif file has no time types", .{});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,13 +138,13 @@ fn parseTzif(data: []const u8) ?i32 {
|
||||||
const v2_header_start = 44 + v1_data_size;
|
const v2_header_start = 44 + v1_data_size;
|
||||||
|
|
||||||
if (data.len < v2_header_start + 44) {
|
if (data.len < v2_header_start + 44) {
|
||||||
log.warn("TZif v2 file truncated", .{});
|
warn("TZif v2 file truncated", .{});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify v2 header magic
|
// Verify v2 header magic
|
||||||
if (!std.mem.eql(u8, data[v2_header_start..][0..4], "TZif")) {
|
if (!std.mem.eql(u8, data[v2_header_start..][0..4], "TZif")) {
|
||||||
log.warn("Invalid TZif v2 header magic", .{});
|
warn("Invalid TZif v2 header magic", .{});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,7 +153,7 @@ fn parseTzif(data: []const u8) ?i32 {
|
||||||
const v2_typecnt = std.mem.readInt(u32, data[v2_header_start + 36 ..][0..4], .big);
|
const v2_typecnt = std.mem.readInt(u32, data[v2_header_start + 36 ..][0..4], .big);
|
||||||
|
|
||||||
if (v2_typecnt == 0) {
|
if (v2_typecnt == 0) {
|
||||||
log.warn("TZif v2 has no time types", .{});
|
warn("TZif v2 has no time types", .{});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,7 +179,7 @@ fn parseTzif(data: []const u8) ?i32 {
|
||||||
/// Each ttinfo is 6 bytes: i32 offset, u8 is_dst, u8 abbr_idx
|
/// Each ttinfo is 6 bytes: i32 offset, u8 is_dst, u8 abbr_idx
|
||||||
fn readLastTtinfoOffset(data: []const u8, ttinfo_offset: usize, typecnt: u32) ?i32 {
|
fn readLastTtinfoOffset(data: []const u8, ttinfo_offset: usize, typecnt: u32) ?i32 {
|
||||||
if (data.len < ttinfo_offset + 6) {
|
if (data.len < ttinfo_offset + 6) {
|
||||||
log.warn("TZif file truncated at ttinfo", .{});
|
warn("TZif file truncated at ttinfo", .{});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,7 +211,7 @@ fn readLastTtinfoOffset(data: []const u8, ttinfo_offset: usize, typecnt: u32) ?i
|
||||||
|
|
||||||
// Sanity check
|
// Sanity check
|
||||||
if (offset < -14 * 3600 or offset > 14 * 3600) {
|
if (offset < -14 * 3600 or offset > 14 * 3600) {
|
||||||
log.warn("TZif offset out of range: {d}", .{offset});
|
warn("TZif offset out of range: {d}", .{offset});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,51 +10,93 @@ pub fn main() !u8 {
|
||||||
defer _ = gpa.deinit();
|
defer _ = gpa.deinit();
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
var stdout_buf: [4096]u8 = undefined;
|
||||||
|
var stderr_buf: [4096]u8 = undefined;
|
||||||
|
var stdout_writer = std.fs.File.stdout().writer(&stdout_buf);
|
||||||
|
var stderr_writer = std.fs.File.stderr().writer(&stderr_buf);
|
||||||
|
const stdout = &stdout_writer.interface;
|
||||||
|
const stderr = &stderr_writer.interface;
|
||||||
|
|
||||||
const args = try std.process.argsAlloc(allocator);
|
const args = try std.process.argsAlloc(allocator);
|
||||||
defer std.process.argsFree(allocator, args);
|
defer std.process.argsFree(allocator, args);
|
||||||
|
|
||||||
if (args.len != 3) {
|
if (args.len < 3) {
|
||||||
std.debug.print("Usage: {s} <deploy-output.json> <ask-states.json>\n", .{args[0]});
|
try stderr.print("Usage: {s} <deploy-output.json> <ask-states.json> [--profile <profile>] [--region <region>]\n", .{args[0]});
|
||||||
|
try stderr.flush();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse optional arguments
|
||||||
|
var profile: ?[]const u8 = null;
|
||||||
|
var region_override: ?[]const u8 = null;
|
||||||
|
var i: usize = 3;
|
||||||
|
while (i < args.len) : (i += 1) {
|
||||||
|
if (std.mem.eql(u8, args[i], "--profile") and i + 1 < args.len) {
|
||||||
|
profile = args[i + 1];
|
||||||
|
i += 1;
|
||||||
|
} else if (std.mem.eql(u8, args[i], "--region") and i + 1 < args.len) {
|
||||||
|
region_override = args[i + 1];
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Read deploy output to get function name and region
|
// Read deploy output to get function name and region
|
||||||
const deploy_output = std.fs.cwd().readFileAlloc(allocator, args[1], 1024 * 1024) catch |err| {
|
const deploy_output = std.fs.cwd().readFileAlloc(allocator, args[1], 1024 * 1024) catch |err| {
|
||||||
std.debug.print("Failed to read deploy output '{s}': {}\n", .{ args[1], err });
|
try stderr.print("Failed to read deploy output '{s}': {}\n", .{ args[1], err });
|
||||||
|
try stderr.flush();
|
||||||
return 1;
|
return 1;
|
||||||
};
|
};
|
||||||
defer allocator.free(deploy_output);
|
defer allocator.free(deploy_output);
|
||||||
|
|
||||||
const deploy_parsed = json.parseFromSlice(json.Value, allocator, deploy_output, .{}) catch |err| {
|
const deploy_parsed = json.parseFromSlice(json.Value, allocator, deploy_output, .{}) catch |err| {
|
||||||
std.debug.print("Failed to parse deploy output: {}\n", .{err});
|
try stderr.print("Failed to parse deploy output: {}\n", .{err});
|
||||||
|
try stderr.flush();
|
||||||
return 1;
|
return 1;
|
||||||
};
|
};
|
||||||
defer deploy_parsed.deinit();
|
defer deploy_parsed.deinit();
|
||||||
|
|
||||||
const function_name = deploy_parsed.value.object.get("function_name").?.string;
|
const function_name = deploy_parsed.value.object.get("function_name").?.string;
|
||||||
const region = deploy_parsed.value.object.get("region").?.string;
|
const region = region_override orelse deploy_parsed.value.object.get("region").?.string;
|
||||||
|
|
||||||
// Read ask-states.json to get skill ID
|
// Read ask-states.json to get skill ID
|
||||||
const ask_states = std.fs.cwd().readFileAlloc(allocator, args[2], 1024 * 1024) catch |err| {
|
const ask_states = std.fs.cwd().readFileAlloc(allocator, args[2], 1024 * 1024) catch |err| {
|
||||||
std.debug.print("Failed to read ask-states.json '{s}': {}\n", .{ args[2], err });
|
try stderr.print("Failed to read ask-states.json '{s}': {}\n", .{ args[2], err });
|
||||||
|
try stderr.flush();
|
||||||
return 1;
|
return 1;
|
||||||
};
|
};
|
||||||
defer allocator.free(ask_states);
|
defer allocator.free(ask_states);
|
||||||
|
|
||||||
const ask_parsed = json.parseFromSlice(json.Value, allocator, ask_states, .{}) catch |err| {
|
const ask_parsed = json.parseFromSlice(json.Value, allocator, ask_states, .{}) catch |err| {
|
||||||
std.debug.print("Failed to parse ask-states.json: {}\n", .{err});
|
try stderr.print("Failed to parse ask-states.json: {}\n", .{err});
|
||||||
|
try stderr.flush();
|
||||||
return 1;
|
return 1;
|
||||||
};
|
};
|
||||||
defer ask_parsed.deinit();
|
defer ask_parsed.deinit();
|
||||||
|
|
||||||
const skill_id = ask_parsed.value.object.get("profiles").?.object.get("default").?.object.get("skillId").?.string;
|
// Use profile name to look up skill ID, defaulting to "default"
|
||||||
|
const profile_name = profile orelse "default";
|
||||||
|
const profiles_obj = ask_parsed.value.object.get("profiles") orelse {
|
||||||
|
try stderr.print("No 'profiles' field in ask-states.json\n", .{});
|
||||||
|
try stderr.flush();
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
const profile_obj = profiles_obj.object.get(profile_name) orelse {
|
||||||
|
try stderr.print("Profile '{s}' not found in ask-states.json\n", .{profile_name});
|
||||||
|
try stderr.flush();
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
const skill_id = profile_obj.object.get("skillId").?.string;
|
||||||
|
|
||||||
std.debug.print("Adding Alexa permission for skill {s} to function {s} in {s}\n", .{ skill_id, function_name, region });
|
try stdout.print("Adding Alexa permission for skill {s} to function {s} in {s}", .{ skill_id, function_name, region });
|
||||||
|
if (profile) |p| try stdout.print(" (profile: {s})", .{p});
|
||||||
|
try stdout.print("\n", .{});
|
||||||
|
try stdout.flush();
|
||||||
|
|
||||||
// Build statement ID from skill ID (use last 12 chars to keep it short but unique)
|
// Build statement ID from skill ID (use last 12 chars to keep it short but unique)
|
||||||
var statement_id_buf: [64]u8 = undefined;
|
var statement_id_buf: [64]u8 = undefined;
|
||||||
const statement_id = std.fmt.bufPrint(&statement_id_buf, "alexa-skill-{s}", .{skill_id[skill_id.len - 12 ..]}) catch {
|
const statement_id = std.fmt.bufPrint(&statement_id_buf, "alexa-skill-{s}", .{skill_id[skill_id.len - 12 ..]}) catch {
|
||||||
std.debug.print("Failed to build statement ID\n", .{});
|
try stderr.print("Failed to build statement ID\n", .{});
|
||||||
|
try stderr.flush();
|
||||||
return 1;
|
return 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -72,6 +114,12 @@ pub fn main() !u8 {
|
||||||
.client = client,
|
.client = client,
|
||||||
.region = region,
|
.region = region,
|
||||||
.diagnostics = &diagnostics,
|
.diagnostics = &diagnostics,
|
||||||
|
.credential_options = .{
|
||||||
|
.profile = .{
|
||||||
|
.profile_name = profile,
|
||||||
|
.prefer_profile_from_file = profile != null,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add permission with skill ID as event source token
|
// Add permission with skill ID as event source token
|
||||||
|
|
@ -88,14 +136,17 @@ pub fn main() !u8 {
|
||||||
|
|
||||||
// 409 Conflict means permission already exists - that's fine
|
// 409 Conflict means permission already exists - that's fine
|
||||||
if (diagnostics.response_status == .conflict) {
|
if (diagnostics.response_status == .conflict) {
|
||||||
std.debug.print("Permission already exists for skill: {s}\n", .{skill_id});
|
try stdout.print("Permission already exists for skill: {s}\n", .{skill_id});
|
||||||
|
try stdout.flush();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
std.debug.print("AddPermission failed: {} (HTTP {})\n", .{ err, diagnostics.response_status });
|
try stderr.print("AddPermission failed: {} (HTTP {})\n", .{ err, diagnostics.response_status });
|
||||||
|
try stderr.flush();
|
||||||
return 1;
|
return 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
std.debug.print("Added Alexa permission for skill: {s}\n", .{skill_id});
|
try stdout.print("Added Alexa permission for skill: {s}\n", .{skill_id});
|
||||||
|
try stdout.flush();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue