Fail a bare invocation when stdout is not a terminal
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/integration Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline failed

Puppet's generate() reads stdout through a pipe with combine: true, so help
text on a zero exit lands in the catalog. Help now requires an interactive
stdout; a piped one exits non-zero with nothing on stdout.

- gate the no-args help path on an isTerminal(stdout) check
- reject an explicitly empty --config instead of resolving it
- ignore a relative XDG_CONFIG_HOME, per the XDG spec
- give the integration subprocess a hermetic HOME/XDG_CONFIG_HOME
- pass []string{} to run(), so cobra cannot fall back to os.Args
This commit is contained in:
2026-09-22 22:50:05 +10:00
parent f7c8ca5fba
commit 4ff55f052a
7 changed files with 219 additions and 22 deletions
+50
View File
@@ -272,9 +272,24 @@ func writeConfig(t *testing.T, dir, roleID string) string {
return path
}
// hermeticEnv strips the developer's environment from the subprocess. The
// tests claim to exercise the baked-in config path, so a real
// ~/.config/certmanager/config.yaml must not resolve ahead of it and point the
// binary at a production Vault.
func hermeticEnv(t *testing.T) []string {
t.Helper()
empty := t.TempDir()
return []string{
"PATH=/usr/bin:/bin",
"HOME=" + empty,
"XDG_CONFIG_HOME=" + empty,
}
}
func runCertmanager(t *testing.T, bin string, args ...string) (int, string, string) {
t.Helper()
cmd := exec.Command(bin, args...)
cmd.Env = hermeticEnv(t)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
@@ -474,6 +489,41 @@ func TestCAChainNeedsNoCredentials(t *testing.T) {
}
}
// A developer with a real ~/.config/certmanager/config.yaml must not have it
// leak into the subprocess, which would run the suite against their Vault.
func TestIgnoresTheDevelopersConfig(t *testing.T) {
waitForVault(t)
roleID := setupVault(t)
dir := t.TempDir()
bin := buildBinary(t, dir, writeConfig(t, dir, roleID))
poisoned := t.TempDir()
cfgDir := filepath.Join(poisoned, "certmanager")
if err := os.MkdirAll(cfgDir, 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
body := "vault:\n addr: http://127.0.0.1:1\n mount_point: nope\n role_name: nope\n" +
" approle_path: approle\n role_id: nope\n"
if err := os.WriteFile(filepath.Join(cfgDir, "config.yaml"), []byte(body), 0o600); err != nil {
t.Fatalf("write config: %v", err)
}
t.Setenv("XDG_CONFIG_HOME", poisoned)
t.Setenv("HOME", poisoned)
code, stdout, stderr := runCertmanager(t, bin, commonName, "--json")
if code != 0 {
t.Fatalf("exit code %d, stderr: %s", code, stderr)
}
var doc map[string]string
if err := json.Unmarshal([]byte(stdout), &doc); err != nil {
t.Fatalf("stdout is not JSON: %v\n%s", err, stdout)
}
if doc["certificate"] == "" {
t.Error("no certificate issued; the baked-in config path was not used")
}
}
func TestCAChainFailsCleanlyForAnUnknownMount(t *testing.T) {
waitForVault(t)
roleID := setupVault(t)