diff --git a/main.go b/main.go index fb6db8c..b798bcb 100644 --- a/main.go +++ b/main.go @@ -215,26 +215,35 @@ func bindPayloadFlags(fs *flag.FlagSet) (scriptPath *string, envNames *stringSli return scriptPath, envNames } +// buildPayload builds env (from -env flags) unconditionally, regardless of +// whether -script or a plain -- command is used -- both need it forwarded +// to the remote side. Previously env was only ever built inside the +// -script branch; a plain -- command silently ignored -env entirely (no +// forwarding *and* no validation that the named vars even existed +// locally), found while diagnosing why chromium-fork's sccache/MinIO +// credentials (forwarded via -env against -- 'autoninja ...' steps) never +// reached the remote build at all. func buildPayload(subcmd string, scriptPath *string, envNames *stringSlice, remainder []string) remotePayload { + env := map[string]string{} + for _, name := range *envNames { + v, ok := os.LookupEnv(name) + if !ok { + fatal("%s: -env %s: not set in this process's environment", subcmd, name) + } + env[name] = v + } + if *scriptPath != "" { script, err := os.ReadFile(*scriptPath) if err != nil { fatal("%s: reading -script %q: %v", subcmd, *scriptPath, err) } - env := map[string]string{} - for _, name := range *envNames { - v, ok := os.LookupEnv(name) - if !ok { - fatal("%s: -env %s: not set in this process's environment", subcmd, name) - } - env[name] = v - } return remotePayload{script: string(script), env: env} } if len(remainder) == 0 { fatal("%s: usage: hetzner-runner %s [flags] -- (or -script )", subcmd, subcmd) } - return remotePayload{command: strings.Join(remainder, " ")} + return remotePayload{command: strings.Join(remainder, " "), env: env} } func bindConfig(fs *flag.FlagSet, cfg *config) { @@ -352,7 +361,7 @@ func runExecCommand(client *hcloud.Client, st *state, payload remotePayload, upl if payload.script != "" { return sshExecScript(st, payload.script, payload.env) } - return sshExec(st, payload.command) + return sshExec(st, payload.command, payload.env) }) if runErr != nil { dlog("exec: Go-level FAILURE (cleaning up): %v", runErr) @@ -441,7 +450,7 @@ func runLifecycle(client *hcloud.Client, token string, cfg config, payload remot if payload.script != "" { return sshExecScript(st, payload.script, payload.env) } - return sshExec(st, payload.command) + return sshExec(st, payload.command, payload.env) }) if err != nil { dlog("runLifecycle: remote command FAILED: %v", err) @@ -573,7 +582,7 @@ func waitForSSH(st *state, timeout time.Duration) error { // TCP is up; give sshd + cloud-init user creation a moment, // then confirm with a real SSH handshake + command. time.Sleep(3 * time.Second) - if code, err := sshExec(st, "true"); err == nil && code == 0 { + if code, err := sshExec(st, "true", nil); err == nil && code == 0 { return nil } else { lastErr = err @@ -586,7 +595,19 @@ func waitForSSH(st *state, timeout time.Duration) error { return fmt.Errorf("timed out after %s, last error: %v", timeout, lastErr) } -func sshExec(st *state, command string) (int, error) { +// sshExec runs command via a plain SSH exec request (no stdin -- see +// sshSession/sshExecScript for why that's what makes a PTY here safe). +// +// env is prefixed onto the command as `export KEY='value'; ...`. Found +// missing entirely (env silently accepted by the CLI flags, validated as +// present locally, then never actually forwarded) while diagnosing why +// chromium-fork's sccache/MinIO credentials never reached the remote +// `autoninja` invocations in ci runs #69/#70 -- every `-- ` step +// using `-env AWS_ACCESS_KEY_ID -env AWS_SECRET_ACCESS_KEY` had those +// values validated against *this* process's environment and then +// dropped, never exported into the remote command's. sshExecScript +// (the -script path) already did this correctly; -- never did. +func sshExec(st *state, command string, env map[string]string) (int, error) { session, client, err := sshSession(st, true) if err != nil { return -1, err @@ -594,9 +615,15 @@ func sshExec(st *state, command string) (int, error) { defer client.Close() defer session.Close() + var buf strings.Builder + for k, v := range env { + fmt.Fprintf(&buf, "export %s=%s; ", k, shellQuote(v)) + } + buf.WriteString(command) + session.Stdout = os.Stdout session.Stderr = os.Stderr - return runSession(session, command) + return runSession(session, buf.String()) } // uploadFile copies a local file to the remote box. See uploadBytes for @@ -739,8 +766,8 @@ func sshExecScript(st *state, script string, env map[string]string) (int, error) if err := uploadBytes(st, []byte(buf.String()), remotePath); err != nil { return -1, fmt.Errorf("uploading script: %w", err) } - code, err := sshExec(st, fmt.Sprintf("bash %s", shellQuote(remotePath))) - if rmCode, rmErr := sshExec(st, fmt.Sprintf("rm -f %s", shellQuote(remotePath))); rmErr == nil && rmCode != 0 { + code, err := sshExec(st, fmt.Sprintf("bash %s", shellQuote(remotePath)), nil) + if rmCode, rmErr := sshExec(st, fmt.Sprintf("rm -f %s", shellQuote(remotePath)), nil); rmErr == nil && rmCode != 0 { fmt.Fprintf(os.Stderr, "warning: failed to remove remote script %s\n", remotePath) } return code, err