Skip to content

Commit 229de09

Browse files
eamonnmoloneyclaude
andcommitted
build(deploy-camunda): address review round 2 on secrets/env preflight
- config env: flip --show-origin default to false so the default output is the machine-parseable NAME/VALUE pair; --show-origin adds the ORIGIN column, matching git config precedent. - config init: suppress terminal echo for promptSecret via x/term.ReadPassword on a TTY; piped/test input keeps the buffered line reader. - config: add ErrDeploymentExists sentinel; config init matches it with errors.Is instead of substring-matching the error text. - preflight: add a chart-path check that resolves the path like root.go and fails red on a bad path, so doctor no longer reports falsely green when its PersistentPreRunE is skipped. - merge.go / preflight.go / test-secret-mapping.yaml: correct the SkipPreflight comment (matrix leaves it false), trim why-narration per the HOW-only policy, and drop the self-contradictory "no recompile needed" note. - preflight_test: isolate buildScenarioEnv seed test from a real .env via t.Chdir; add TestCheckChartPath. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d77349c commit 229de09

8 files changed

Lines changed: 130 additions & 22 deletions

File tree

scripts/deploy-camunda/cmd/config_env.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import (
1212
)
1313

1414
// newEnvCommand creates `config env`: prints the effective environment a deploy
15-
// would see and which layer each value came from, with secret values masked.
16-
// Mirrors `git config --show-origin`.
15+
// would see, with secret values masked. Default output is two machine-parseable
16+
// columns (NAME, VALUE); --show-origin adds an ORIGIN column, mirroring
17+
// `git config` (plain by default, origin column only when asked).
1718
func newEnvCommand() *cobra.Command {
1819
var showOrigin bool
1920
var unmask bool
@@ -55,7 +56,7 @@ unless --unmask is passed.`,
5556
},
5657
}
5758

58-
cmd.Flags().BoolVar(&showOrigin, "show-origin", true, "Show which layer each value came from")
59+
cmd.Flags().BoolVar(&showOrigin, "show-origin", false, "Add an ORIGIN column showing which layer each value came from")
5960
cmd.Flags().BoolVar(&unmask, "unmask", false, "Show secret values in clear text (key/secret/password/token)")
6061
return cmd
6162
}

scripts/deploy-camunda/cmd/config_init.go

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"scripts/prepare-helm-values/pkg/env"
2020

2121
"github.com/spf13/cobra"
22+
"golang.org/x/term"
2223
)
2324

2425
// newInitCommand creates the `config init` wizard: an interactive first-run
@@ -77,7 +78,7 @@ checklist without prompting.`,
7778
}
7879
if err := config.CreateDeployment(cfgRes.Path, name); err != nil {
7980
// Already-exists is fine — we'll update it in place.
80-
if !strings.Contains(err.Error(), "already exists") {
81+
if !errors.Is(err, config.ErrDeploymentExists) {
8182
return err
8283
}
8384
fmt.Fprintf(out, " profile %q already exists — updating it\n", name)
@@ -246,16 +247,47 @@ func promptLine(ctx context.Context, out io.Writer, r *bufio.Reader, label, def
246247
}
247248

248249
func promptSecret(ctx context.Context, out io.Writer, r *bufio.Reader, label string) (string, error) {
249-
// Note: input is not hidden (matches existing env.Prompt behavior); we simply
250-
// never echo the value back afterwards.
251250
fmt.Fprintf(out, "%s: ", label)
251+
// On a real terminal, read with echo disabled so the secret never appears on
252+
// screen or in scrollback. Piped/redirected input (tests, scripts) has
253+
// nothing to hide and falls back to the buffered line reader.
254+
stdinFd := int(os.Stdin.Fd())
255+
if r.Buffered() == 0 && term.IsTerminal(stdinFd) {
256+
secret, err := readSecretCtx(ctx, stdinFd)
257+
fmt.Fprintln(out)
258+
return secret, err
259+
}
252260
line, err := readLineCtx(ctx, r)
253261
if err != nil {
254262
return "", err
255263
}
256264
return strings.TrimSpace(line), nil
257265
}
258266

267+
// readSecretCtx reads an echo-suppressed line from the terminal fd, aborting if
268+
// ctx is cancelled. Like readLineCtx, the parked term.ReadPassword goroutine is
269+
// an accepted leak for this single-run CLI.
270+
func readSecretCtx(ctx context.Context, fd int) (string, error) {
271+
type res struct {
272+
b []byte
273+
err error
274+
}
275+
ch := make(chan res, 1)
276+
go func() {
277+
b, err := term.ReadPassword(fd)
278+
ch <- res{b, err}
279+
}()
280+
select {
281+
case <-ctx.Done():
282+
return "", ctx.Err()
283+
case rr := <-ch:
284+
if rr.err != nil && !errors.Is(rr.err, io.EOF) {
285+
return "", rr.err
286+
}
287+
return strings.TrimSpace(string(rr.b)), nil
288+
}
289+
}
290+
259291
func promptYesNo(ctx context.Context, out io.Writer, r *bufio.Reader, label string, def bool) (bool, error) {
260292
suffix := "y/N"
261293
if def {

scripts/deploy-camunda/config/config.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package config
22

33
import (
4+
"errors"
45
"fmt"
56
"io/fs"
67
"os"
@@ -629,6 +630,10 @@ func GetValue(cfgPath, key string) (string, error) {
629630
return formatValue(val), nil
630631
}
631632

633+
// ErrDeploymentExists is returned by CreateDeployment when the named deployment
634+
// is already present. Callers match it with errors.Is to take an update path.
635+
var ErrDeploymentExists = errors.New("deployment already exists")
636+
632637
// CreateDeployment creates a new empty deployment configuration.
633638
func CreateDeployment(cfgPath, name string) error {
634639
content, err := os.ReadFile(cfgPath)
@@ -655,7 +660,7 @@ func CreateDeployment(cfgPath, name string) error {
655660

656661
// Check if deployment already exists
657662
if _, exists := deployments[name]; exists {
658-
return fmt.Errorf("deployment %q already exists", name)
663+
return fmt.Errorf("deployment %q: %w", name, ErrDeploymentExists)
659664
}
660665

661666
// Create empty deployment

scripts/deploy-camunda/config/merge.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,9 @@ type RuntimeFlags struct {
190190
ConfigFound bool
191191

192192
// SkipPreflight disables the fail-fast secrets/env preflight that
193-
// deploy.Execute runs before any cluster mutation. The matrix runner sets
194-
// this true because it performs its own pre-dispatch docker login and
195-
// kube-context warmup; direct deploys leave it false so missing inputs
196-
// surface up front rather than mid-deploy.
193+
// deploy.Execute runs before any cluster mutation. Set via --skip-preflight
194+
// as an escape hatch; both direct deploys and matrix entries leave it false
195+
// so missing inputs surface up front rather than mid-deploy.
197196
SkipPreflight bool
198197

199198
// ChangedFlags tracks which CLI flags were explicitly set by the user.

scripts/deploy-camunda/deploy/data/test-secret-mapping.yaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# Vault secret mapping scaffolded by --auto-generate-secrets and `config init`.
22
#
33
# This file is the single source of truth for which environment variables are
4-
# packed into the test Secret. Edit it directly (no recompile needed beyond a
5-
# rebuild of the embedding binary) instead of the old hardcoded string in
6-
# secrets.go. All variables share one vault path, so the path is declared once.
4+
# packed into the test Secret, replacing the old hardcoded string in secrets.go.
5+
# It is baked into the binary via //go:embed at compile time, so any edit here
6+
# only takes effect after rebuilding deploy-camunda. All variables share one
7+
# vault path, so the path is declared once.
78
#
89
# generateTestSecrets random-fills the four DISTRO_QA_E2E_TESTS_* values when
910
# they are unset; the remaining IDP_*/OPENAI_* values are read from the

scripts/deploy-camunda/deploy/preflight.go

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
1-
// Package deploy preflight.go implements a self-diagnosing check of the
2-
// secrets/environment setup a deployment needs, mirroring tools like
3-
// `flyctl doctor` and `gh auth status`. The same checks back both the
4-
// standalone `deploy-camunda doctor` command and the fail-fast guard that runs
5-
// before any cluster mutation, so a missing credential surfaces up front with a
6-
// remediation hint instead of mid-deploy as an ImagePullBackOff or a
7-
// `kubectl apply` parse error.
1+
// Package deploy preflight.go runs the secrets/environment checks that back
2+
// both `deploy-camunda doctor` and the fail-fast guard in deploy.Execute.
83
package deploy
94

105
import (
@@ -101,10 +96,16 @@ func Preflight(ctx context.Context, flags *config.RuntimeFlags, opts PreflightOp
10196
// and companion placeholder checks don't false-positive on values the deploy
10297
// supplies itself. Vault-mapping and docker checks use baseEnv — those vars are
10398
// never deploy-computed.
99+
// Resolve the chart path first: doctor skips root.go's PersistentPreRunE, so
100+
// the check both validates the path and rewrites flags.Chart.ChartPath to the
101+
// resolved directory the scenario checks below depend on.
102+
chartCheck := checkChartPath(flags)
103+
104104
baseEnv := effectiveEnv(flags)
105105
deployEnv := scenarioDeployEnv(flags, baseEnv)
106106

107107
r.Checks = append(r.Checks, checkConfigFile(opts))
108+
r.Checks = append(r.Checks, chartCheck)
108109
r.Checks = append(r.Checks, checkKubeContext(ctx, flags, opts))
109110
r.Checks = append(r.Checks, checkDockerCredentials(flags, baseEnv)...)
110111
r.Checks = append(r.Checks, checkVaultMapping(flags, baseEnv))
@@ -312,6 +313,36 @@ func checkVaultMapping(flags *config.RuntimeFlags, envMap map[string]string) Che
312313
}
313314
}
314315

316+
// checkChartPath resolves the configured chart path the same way root.go's
317+
// PersistentPreRunE does — a relative path is joined against repoRoot — and
318+
// fails when it does not resolve to a directory. On success it rewrites
319+
// flags.Chart.ChartPath to the resolved path so the scenario/companion checks
320+
// scan the same directory a deploy would.
321+
func checkChartPath(flags *config.RuntimeFlags) Check {
322+
path := strings.TrimSpace(flags.Chart.ChartPath)
323+
if path == "" {
324+
return Check{Name: "chart path", Status: StatusOK, Detail: "no chart configured"}
325+
}
326+
if !filepath.IsAbs(path) && flags.Chart.RepoRoot != "" {
327+
if _, err := os.Stat(path); err != nil {
328+
resolved := filepath.Join(flags.Chart.RepoRoot, path)
329+
if fi, err := os.Stat(resolved); err == nil && fi.IsDir() {
330+
path = resolved
331+
}
332+
}
333+
}
334+
if fi, err := os.Stat(path); err != nil || !fi.IsDir() {
335+
return Check{
336+
Name: "chart path",
337+
Status: StatusFail,
338+
Detail: fmt.Sprintf("%q does not exist or is not a directory", path),
339+
Remediation: "set --repo-root/--chart/--version or --chart-path to a valid chart directory",
340+
}
341+
}
342+
flags.Chart.ChartPath = path
343+
return Check{Name: "chart path", Status: StatusOK, Detail: path}
344+
}
345+
315346
// checkScenarioEnv scans the values file(s) for the selected scenario(s) and
316347
// reports any $PLACEHOLDER env vars that are unset. Resolution failures are a
317348
// soft warning rather than a hard fail so `doctor` is still useful when no

scripts/deploy-camunda/deploy/preflight_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,10 @@ func TestCheckScenarioEnvScansLayeredFiles(t *testing.T) {
204204
// scenarios fail locally on the $VENOM_CLIENT_ID/$CONNECTORS_CLIENT_ID
205205
// placeholders. flags.ExtraEnv (used by OIDC/Entra) must still override.
206206
func TestBuildScenarioEnvSeedsKeycloakClientIDs(t *testing.T) {
207+
// Isolate from any real .env in the checkout: buildScenarioEnv now defaults
208+
// EnvFile to ".env" in CWD, which would otherwise leak local values over the
209+
// seed-override assertions below.
210+
t.Chdir(t.TempDir())
207211
ctx := &ScenarioContext{}
208212
t.Run("seeds keycloak defaults", func(t *testing.T) {
209213
env, err := buildScenarioEnv(ctx, &config.RuntimeFlags{})
@@ -230,6 +234,41 @@ func TestBuildScenarioEnvSeedsKeycloakClientIDs(t *testing.T) {
230234
})
231235
}
232236

237+
func TestCheckChartPath(t *testing.T) {
238+
dir := t.TempDir()
239+
240+
t.Run("no chart configured is OK", func(t *testing.T) {
241+
if c := checkChartPath(&config.RuntimeFlags{}); c.Status != StatusOK {
242+
t.Errorf("status = %q, want ok", c.Status)
243+
}
244+
})
245+
246+
t.Run("nonexistent path fails red", func(t *testing.T) {
247+
f := &config.RuntimeFlags{}
248+
f.Chart.ChartPath = filepath.Join(dir, "does-not-exist")
249+
if c := checkChartPath(f); c.Status != StatusFail {
250+
t.Errorf("status = %q, want fail", c.Status)
251+
}
252+
})
253+
254+
t.Run("relative path resolves against repoRoot", func(t *testing.T) {
255+
chart := filepath.Join(dir, "charts", "camunda-platform-8.10")
256+
if err := os.MkdirAll(chart, 0o755); err != nil {
257+
t.Fatal(err)
258+
}
259+
f := &config.RuntimeFlags{}
260+
f.Chart.RepoRoot = dir
261+
f.Chart.ChartPath = filepath.Join("charts", "camunda-platform-8.10")
262+
c := checkChartPath(f)
263+
if c.Status != StatusOK {
264+
t.Fatalf("status = %q, want ok", c.Status)
265+
}
266+
if f.Chart.ChartPath != chart {
267+
t.Errorf("ChartPath = %q, want resolved %q", f.Chart.ChartPath, chart)
268+
}
269+
})
270+
}
271+
233272
func TestCheckCompanionEnv(t *testing.T) {
234273
withCharts := func(charts ...config.CompanionChart) *config.RuntimeFlags {
235274
return &config.RuntimeFlags{CompanionCharts: charts}

scripts/deploy-camunda/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/mattn/go-runewidth v0.0.24
1010
github.com/spf13/cobra v1.10.2
1111
github.com/spf13/pflag v1.0.10
12+
golang.org/x/term v0.44.0
1213
gopkg.in/yaml.v3 v3.0.1
1314
scripts/camunda-core v0.0.0
1415
scripts/camunda-deployer v0.0.0
@@ -60,7 +61,6 @@ require (
6061
golang.org/x/oauth2 v0.27.0 // indirect
6162
golang.org/x/sync v0.20.0 // indirect
6263
golang.org/x/sys v0.46.0 // indirect
63-
golang.org/x/term v0.44.0 // indirect
6464
golang.org/x/text v0.23.0 // indirect
6565
golang.org/x/time v0.3.0 // indirect
6666
google.golang.org/protobuf v1.34.2 // indirect

0 commit comments

Comments
 (0)