diff --git a/docs/vctx.md b/docs/vctx.md index 85345a1..8cdb806 100644 --- a/docs/vctx.md +++ b/docs/vctx.md @@ -71,7 +71,7 @@ coloured, SIGINT/SIGTERM are forwarded, and vault's exit code is returned Without colour (piped output, `NO_COLOR`, `table`/`raw`/`pretty` formats) `vctx` execs `vault` exactly as before, so scripts see unchanged output. Streaming or interactive commands (`monitor`, `debug`, `events`, `ssh`, -`agent`, `proxy`, `server`) are always exec'd and never coloured. `JQ_COLORS` +`agent`, `proxy`, `server`, `login`, `operator`) are always exec'd and never coloured. `JQ_COLORS` overrides the JSON palette as in `jq`. ## Flags diff --git a/vctx/main.go b/vctx/main.go index 853cb26..f68be85 100644 --- a/vctx/main.go +++ b/vctx/main.go @@ -35,9 +35,10 @@ var execVault = syscall.Exec // osExit is swapped in tests to observe the child's exit code. var osExit = os.Exit -// streaming commands are never captured: buffering their stdout would hide -// output until exit (or break the TTY, for ssh). -var streaming = map[string]bool{"monitor": true, "debug": true, "events": true, "ssh": true, "agent": true, "proxy": true, "server": true} +// uncaptured commands are always exec'd: buffering their stdout would hide +// streamed output or prompts (unseal keys, MFA passcodes) until exit, or break +// the TTY, for ssh. +var uncaptured = map[string]bool{"monitor": true, "debug": true, "events": true, "ssh": true, "agent": true, "proxy": true, "server": true, "login": true, "operator": true} func main() { if err := newRootCmd().Execute(); err != nil { @@ -269,7 +270,7 @@ func colourFormat(args, env []string) string { f = val } f = strings.ToLower(f) - if streaming[cmd] || (f != "json" && f != "yaml") { + if uncaptured[cmd] || (f != "json" && f != "yaml") { return "" } return f diff --git a/vctx/main_test.go b/vctx/main_test.go index e56deef..e64d3da 100644 --- a/vctx/main_test.go +++ b/vctx/main_test.go @@ -7,7 +7,9 @@ import ( "path/filepath" "reflect" "strings" + "syscall" "testing" + "time" "git.unkin.net/unkin/vault-tools/shared" ) @@ -278,6 +280,10 @@ func TestColourFormat(t *testing.T) { {[]string{"events", "subscribe", "kv*"}, []string{"VAULT_FORMAT=json"}, ""}, {[]string{"monitor"}, []string{"VAULT_FORMAT=json"}, ""}, {[]string{"ssh", "-format=json", "host"}, nil, ""}, + {[]string{"login", "-method=userpass"}, []string{"VAULT_FORMAT=json"}, ""}, + {[]string{"operator", "unseal"}, []string{"VAULT_FORMAT=json"}, ""}, + {[]string{"-format=yaml", "operator", "generate-root"}, nil, ""}, + {[]string{"operator", "rekey", "-format=json"}, nil, ""}, } for _, c := range cases { if got := colourFormat(c.args, c.env); got != c.want { @@ -323,6 +329,41 @@ func TestRunChild(t *testing.T) { } } +func TestRunChildForwardsSignal(t *testing.T) { + dir := t.TempDir() + bin := fakeVault(t, `trap 'echo TERM > "$D/got"; trap - TERM; kill -TERM $$' TERM +touch "$D/ready" +i=0; while [ $i -lt 100 ]; do sleep 0.05; i=$((i+1)); done`) + type res struct { + code int + err error + } + ch := make(chan res, 1) + go func() { + code, err := runChild(bin, []string{bin}, append(os.Environ(), "D="+dir), "json", &bytes.Buffer{}) + ch <- res{code, err} + }() + for i := 0; i < 200; i++ { + if _, err := os.Stat(filepath.Join(dir, "ready")); err == nil { + break + } + time.Sleep(10 * time.Millisecond) + } + if err := syscall.Kill(os.Getpid(), syscall.SIGTERM); err != nil { + t.Fatal(err) + } + r := <-ch + if r.err != nil { + t.Fatal(r.err) + } + if got, _ := os.ReadFile(filepath.Join(dir, "got")); string(got) != "TERM\n" { + t.Errorf("child did not receive SIGTERM (got %q)", got) + } + if r.code != 128+15 { + t.Errorf("code = %d, want %d", r.code, 128+15) + } +} + func TestRunChildPassesArgsAndEnv(t *testing.T) { bin := fakeVault(t, `echo "{\"args\":\"$*\",\"tok\":\"$VAULT_TOKEN\"}"`) var out bytes.Buffer @@ -353,6 +394,8 @@ func TestRunVctxColourDecision(t *testing.T) { {"always", []string{"status"}, true}, {"always", []string{"kv", "get", "kv/foo"}, false}, {"always", []string{"read", "-format=json", "x"}, false}, + {"always", []string{"operator", "unseal", "-format=json"}, true}, + {"always", []string{"login", "-format=json"}, true}, } for _, c := range cases { execed, exited := false, -1