From 11093140aac1d44db82042a97945efced8fa4ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Motiejus=20Jak=C5=A1tys?= Date: Mon, 27 Oct 2025 15:36:27 +0000 Subject: [PATCH] gcloud-wrapper: experiments for zig 0.15 --- pkgs/gcloud-wrapped/build.zig | 39 +++++ pkgs/gcloud-wrapped/default.nix | 6 +- pkgs/gcloud-wrapped/go.mod | 3 - pkgs/gcloud-wrapped/main.go | 139 ----------------- pkgs/gcloud-wrapped/main.zig | 268 ++++++++++++++++++++++++++++++++ 5 files changed, 310 insertions(+), 145 deletions(-) create mode 100644 pkgs/gcloud-wrapped/build.zig delete mode 100644 pkgs/gcloud-wrapped/go.mod delete mode 100644 pkgs/gcloud-wrapped/main.go create mode 100644 pkgs/gcloud-wrapped/main.zig diff --git a/pkgs/gcloud-wrapped/build.zig b/pkgs/gcloud-wrapped/build.zig new file mode 100644 index 0000000..a19e30e --- /dev/null +++ b/pkgs/gcloud-wrapped/build.zig @@ -0,0 +1,39 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "gcloud-wrapper", + .root_module = b.createModule(.{ + .root_source_file = b.path("main.zig"), + .target = target, + .optimize = optimize, + }), + }); + + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const exe_unit_tests = b.addTest(.{ + .root_module = b.createModule(.{ + .root_source_file = b.path("main.zig"), + .target = target, + .optimize = optimize, + }), + }); + + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_exe_unit_tests.step); +} diff --git a/pkgs/gcloud-wrapped/default.nix b/pkgs/gcloud-wrapped/default.nix index 5a5a10c..4f67bca 100644 --- a/pkgs/gcloud-wrapped/default.nix +++ b/pkgs/gcloud-wrapped/default.nix @@ -1,9 +1,9 @@ -{ buildGoModule, pkgs }: +{ pkgs }: let - gcloud-wrapper = buildGoModule { + gcloud-wrapper = pkgs.stdenv.mkDerivation { name = "gcloud-wrapper"; src = ./.; - vendorHash = null; + nativeBuildInputs = [ pkgs.pkgs-unstable.zig_0_15.hook ]; }; in pkgs.symlinkJoin { diff --git a/pkgs/gcloud-wrapped/go.mod b/pkgs/gcloud-wrapped/go.mod deleted file mode 100644 index 34f91bc..0000000 --- a/pkgs/gcloud-wrapped/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.jakstys.lt/motiejus/config/pkgs/gcloud-wrapper - -go 1.23 diff --git a/pkgs/gcloud-wrapped/main.go b/pkgs/gcloud-wrapped/main.go deleted file mode 100644 index acf3f1e..0000000 --- a/pkgs/gcloud-wrapped/main.go +++ /dev/null @@ -1,139 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "os" - "os/exec" - "path/filepath" - "time" -) - -const ( - cachePath = ".config/gcloud/config-helper-cache.json" - cacheThreshold = 1 * time.Second -) - -type credentialCache struct { - Credential struct { - TokenExpiry string `json:"token_expiry"` - } `json:"credential"` -} - -func getCachePath() (string, error) { - home, err := os.UserHomeDir() - if err != nil { - return "", err - } - return filepath.Join(home, cachePath), nil -} - -func argsMatch(args []string) bool { - return len(args) == 4 && - args[0] == "config" && - args[1] == "config-helper" && - args[2] == "--format" && - args[3] == "json" -} - -func parseISO8601(s string) (time.Time, error) { - return time.Parse(time.RFC3339, s) -} - -func execGcloud(args []string) { - argv := make([]string, len(args)) - argv[0] = "gcloud-wrapped" - copy(argv[1:], args[1:]) - - cmd := exec.Command(argv[0], argv[1:]...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - - if err := cmd.Run(); err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - os.Exit(exitErr.ExitCode()) - } - os.Exit(1) - } - os.Exit(0) -} - -func runGcloudAndCache(cachePath string) error { - cmd := exec.Command("gcloud-wrapped", "config", "config-helper", "--format", "json") - output, err := cmd.CombinedOutput() - - if err != nil { - os.Remove(cachePath) - os.Stderr.Write(output) - if exitErr, ok := err.(*exec.ExitError); ok { - os.Exit(exitErr.ExitCode()) - } - os.Exit(1) - } - - if err := os.MkdirAll(filepath.Dir(cachePath), 0755); err != nil { - return err - } - - if err := os.WriteFile(cachePath, output, 0600); err != nil { - return err - } - - os.Stdout.Write(output) - return nil -} - -func main() { - args := os.Args[1:] - - if !argsMatch(args) { - execGcloud(os.Args) - } - - cachePath, err := getCachePath() - if err != nil { - fmt.Fprintf(os.Stderr, "failed to get cache path: %v\n", err) - os.Exit(1) - } - - cacheData, err := os.ReadFile(cachePath) - if err != nil { - if os.IsNotExist(err) { - if err := runGcloudAndCache(cachePath); err != nil { - fmt.Fprintf(os.Stderr, "failed to run gcloud: %v\n", err) - os.Exit(1) - } - return - } - fmt.Fprintf(os.Stderr, "failed to read cache: %v\n", err) - os.Exit(1) - } - - var cache credentialCache - if err := json.Unmarshal(cacheData, &cache); err != nil { - if err := runGcloudAndCache(cachePath); err != nil { - fmt.Fprintf(os.Stderr, "failed to run gcloud: %v\n", err) - os.Exit(1) - } - return - } - - expiry, err := parseISO8601(cache.Credential.TokenExpiry) - if err != nil { - if err := runGcloudAndCache(cachePath); err != nil { - fmt.Fprintf(os.Stderr, "failed to run gcloud: %v\n", err) - os.Exit(1) - } - return - } - - if time.Until(expiry) > cacheThreshold { - os.Stdout.Write(cacheData) - } else { - if err := runGcloudAndCache(cachePath); err != nil { - fmt.Fprintf(os.Stderr, "failed to run gcloud: %v\n", err) - os.Exit(1) - } - } -} diff --git a/pkgs/gcloud-wrapped/main.zig b/pkgs/gcloud-wrapped/main.zig new file mode 100644 index 0000000..4dfe20e --- /dev/null +++ b/pkgs/gcloud-wrapped/main.zig @@ -0,0 +1,268 @@ +const std = @import("std"); + +const cache_path = ".config/gcloud/config-helper-cache.json"; +const cache_threshold_ns: i128 = std.time.ns_per_s; + +const CredentialCache = struct { + credential: struct { + token_expiry: []const u8, + }, +}; + +fn getCachePath(allocator: std.mem.Allocator) ![]u8 { + const home = std.posix.getenv("HOME") orelse return error.NoHomeDir; + return std.fs.path.join(allocator, &.{ home, cache_path }); +} + +fn argsMatch(args: []const []const u8) bool { + if (args.len != 4) return false; + if (!std.mem.eql(u8, args[0], "config")) return false; + if (!std.mem.eql(u8, args[1], "config-helper")) return false; + if (!std.mem.eql(u8, args[2], "--format")) return false; + if (!std.mem.eql(u8, args[3], "json")) return false; + return true; +} + +fn parseISO8601(s: []const u8) !i64 { + var year: u16 = 0; + var month: u8 = 0; + var day: u8 = 0; + var hour: u8 = 0; + var minute: u8 = 0; + var second: u8 = 0; + + if (s.len < 19) return error.InvalidFormat; + + year = try std.fmt.parseInt(u16, s[0..4], 10); + if (s[4] != '-') return error.InvalidFormat; + month = try std.fmt.parseInt(u8, s[5..7], 10); + if (s[7] != '-') return error.InvalidFormat; + day = try std.fmt.parseInt(u8, s[8..10], 10); + if (s[10] != 'T') return error.InvalidFormat; + hour = try std.fmt.parseInt(u8, s[11..13], 10); + if (s[13] != ':') return error.InvalidFormat; + minute = try std.fmt.parseInt(u8, s[14..16], 10); + if (s[16] != ':') return error.InvalidFormat; + second = try std.fmt.parseInt(u8, s[17..19], 10); + + var days_since_epoch: i64 = 0; + var y: u16 = std.time.epoch.epoch_year; + while (y < year) : (y += 1) { + days_since_epoch += std.time.epoch.getDaysInYear(y); + } + + const days_in_months = [12]u8{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + var m: u8 = 1; + while (m < month) : (m += 1) { + days_since_epoch += days_in_months[m - 1]; + if (m == 2 and std.time.epoch.isLeapYear(year)) { + days_since_epoch += 1; + } + } + + days_since_epoch += day - 1; + + const seconds_since_epoch = days_since_epoch * std.time.epoch.secs_per_day + + @as(i64, hour) * 3600 + + @as(i64, minute) * 60 + + @as(i64, second); + + return seconds_since_epoch; +} + +fn execGcloud(allocator: std.mem.Allocator, args: []const []const u8, gcloud_path: []const u8) !noreturn { + const argv = try allocator.alloc([]const u8, args.len); + defer allocator.free(argv); + + argv[0] = gcloud_path; + @memcpy(argv[1..], args[1..]); + + const result = std.process.execv(allocator, argv); + std.debug.print("exec failed: {}\n", .{result}); + std.process.exit(1); +} + +fn runGcloudAndCache(allocator: std.mem.Allocator, cache_file_path: []const u8, gcloud_path: []const u8, stdout: std.fs.File) !void { + const argv = [_][]const u8{ gcloud_path, "config", "config-helper", "--format", "json" }; + + const result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &argv, + }); + defer allocator.free(result.stdout); + defer allocator.free(result.stderr); + + if (result.term.Exited != 0) { + std.fs.deleteFileAbsolute(cache_file_path) catch {}; + try std.fs.File.stderr().writeAll(result.stderr); + std.process.exit(result.term.Exited); + } + + const dir_path = std.fs.path.dirname(cache_file_path) orelse return error.InvalidPath; + try std.fs.cwd().makePath(dir_path); + + const file = try std.fs.createFileAbsolute(cache_file_path, .{ .mode = 0o600 }); + defer file.close(); + try file.writeAll(result.stdout); + + try stdout.writeAll(result.stdout); +} + +fn runMain(allocator: std.mem.Allocator, _: []const []const u8, gcloud_path: []const u8, cache_dir: []const u8, stdout: std.fs.File, now_ts: i64) !void { + const cache_file_path = try std.fs.path.join(allocator, &.{ cache_dir, "config-helper-cache.json" }); + defer allocator.free(cache_file_path); + + const cache_file = std.fs.openFileAbsolute(cache_file_path, .{}) catch |err| { + if (err == error.FileNotFound) { + try runGcloudAndCache(allocator, cache_file_path, gcloud_path, stdout); + return; + } + std.debug.print("failed to open cache: {}\n", .{err}); + std.process.exit(1); + }; + defer cache_file.close(); + + const cache_data = cache_file.readToEndAlloc(allocator, 1024 * 1024) catch |err| { + std.debug.print("failed to read cache file: {}\n", .{err}); + std.process.exit(1); + }; + defer allocator.free(cache_data); + + const parsed = std.json.parseFromSlice(CredentialCache, allocator, cache_data, .{}) catch { + try runGcloudAndCache(allocator, cache_file_path, gcloud_path, stdout); + return; + }; + defer parsed.deinit(); + + const expiry_ts = parseISO8601(parsed.value.credential.token_expiry) catch { + try runGcloudAndCache(allocator, cache_file_path, gcloud_path, stdout); + return; + }; + + const until_expiry_ns = (expiry_ts - now_ts) * std.time.ns_per_s; + + if (until_expiry_ns > cache_threshold_ns) { + try stdout.writeAll(cache_data); + } else { + try runGcloudAndCache(allocator, cache_file_path, gcloud_path, stdout); + } +} + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + const args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + + const user_args = args[1..]; + + if (!argsMatch(user_args)) { + try execGcloud(allocator, args, "gcloud-wrapped"); + } + + const cache_dir_path = blk: { + const home = std.posix.getenv("HOME") orelse return error.NoHomeDir; + break :blk try std.fs.path.join(allocator, &.{ home, ".config/gcloud" }); + }; + defer allocator.free(cache_dir_path); + + const now_ts: i64 = @intCast(@divFloor(std.time.nanoTimestamp(), std.time.ns_per_s)); + try runMain(allocator, user_args, "gcloud-wrapped", cache_dir_path, std.fs.File.stdout(), now_ts); +} + +test "argsMatch with valid args" { + const args = [_][]const u8{ "config", "config-helper", "--format", "json" }; + try std.testing.expect(argsMatch(&args)); +} + +test "argsMatch with invalid args - wrong length" { + const args = [_][]const u8{ "config", "config-helper" }; + try std.testing.expect(!argsMatch(&args)); +} + +test "argsMatch with invalid args - wrong command" { + const args = [_][]const u8{ "compute", "config-helper", "--format", "json" }; + try std.testing.expect(!argsMatch(&args)); +} + +test "parseISO8601 valid timestamp" { + const ts = try parseISO8601("2025-01-15T10:30:45Z"); + try std.testing.expect(ts > 0); +} + +test "parseISO8601 invalid format" { + const result = parseISO8601("invalid"); + try std.testing.expectError(error.InvalidFormat, result); +} + +test "integration: cache valid token" { + const allocator = std.testing.allocator; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const cache_dir_path = try tmp_dir.dir.realpathAlloc(allocator, "."); + defer allocator.free(cache_dir_path); + + const gcloud_script_path = try std.fs.path.join(allocator, &.{ cache_dir_path, "mock-gcloud" }); + defer allocator.free(gcloud_script_path); + + const far_future = "2099-12-31T23:59:59Z"; + const mock_response = try std.fmt.allocPrint(allocator, + \\{{"credential": {{"token_expiry": "{s}"}}}} + , .{far_future}); + defer allocator.free(mock_response); + + const script_content = try std.fmt.allocPrint(allocator, + \\#!/bin/sh + \\echo '{s}' + \\ + , .{mock_response}); + defer allocator.free(script_content); + + { + const script_file = try tmp_dir.dir.createFile("mock-gcloud", .{ .mode = 0o755 }); + defer script_file.close(); + try script_file.writeAll(script_content); + } + + const stdout_path = try std.fs.path.join(allocator, &.{ cache_dir_path, "stdout.txt" }); + defer allocator.free(stdout_path); + + const stdout_file = try tmp_dir.dir.createFile("stdout.txt", .{ .read = true }); + defer stdout_file.close(); + + const user_args = [_][]const u8{ "config", "config-helper", "--format", "json" }; + const now_ts = try parseISO8601("2025-01-01T00:00:00Z"); + + try runMain(allocator, &user_args, gcloud_script_path, cache_dir_path, stdout_file, now_ts); + + const cache_file_path = try std.fs.path.join(allocator, &.{ cache_dir_path, "config-helper-cache.json" }); + defer allocator.free(cache_file_path); + + const cache_file_check = try std.fs.openFileAbsolute(cache_file_path, .{}); + defer cache_file_check.close(); + const cached_data = try cache_file_check.readToEndAlloc(allocator, 1024 * 1024); + defer allocator.free(cached_data); + + try std.testing.expect(std.mem.indexOf(u8, cached_data, far_future) != null); + + try stdout_file.seekTo(0); + const stdout_data = try stdout_file.readToEndAlloc(allocator, 1024); + defer allocator.free(stdout_data); + + try std.testing.expect(std.mem.indexOf(u8, stdout_data, far_future) != null); + + const second_stdout_file = try tmp_dir.dir.createFile("stdout2.txt", .{ .read = true }); + defer second_stdout_file.close(); + + try runMain(allocator, &user_args, gcloud_script_path, cache_dir_path, second_stdout_file, now_ts); + + try second_stdout_file.seekTo(0); + const second_stdout_data = try second_stdout_file.readToEndAlloc(allocator, 1024); + defer allocator.free(second_stdout_data); + + try std.testing.expectEqualStrings(stdout_data, second_stdout_data); +}