From ffca4cbf52832ec256c8b50c9e2808bfa3369e5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Motiejus=20Jak=C5=A1tys?= Date: Tue, 21 Oct 2025 11:15:01 +0000 Subject: [PATCH] add gcloud-wrapper --- flake.nix | 1 + pkgs/gcloud-wrapper/default.nix | 6 ++ pkgs/gcloud-wrapper/go.mod | 3 + pkgs/gcloud-wrapper/main.go | 139 ++++++++++++++++++++++++++++++++ shared/work/default.nix | 19 ++++- 5 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 pkgs/gcloud-wrapper/default.nix create mode 100644 pkgs/gcloud-wrapper/go.mod create mode 100644 pkgs/gcloud-wrapper/main.go diff --git a/flake.nix b/flake.nix index 10688ac..b87ca74 100644 --- a/flake.nix +++ b/flake.nix @@ -106,6 +106,7 @@ tmuxbash = super.callPackage ./pkgs/tmuxbash.nix { }; vanta-agent = super.callPackage ./pkgs/vanta-agent.nix { }; chronoctl = super.callPackage ./pkgs/chronoctl.nix { }; + gcloud-wrapper = super.callPackage ./pkgs/gcloud-wrapper { }; pkgs-unstable = import nixpkgs-unstable { inherit (super) system; diff --git a/pkgs/gcloud-wrapper/default.nix b/pkgs/gcloud-wrapper/default.nix new file mode 100644 index 0000000..67f727c --- /dev/null +++ b/pkgs/gcloud-wrapper/default.nix @@ -0,0 +1,6 @@ +{ buildGoModule }: +buildGoModule { + name = "gcloud-wrapper"; + src = ./.; + vendorHash = null; +} diff --git a/pkgs/gcloud-wrapper/go.mod b/pkgs/gcloud-wrapper/go.mod new file mode 100644 index 0000000..34f91bc --- /dev/null +++ b/pkgs/gcloud-wrapper/go.mod @@ -0,0 +1,3 @@ +module git.jakstys.lt/motiejus/config/pkgs/gcloud-wrapper + +go 1.23 diff --git a/pkgs/gcloud-wrapper/main.go b/pkgs/gcloud-wrapper/main.go new file mode 100644 index 0000000..315cabd --- /dev/null +++ b/pkgs/gcloud-wrapper/main.go @@ -0,0 +1,139 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "time" +) + +const ( + cachePath = ".config/gcloud/config-helper-cache.json" + cacheThreshold = 10 * time.Minute +) + +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/shared/work/default.nix b/shared/work/default.nix index 312c328..5d5d513 100644 --- a/shared/work/default.nix +++ b/shared/work/default.nix @@ -1,4 +1,21 @@ { config, pkgs, ... }: +let + gcloud-wrapped = pkgs.symlinkJoin { + name = "google-cloud-sdk-wrapped"; + paths = [ pkgs.google-cloud-sdk ]; + nativeBuildInputs = [ pkgs.makeWrapper ]; + postBuild = '' + # Remove the original gcloud symlink + rm $out/bin/gcloud + + # Create a shell wrapper called gcloud-wrapped that executes the real gcloud + makeWrapper ${pkgs.google-cloud-sdk}/bin/gcloud $out/bin/gcloud-wrapped + + # Replace gcloud with our caching wrapper + ln -s ${pkgs.gcloud-wrapper}/bin/gcloud-wrapper $out/bin/gcloud + ''; + }; +in { mj.base.users.email = null; @@ -19,7 +36,7 @@ github-cli claude-code docker-compose - google-cloud-sdk + gcloud-wrapped kubectl-node-shell liburing.dev