From 5a64b10b251dcb72ae15855fd096a3672a123629 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Fri, 23 Oct 2026 21:57:28 +1100 Subject: [PATCH 1/3] Set VAULT_FORMAT in vctx from the vault command --- docs/vctx.md | 25 ++++++++++++++--- vctx/main.go | 66 ++++++++++++++++++++++++++++++++++++++------ vctx/main_test.go | 70 +++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 139 insertions(+), 22 deletions(-) diff --git a/docs/vctx.md b/docs/vctx.md index d9ec725..66afc25 100644 --- a/docs/vctx.md +++ b/docs/vctx.md @@ -8,7 +8,7 @@ invocation only, and execs `vault` with the remaining arguments. ## Synopsis ``` -vctx --context +vctx --context [--format ] vctx version ``` @@ -26,6 +26,8 @@ vctx --context sydney kv put kv/foo/bar secret=baz - `VAULT_TOKEN` = the cached token - `VAULT_NAMESPACE` = the context's namespace (only if non-empty; falls back to the namespace recorded in the cached token) + - `VAULT_FORMAT` = `--format` if given, else picked from the command (see + [Output format](#output-format)) 4. `exec`s the `vault` binary (found on `PATH`) with everything after the context. Because it replaces the process, `vault`'s exit status, signals and TTY behaviour pass straight through. @@ -33,10 +35,25 @@ vctx --context sydney kv put kv/foo/bar secret=baz The ambient `VAULT_ADDR` / `VAULT_TOKEN` in your shell are ignored for the call — `vctx` always targets the chosen context. -## The `--context` flag +## Output format -`--context` is the only flag `vctx` consumes; it must come **before** the vault -command. Everything from the first non-flag argument onward is handed to `vault` +Without `--format`, `vctx` sets `VAULT_FORMAT` from the vault command: + +| Command | `VAULT_FORMAT` | +|--------------------------------------------|----------------| +| `kv list ...` | `table` | +| `kv get ...` | `yaml` | +| any path containing `/creds/` (read/write) | `yaml` | +| anything else | unset (vault default) | + +It is left alone when `VAULT_FORMAT` is already set in the environment or the +vault args include `-format` or `-field`. `--format` (`table`, `json`, `yaml`, +`pretty`, `raw`) always sets it. + +## Flags + +`--context` and `--format` are the only flags `vctx` consumes; they must come +**before** the vault command. Everything from the first non-flag argument onward is handed to `vault` untouched, so vault's own flags work normally: ```bash diff --git a/vctx/main.go b/vctx/main.go index 0709e3d..e6358da 100644 --- a/vctx/main.go +++ b/vctx/main.go @@ -12,6 +12,7 @@ import ( "fmt" "os" "os/exec" + "strings" "syscall" "git.unkin.net/unkin/vault-tools/shared" @@ -32,15 +33,16 @@ func main() { } func newRootCmd() *cobra.Command { - var context string + var context, format string root := &cobra.Command{ Use: "vctx --context ", Short: "Run the vault CLI against a named context.", Long: "vctx resolves a context from ~/.config/vcm (shared with vcm), loads its\n" + "cached token from ~/.cache/vcm/, sets VAULT_ADDR, VAULT_TOKEN and\n" + - "VAULT_NAMESPACE for this invocation only, and execs `vault` with the\n" + - "remaining arguments.\n\n" + + "VAULT_NAMESPACE (and VAULT_FORMAT: yaml for kv get and */creds/*, table\n" + + "for kv list, unless already chosen) for this invocation only, and execs\n" + + "`vault` with the remaining arguments.\n\n" + "Example: vctx --context sydney kv put kv/foo/bar secret=baz", // Everything from the first non-flag argument on is the vault command // line; SetInterspersed(false) below stops flag parsing there so flags @@ -48,7 +50,7 @@ func newRootCmd() *cobra.Command { Args: cobra.ArbitraryArgs, SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - return runVctx(context, args) + return runVctx(context, format, args) }, } @@ -64,6 +66,9 @@ func newRootCmd() *cobra.Command { } return cfg.ContextNames(), cobra.ShellCompDirectiveNoFileComp }) + root.Flags().StringVar(&format, "format", "", "VAULT_FORMAT for this call (default: picked from the vault command)") + _ = root.RegisterFlagCompletionFunc("format", cobra.FixedCompletions( + []string{"table", "json", "yaml", "pretty", "raw"}, cobra.ShellCompDirectiveNoFileComp)) root.AddCommand(&cobra.Command{ Use: "version", @@ -78,7 +83,7 @@ func newRootCmd() *cobra.Command { // runVctx validates inputs, builds the vault invocation for the context, and // execs it (replacing this process). -func runVctx(context string, args []string) error { +func runVctx(context, format string, args []string) error { if context == "" { return fmt.Errorf("--context is required") } @@ -86,7 +91,7 @@ func runVctx(context string, args []string) error { return fmt.Errorf("no vault arguments given (e.g. vctx --context %s kv list kv/)", context) } - bin, argv, env, err := buildInvocation(context, args, os.Environ()) + bin, argv, env, err := buildInvocation(context, format, args, os.Environ()) if err != nil { return err } @@ -102,8 +107,9 @@ func runVctx(context string, args []string) error { // binary, and returns the binary path, argv (vault + args) and the environment // to exec with. baseEnv is the starting environment (normally os.Environ()); // the Vault settings are appended so they override any ambient values (exec -// semantics: the last assignment of a variable wins). -func buildInvocation(context string, args, baseEnv []string) (bin string, argv []string, env []string, err error) { +// semantics: the last assignment of a variable wins). format, when non-empty, +// is set as VAULT_FORMAT; otherwise autoFormat picks one. +func buildInvocation(context, format string, args, baseEnv []string) (bin string, argv []string, env []string, err error) { cfg, err := shared.Load() if err != nil { return "", nil, nil, err @@ -122,7 +128,7 @@ func buildInvocation(context string, args, baseEnv []string) (bin string, argv [ return "", nil, nil, fmt.Errorf("vault CLI not found in PATH: %w", err) } - env = make([]string, len(baseEnv), len(baseEnv)+3) + env = make([]string, len(baseEnv), len(baseEnv)+4) copy(env, baseEnv) env = append(env, "VAULT_ADDR="+rc.Address, @@ -138,6 +144,48 @@ func buildInvocation(context string, args, baseEnv []string) (bin string, argv [ env = append(env, "VAULT_NAMESPACE="+ns) } + if format == "" { + format = autoFormat(args, baseEnv) + } + if format != "" { + env = append(env, "VAULT_FORMAT="+format) + } + argv = append([]string{vaultBin}, args...) return vaultBin, argv, env, nil } + +// autoFormat picks VAULT_FORMAT from the vault command line: yaml for +// `kv get` and dynamic creds (`*/creds/*`), table for `kv list`, else "". +// It returns "" when the caller already chose (ambient VAULT_FORMAT, or a +// -format/-field flag, since vault formats -field values in non-table modes). +func autoFormat(args, baseEnv []string) string { + for _, e := range baseEnv { + if strings.HasPrefix(e, "VAULT_FORMAT=") && e != "VAULT_FORMAT=" { + return "" + } + } + var pos []string + for _, a := range args { + name, _, _ := strings.Cut(strings.TrimLeft(a, "-"), "=") + switch { + case !strings.HasPrefix(a, "-"): + pos = append(pos, a) + case name == "format" || name == "field": + return "" + } + } + isKV := func(sub string) bool { return len(pos) >= 2 && pos[0] == "kv" && pos[1] == sub } + switch { + case isKV("list"): + return "table" + case isKV("get"): + return "yaml" + } + for _, p := range pos { + if strings.Contains(p, "/creds/") { + return "yaml" + } + } + return "" +} diff --git a/vctx/main_test.go b/vctx/main_test.go index 4451777..f8612ea 100644 --- a/vctx/main_test.go +++ b/vctx/main_test.go @@ -74,7 +74,7 @@ func TestBuildInvocationArgsAndEnvOverride(t *testing.T) { baseEnv := []string{"HOME=/home/x", "VAULT_ADDR=ambient", "VAULT_TOKEN=ambient"} args := []string{"kv", "get", "-field=foo", "secret/x"} - bin, argv, env, err := buildInvocation("sydney", args, baseEnv) + bin, argv, env, err := buildInvocation("sydney", "", args, baseEnv) if err != nil { t.Fatalf("buildInvocation: %v", err) } @@ -105,7 +105,7 @@ func TestBuildInvocationNamespaceFromContext(t *testing.T) { setupVctx(t, vctxConfig, map[string]shared.Token{ "staging/sydney": {Address: "https://vault-staging.syd1.au.unkin.net", Namespace: "staging", Token: "s.X"}, }) - _, _, env, err := buildInvocation("staging/sydney", []string{"token", "lookup"}, nil) + _, _, env, err := buildInvocation("staging/sydney", "", []string{"token", "lookup"}, nil) if err != nil { t.Fatalf("buildInvocation: %v", err) } @@ -120,7 +120,7 @@ func TestBuildInvocationNamespaceFallsBackToToken(t *testing.T) { setupVctx(t, vctxConfig, map[string]shared.Token{ "sydney": {Address: "https://vault.syd1.au.unkin.net", Namespace: "from-token", Token: "s.X"}, }) - _, _, env, err := buildInvocation("sydney", []string{"status"}, nil) + _, _, env, err := buildInvocation("sydney", "", []string{"status"}, nil) if err != nil { t.Fatalf("buildInvocation: %v", err) } @@ -131,7 +131,7 @@ func TestBuildInvocationNamespaceFallsBackToToken(t *testing.T) { func TestBuildInvocationMissingToken(t *testing.T) { setupVctx(t, vctxConfig, nil) // no cached tokens - _, _, _, err := buildInvocation("sydney", []string{"status"}, nil) + _, _, _, err := buildInvocation("sydney", "", []string{"status"}, nil) if err == nil { t.Fatal("expected error for missing cached token") } @@ -142,16 +142,16 @@ func TestBuildInvocationMissingToken(t *testing.T) { func TestBuildInvocationUnknownContext(t *testing.T) { setupVctx(t, vctxConfig, nil) - if _, _, _, err := buildInvocation("nope", []string{"status"}, nil); err == nil { + if _, _, _, err := buildInvocation("nope", "", []string{"status"}, nil); err == nil { t.Error("expected error for unknown context") } } func TestRunVctxValidation(t *testing.T) { - if err := runVctx("", []string{"status"}); err == nil || !strings.Contains(err.Error(), "--context is required") { + if err := runVctx("", "", []string{"status"}); err == nil || !strings.Contains(err.Error(), "--context is required") { t.Errorf("empty context: got %v", err) } - if err := runVctx("sydney", nil); err == nil || !strings.Contains(err.Error(), "no vault arguments") { + if err := runVctx("sydney", "", nil); err == nil || !strings.Contains(err.Error(), "no vault arguments") { t.Errorf("no args: got %v", err) } } @@ -170,7 +170,7 @@ func TestRunVctxExecsWithBuiltInvocation(t *testing.T) { } t.Cleanup(func() { execVault = orig }) - if err := runVctx("sydney", []string{"kv", "list", "kv/"}); err != nil { + if err := runVctx("sydney", "", []string{"kv", "list", "kv/"}); err != nil { t.Fatalf("runVctx: %v", err) } if gotBin != vault { @@ -192,8 +192,60 @@ func TestRunVctxSurfacesExecError(t *testing.T) { execVault = func(bin string, argv, env []string) error { return errors.New("boom") } t.Cleanup(func() { execVault = orig }) - err := runVctx("sydney", []string{"status"}) + err := runVctx("sydney", "", []string{"status"}) if err == nil || !strings.Contains(err.Error(), "exec vault") { t.Errorf("expected wrapped exec error, got %v", err) } } + +func TestAutoFormat(t *testing.T) { + cases := []struct { + args []string + env []string + want string + }{ + {[]string{"kv", "get", "-mount=secret", "foo"}, nil, "yaml"}, + {[]string{"read", "aws/creds/role"}, nil, "yaml"}, + {[]string{"write", "db/creds/x", "ttl=1h"}, nil, "yaml"}, + {[]string{"kv", "list"}, nil, "table"}, + {[]string{"kv", "list", "aws/creds/"}, nil, "table"}, + {[]string{"status"}, nil, ""}, + {[]string{"kv", "put", "kv/foo", "a=b"}, nil, ""}, + {[]string{"kv", "get", "-format=json", "kv/foo"}, nil, ""}, + {[]string{"read", "--format", "json", "aws/creds/role"}, nil, ""}, + {[]string{"kv", "get", "-field=password", "kv/foo"}, nil, ""}, + {[]string{"kv", "get", "kv/foo"}, []string{"VAULT_FORMAT=json"}, ""}, + {[]string{"kv", "get", "kv/foo"}, []string{"VAULT_FORMAT="}, "yaml"}, + } + for _, c := range cases { + if got := autoFormat(c.args, c.env); got != c.want { + t.Errorf("autoFormat(%v, %v) = %q, want %q", c.args, c.env, got, c.want) + } + } +} + +func TestBuildInvocationFormat(t *testing.T) { + setupVctx(t, vctxConfig, map[string]shared.Token{ + "sydney": {Address: "https://vault.syd1.au.unkin.net", Token: "s.X"}, + }) + cases := []struct { + flag string + args []string + want string + set bool + }{ + {"", []string{"kv", "get", "kv/foo"}, "yaml", true}, + {"", []string{"status"}, "", false}, + {"json", []string{"kv", "get", "kv/foo"}, "json", true}, + {"json", []string{"status"}, "json", true}, + } + for _, c := range cases { + _, _, env, err := buildInvocation("sydney", c.flag, c.args, nil) + if err != nil { + t.Fatalf("buildInvocation: %v", err) + } + if v, ok := lastVal(env, "VAULT_FORMAT"); v != c.want || ok != c.set { + t.Errorf("flag %q args %v: VAULT_FORMAT = %q (set %v), want %q (set %v)", c.flag, c.args, v, ok, c.want, c.set) + } + } +} From 718a3a9063c2f897ab7bbb666138aa77a477b037 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Fri, 23 Oct 2026 22:00:31 +1100 Subject: [PATCH 2/3] Stop vctx format scan at --; limit raw to vault read --- docs/vctx.md | 2 +- vctx/main.go | 8 ++++++-- vctx/main_test.go | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/vctx.md b/docs/vctx.md index 66afc25..02f113f 100644 --- a/docs/vctx.md +++ b/docs/vctx.md @@ -48,7 +48,7 @@ Without `--format`, `vctx` sets `VAULT_FORMAT` from the vault command: It is left alone when `VAULT_FORMAT` is already set in the environment or the vault args include `-format` or `-field`. `--format` (`table`, `json`, `yaml`, -`pretty`, `raw`) always sets it. +`pretty`; `raw` for `vault read` only) always sets it. ## Flags diff --git a/vctx/main.go b/vctx/main.go index e6358da..5b8ad6e 100644 --- a/vctx/main.go +++ b/vctx/main.go @@ -68,7 +68,7 @@ func newRootCmd() *cobra.Command { }) root.Flags().StringVar(&format, "format", "", "VAULT_FORMAT for this call (default: picked from the vault command)") _ = root.RegisterFlagCompletionFunc("format", cobra.FixedCompletions( - []string{"table", "json", "yaml", "pretty", "raw"}, cobra.ShellCompDirectiveNoFileComp)) + []string{"table", "json", "yaml", "pretty"}, cobra.ShellCompDirectiveNoFileComp)) root.AddCommand(&cobra.Command{ Use: "version", @@ -166,7 +166,11 @@ func autoFormat(args, baseEnv []string) string { } } var pos []string - for _, a := range args { + for i, a := range args { + if a == "--" { + pos = append(pos, args[i+1:]...) + break + } name, _, _ := strings.Cut(strings.TrimLeft(a, "-"), "=") switch { case !strings.HasPrefix(a, "-"): diff --git a/vctx/main_test.go b/vctx/main_test.go index f8612ea..7613fe2 100644 --- a/vctx/main_test.go +++ b/vctx/main_test.go @@ -214,6 +214,7 @@ func TestAutoFormat(t *testing.T) { {[]string{"kv", "get", "-format=json", "kv/foo"}, nil, ""}, {[]string{"read", "--format", "json", "aws/creds/role"}, nil, ""}, {[]string{"kv", "get", "-field=password", "kv/foo"}, nil, ""}, + {[]string{"kv", "get", "--", "-format=json"}, nil, "yaml"}, {[]string{"kv", "get", "kv/foo"}, []string{"VAULT_FORMAT=json"}, ""}, {[]string{"kv", "get", "kv/foo"}, []string{"VAULT_FORMAT="}, "yaml"}, } From 0d9dd267fc7aac3321220469be7b2947d85068e2 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Fri, 23 Oct 2026 22:03:45 +1100 Subject: [PATCH 3/3] Drop ambient copies of vctx-managed env vars before exec --- docs/vctx.md | 4 ++-- vctx/main.go | 35 +++++++++++++++++++++------------ vctx/main_test.go | 50 +++++++++++++++++++++++++++-------------------- 3 files changed, 54 insertions(+), 35 deletions(-) diff --git a/docs/vctx.md b/docs/vctx.md index 02f113f..2d35415 100644 --- a/docs/vctx.md +++ b/docs/vctx.md @@ -32,7 +32,7 @@ vctx --context sydney kv put kv/foo/bar secret=baz context. Because it replaces the process, `vault`'s exit status, signals and TTY behaviour pass straight through. -The ambient `VAULT_ADDR` / `VAULT_TOKEN` in your shell are ignored for the call +The ambient `VAULT_ADDR` / `VAULT_TOKEN` / `VAULT_NAMESPACE` in your shell are ignored for the call — `vctx` always targets the chosen context. ## Output format @@ -48,7 +48,7 @@ Without `--format`, `vctx` sets `VAULT_FORMAT` from the vault command: It is left alone when `VAULT_FORMAT` is already set in the environment or the vault args include `-format` or `-field`. `--format` (`table`, `json`, `yaml`, -`pretty`; `raw` for `vault read` only) always sets it. +`pretty`; `raw` for `vault read` only) always sets it, overriding the environment. ## Flags diff --git a/vctx/main.go b/vctx/main.go index 5b8ad6e..93cef0c 100644 --- a/vctx/main.go +++ b/vctx/main.go @@ -106,9 +106,10 @@ func runVctx(context, format string, args []string) error { // buildInvocation resolves the context and its cached token, locates the vault // binary, and returns the binary path, argv (vault + args) and the environment // to exec with. baseEnv is the starting environment (normally os.Environ()); -// the Vault settings are appended so they override any ambient values (exec -// semantics: the last assignment of a variable wins). format, when non-empty, -// is set as VAULT_FORMAT; otherwise autoFormat picks one. +// ambient copies of the keys vctx sets are dropped first, since Go programs +// (vault included) honour the first occurrence of a duplicated variable. +// VAULT_NAMESPACE is always dropped so a context without one is not redirected. +// format, when non-empty, is set as VAULT_FORMAT; otherwise autoFormat picks one. func buildInvocation(context, format string, args, baseEnv []string) (bin string, argv []string, env []string, err error) { cfg, err := shared.Load() if err != nil { @@ -128,12 +129,10 @@ func buildInvocation(context, format string, args, baseEnv []string) (bin string return "", nil, nil, fmt.Errorf("vault CLI not found in PATH: %w", err) } - env = make([]string, len(baseEnv), len(baseEnv)+4) - copy(env, baseEnv) - env = append(env, - "VAULT_ADDR="+rc.Address, - "VAULT_TOKEN="+tok.Token, - ) + set := map[string]string{ + "VAULT_ADDR": rc.Address, + "VAULT_TOKEN": tok.Token, + } // Namespace comes from the context, falling back to what the token was // issued under; only set when non-empty. ns := rc.Namespace @@ -141,14 +140,26 @@ func buildInvocation(context, format string, args, baseEnv []string) (bin string ns = tok.Namespace } if ns != "" { - env = append(env, "VAULT_NAMESPACE="+ns) + set["VAULT_NAMESPACE"] = ns } - if format == "" { format = autoFormat(args, baseEnv) } if format != "" { - env = append(env, "VAULT_FORMAT="+format) + set["VAULT_FORMAT"] = format + } + + for _, e := range baseEnv { + k, _, _ := strings.Cut(e, "=") + if _, ok := set[k]; ok || k == "VAULT_NAMESPACE" { + continue + } + env = append(env, e) + } + for _, k := range []string{"VAULT_ADDR", "VAULT_TOKEN", "VAULT_NAMESPACE", "VAULT_FORMAT"} { + if v, ok := set[k]; ok { + env = append(env, k+"="+v) + } } argv = append([]string{vaultBin}, args...) diff --git a/vctx/main_test.go b/vctx/main_test.go index 7613fe2..ab8b056 100644 --- a/vctx/main_test.go +++ b/vctx/main_test.go @@ -53,16 +53,20 @@ contexts: namespace: staging ` -// lastVal returns the value of the last occurrence of key in an env slice, -// mirroring exec's "last assignment wins" semantics. -func lastVal(env []string, key string) (string, bool) { - val, ok := "", false +// envVal returns key's value from an env slice, failing the test if the key +// appears more than once (Go's os/exec children honour the first occurrence). +func envVal(t *testing.T, env []string, key string) (string, bool) { + t.Helper() + val, n := "", 0 for _, e := range env { if strings.HasPrefix(e, key+"=") { - val, ok = e[len(key)+1:], true + val, n = e[len(key)+1:], n+1 } } - return val, ok + if n > 1 { + t.Errorf("%s appears %d times in env %v", key, n, env) + } + return val, n > 0 } func TestBuildInvocationArgsAndEnvOverride(t *testing.T) { @@ -71,7 +75,7 @@ func TestBuildInvocationArgsAndEnvOverride(t *testing.T) { }) // Ambient VAULT_ADDR/VAULT_TOKEN must be overridden by the context's values. - baseEnv := []string{"HOME=/home/x", "VAULT_ADDR=ambient", "VAULT_TOKEN=ambient"} + baseEnv := []string{"HOME=/home/x", "VAULT_ADDR=ambient", "VAULT_TOKEN=ambient", "VAULT_NAMESPACE=ambient"} args := []string{"kv", "get", "-field=foo", "secret/x"} bin, argv, env, err := buildInvocation("sydney", "", args, baseEnv) @@ -86,17 +90,17 @@ func TestBuildInvocationArgsAndEnvOverride(t *testing.T) { if !reflect.DeepEqual(argv, wantArgv) { t.Errorf("argv = %v, want %v", argv, wantArgv) } - if v, _ := lastVal(env, "VAULT_ADDR"); v != "https://vault.syd1.au.unkin.net" { + if v, _ := envVal(t, env, "VAULT_ADDR"); v != "https://vault.syd1.au.unkin.net" { t.Errorf("VAULT_ADDR = %q, want context override to win", v) } - if v, _ := lastVal(env, "VAULT_TOKEN"); v != "s.SYDTOK" { + if v, _ := envVal(t, env, "VAULT_TOKEN"); v != "s.SYDTOK" { t.Errorf("VAULT_TOKEN = %q, want s.SYDTOK", v) } - if _, ok := lastVal(env, "VAULT_NAMESPACE"); ok { + if _, ok := envVal(t, env, "VAULT_NAMESPACE"); ok { t.Error("VAULT_NAMESPACE should be unset for a context with no namespace") } // The ambient HOME must be preserved. - if v, _ := lastVal(env, "HOME"); v != "/home/x" { + if v, _ := envVal(t, env, "HOME"); v != "/home/x" { t.Errorf("HOME = %q, base env not preserved", v) } } @@ -109,7 +113,7 @@ func TestBuildInvocationNamespaceFromContext(t *testing.T) { if err != nil { t.Fatalf("buildInvocation: %v", err) } - if v, _ := lastVal(env, "VAULT_NAMESPACE"); v != "staging" { + if v, _ := envVal(t, env, "VAULT_NAMESPACE"); v != "staging" { t.Errorf("VAULT_NAMESPACE = %q, want staging", v) } } @@ -124,7 +128,7 @@ func TestBuildInvocationNamespaceFallsBackToToken(t *testing.T) { if err != nil { t.Fatalf("buildInvocation: %v", err) } - if v, _ := lastVal(env, "VAULT_NAMESPACE"); v != "from-token" { + if v, _ := envVal(t, env, "VAULT_NAMESPACE"); v != "from-token" { t.Errorf("VAULT_NAMESPACE = %q, want from-token fallback", v) } } @@ -179,7 +183,7 @@ func TestRunVctxExecsWithBuiltInvocation(t *testing.T) { if !reflect.DeepEqual(gotArgv, []string{vault, "kv", "list", "kv/"}) { t.Errorf("exec argv = %v", gotArgv) } - if v, _ := lastVal(gotEnv, "VAULT_TOKEN"); v != "s.SYDTOK" { + if v, _ := envVal(t, gotEnv, "VAULT_TOKEN"); v != "s.SYDTOK" { t.Errorf("exec env VAULT_TOKEN = %q", v) } } @@ -229,24 +233,28 @@ func TestBuildInvocationFormat(t *testing.T) { setupVctx(t, vctxConfig, map[string]shared.Token{ "sydney": {Address: "https://vault.syd1.au.unkin.net", Token: "s.X"}, }) + ambient := []string{"VAULT_FORMAT=table"} cases := []struct { flag string args []string + base []string want string set bool }{ - {"", []string{"kv", "get", "kv/foo"}, "yaml", true}, - {"", []string{"status"}, "", false}, - {"json", []string{"kv", "get", "kv/foo"}, "json", true}, - {"json", []string{"status"}, "json", true}, + {"", []string{"kv", "get", "kv/foo"}, nil, "yaml", true}, + {"", []string{"status"}, nil, "", false}, + {"json", []string{"kv", "get", "kv/foo"}, nil, "json", true}, + {"json", []string{"status"}, nil, "json", true}, + {"", []string{"kv", "get", "kv/foo"}, ambient, "table", true}, + {"json", []string{"kv", "get", "kv/foo"}, ambient, "json", true}, } for _, c := range cases { - _, _, env, err := buildInvocation("sydney", c.flag, c.args, nil) + _, _, env, err := buildInvocation("sydney", c.flag, c.args, c.base) if err != nil { t.Fatalf("buildInvocation: %v", err) } - if v, ok := lastVal(env, "VAULT_FORMAT"); v != c.want || ok != c.set { - t.Errorf("flag %q args %v: VAULT_FORMAT = %q (set %v), want %q (set %v)", c.flag, c.args, v, ok, c.want, c.set) + if v, ok := envVal(t, env, "VAULT_FORMAT"); v != c.want || ok != c.set { + t.Errorf("flag %q args %v base %v: VAULT_FORMAT = %q (set %v), want %q (set %v)", c.flag, c.args, c.base, v, ok, c.want, c.set) } } }