From 1a7698f9d3eaafd2600f2047a6ed10cc4a7885fc Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 2 Nov 2026 08:35:25 +0100 Subject: [PATCH 01/15] Spawn login sub-shells by default --- cmd/subshell.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/subshell.go b/cmd/subshell.go index 1eaa7384..76524685 100644 --- a/cmd/subshell.go +++ b/cmd/subshell.go @@ -50,7 +50,7 @@ func runCommandOrSubshell(roxieConfig deployer.RoxieConfig, centralDeploymentInf shellPath := resolveShellPath() log.Infof("Spawning sub-shell: %s", shellPath) printBanner(centralDeploymentInfo) - cmd = exec.Command(shellPath, "-i") + cmd = exec.Command(shellPath, "-l", "-i") } else { // args is non-empty. cmd = exec.Command(args[0], args[1:]...) From b66011d548788a1e2c96748e02a4432bbcc5a2cd Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 2 Nov 2026 10:21:20 +0100 Subject: [PATCH 02/15] Fix gke provisioning --- .github/workflows/e2e-tests.yml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index cc690330..4a086642 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -62,6 +62,7 @@ jobs: - name: Authenticate to GCloud + id: gcp-auth if: inputs.cluster-type == 'gke' uses: google-github-actions/auth@v3 with: @@ -73,9 +74,20 @@ jobs: with: install_components: "gke-gcloud-auth-plugin" - - name: Configure GKE auth plugin - if: inputs.cluster-type == 'gke' - run: echo "USE_GKE_GCLOUD_AUTH_PLUGIN=True" >> "$GITHUB_ENV" + - name: Use stored gcloud credentials + if: steps.gcp-auth.outcome == 'success' + env: + GCP_CREDENTIALS_JSON: ${{ secrets.ROXIE_CI_AUTOMATION_GCP_SA }} + run: | + echo "USE_GKE_GCLOUD_AUTH_PLUGIN=True" >> "$GITHUB_ENV" + expected_account="$(jq -er '.client_email' <<< "$GCP_CREDENTIALS_JSON")" + if [[ "$(gcloud config get-value core/account)" != "$expected_account" ]]; then + echo "::error::gcloud's active account does not match the CI service account" + exit 1 + fi + # setup-gcloud already stored these credentials. Avoid the file-override crash: + # https://github.com/google-github-actions/auth/issues/535 + echo "CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE=" >> "$GITHUB_ENV" - name: Download production infractl uses: stackrox/actions/infra/install-infractl@v1.0.48 From 00f866b308db5fc89c7813a1c07caf846438b49d Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 2 Nov 2026 08:41:41 +0100 Subject: [PATCH 03/15] Let roxie log exclusively to stderr, never to stdout --- internal/logger/logger.go | 17 +++++------------ internal/logger/logger_test.go | 18 +++++++++--------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 6df7a2fe..4e7658c8 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -31,19 +31,18 @@ type Logger struct { // New creates a new roxie logger. func New() *Logger { - return NewWithWriters(os.Stdout, os.Stderr) + return NewWithWriter(os.Stderr) } -// NewWithWriters creates a new roxie logger with configurable writers for stdout/stderr. -func NewWithWriters(stdout, stderr io.Writer) *Logger { +// NewWithWriter creates a new roxie logger that writes all output to w. +func NewWithWriter(w io.Writer) *Logger { level := &slog.LevelVar{} level.Set(LevelDim) h := &handler{ level: level, startTime: time.Now(), - stdout: stdout, - stderr: stderr, + stderr: w, } return &Logger{ @@ -172,7 +171,6 @@ type handler struct { mu sync.Mutex level *slog.LevelVar startTime time.Time - stdout io.Writer stderr io.Writer } @@ -192,14 +190,9 @@ func (h *handler) Handle(_ context.Context, r slog.Record) error { timestamp := color.GreenString("%02d:%02d", minutes, seconds) message := styleForLevel(r.Level).Sprint(r.Message) - w := h.stdout - if r.Level >= slog.LevelError { - w = h.stderr - } - h.mu.Lock() defer h.mu.Unlock() - fmt.Fprintf(w, "%s %s\n", timestamp, message) + fmt.Fprintf(h.stderr, "%s %s\n", timestamp, message) return nil } diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go index f5628b72..9a3fe53a 100644 --- a/internal/logger/logger_test.go +++ b/internal/logger/logger_test.go @@ -9,28 +9,28 @@ import ( ) func TestDebugOnlyEmitsWhenVerbose(t *testing.T) { - var stdout, stderr bytes.Buffer - l := NewWithWriters(&stdout, &stderr) + var buf bytes.Buffer + l := NewWithWriter(&buf) // Debug is suppressed at the default level (LevelDim). l.Debug("hidden") - if stdout.Len() != 0 { - t.Fatalf("expected no output before SetVerbose(true), got %q", stdout.String()) + if buf.Len() != 0 { + t.Fatalf("expected no output before SetVerbose(true), got %q", buf.String()) } // Enabling verbose lowers the level to slog.LevelDebug, so Debug emits. l.SetVerbose(true) l.Debug("visible") - if !strings.Contains(stdout.String(), "visible") { - t.Fatalf("expected debug output to contain %q, got %q", "visible", stdout.String()) + if !strings.Contains(buf.String(), "visible") { + t.Fatalf("expected debug output to contain %q, got %q", "visible", buf.String()) } // Disabling verbose restores the default level and suppresses Debug again. - before := stdout.Len() + before := buf.Len() l.SetVerbose(false) l.Debug("hidden again") - if stdout.Len() != before { - t.Fatalf("expected no new output after SetVerbose(false), got %q", stdout.String()[before:]) + if buf.Len() != before { + t.Fatalf("expected no new output after SetVerbose(false), got %q", buf.String()[before:]) } } From 4a964e125105afa5d9aa45447f6836bdbb31278e Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 2 Nov 2026 09:04:53 +0100 Subject: [PATCH 04/15] Base decisions for colorized output on stderr, not stdout --- cmd/main.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmd/main.go b/cmd/main.go index 99c35bbd..d0f4af11 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -9,6 +9,7 @@ import ( "dario.cat/mergo" "github.com/fatih/color" + "github.com/mattn/go-isatty" "github.com/spf13/cobra" "github.com/stackrox/roxie/internal/deployer" "github.com/stackrox/roxie/internal/helpers" @@ -78,6 +79,12 @@ Red Hat Advanced Cluster Security (ACS) on any Kubernetes/OpenShift cluster.`, } func init() { + // All log output goes to stderr, so base the color decision on stderr's TTY status + // instead of the fatih/color default (stdout). This keeps colors working when stdout + // is captured, e.g. eval "$(roxie deploy --eval)". + _, noColorSet := os.LookupEnv("NO_COLOR") + color.NoColor = noColorSet || os.Getenv("TERM") == "dumb" || !isatty.IsTerminal(os.Stderr.Fd()) + rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output (show CRs)") rootCmd.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "Do not actually modify cluster") rootCmd.PersistentFlags().BoolVar(&skipUserConfig, "skip-user-config", false, From 6019e43cd581c8d94bc5124c6ecc3563949c8390 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 2 Nov 2026 09:21:27 +0100 Subject: [PATCH 05/15] Implement --eval --- cmd/deploy.go | 38 +++++++++++++++++++++++++++++++++----- cmd/main.go | 2 ++ cmd/shell.go | 11 ++++++++++- cmd/subshell.go | 31 ++++++++++++++++++------------- 4 files changed, 63 insertions(+), 19 deletions(-) diff --git a/cmd/deploy.go b/cmd/deploy.go index 4089b128..4ddd5863 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -212,6 +212,17 @@ this flag can be used to tell roxie how to pre-load images for the current clust }), ) + registerFlag(cmd, settings, "eval", "Print environment as eval-able export statements to stdout", + withNoOptDefVal("true"), + withApplyFnBool(func(config *deployer.Config, val bool) error { + evalMode = true + config.Roxie.Envrc = new("/dev/stdout") + return nil + }), + ) + + cmd.MarkFlagsMutuallyExclusive("eval", "envrc") + // Make --override an alias for --config, for backwards compatibility. cmd.Flags().SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName { if name == "override" { @@ -361,11 +372,28 @@ func runDeploy(cmd *cobra.Command, args []string) error { if err := manifest.CreateManifestSecretOnCluster(ctx, m); err != nil { log.Warningf("Failed to save roxie manifest: %v", err) } - } - if components.IncludesCentral() && !deploySettings.Roxie.EnvrcSet() { - if err := spawnSubshellForDeployerEnv(deploySettings.Roxie, d); err != nil { - return fmt.Errorf("failed to spawn subshell: %w", err) + if components.IncludesCentral() { + if evalMode { + tempDir, err := os.MkdirTemp("", "roxie-shell-*") + if err != nil { + return fmt.Errorf("failed to create temp dir: %w", err) + } + centralDeploymentInfo, err := manifest.ManifestToCentralDeploymentInfo(ctx, tempDir, &m) + if err != nil { + return fmt.Errorf("extracting central deployment info from manifest: %w", err) + } + + printBanner(os.Stderr, centralDeploymentInfo) + printExports(centralDeploymentInfo) + return nil + } + + if !deploySettings.Roxie.EnvrcSet() { + if err := spawnSubshellForDeployerEnv(deploySettings.Roxie, d); err != nil { + return fmt.Errorf("failed to spawn subshell: %w", err) + } + } } } @@ -497,7 +525,7 @@ func deployValidate(components component.Component, deploySettings *deployer.Con } if components.IncludesCentral() && !env.RunningInteractively && !deploySettings.Roxie.EnvrcSet() { - return errors.New("running without a controlling terminal requires --envrc to be set") + return errors.New("running without a controlling terminal requires --envrc or --eval to be set") } if deploySettings.Roxie.UsesCustomRegistry() { diff --git a/cmd/main.go b/cmd/main.go index d0f4af11..466fb8d5 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -25,6 +25,8 @@ var ( shell string dryRun bool + evalMode bool + skipUserConfig bool // We need this set up before command line flags are parsed. diff --git a/cmd/shell.go b/cmd/shell.go index a403332f..dcbe6fb8 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -47,6 +47,7 @@ Examples: } cmd.Flags().StringVar(&shell, "shell", "", "Shell to spawn") + cmd.Flags().BoolVar(&evalMode, "eval", false, "Print environment as eval-able export statements to stdout") return cmd } @@ -82,12 +83,20 @@ func runShell(args []string) error { if err != nil { return fmt.Errorf("failed to create temp dir: %w", err) } - defer os.RemoveAll(tempDir) + if !evalMode { + defer os.RemoveAll(tempDir) + } centralDeploymentInfo, err := manifest.ManifestToCentralDeploymentInfo(ctx, tempDir, m) if err != nil { return fmt.Errorf("extracting central deployment info from manifest: %w", err) } + if evalMode { + printBanner(os.Stderr, centralDeploymentInfo) + printExports(centralDeploymentInfo) + return nil + } + return runCommandOrSubshell(config.Roxie, centralDeploymentInfo, args) } diff --git a/cmd/subshell.go b/cmd/subshell.go index 76524685..f3ef8bfb 100644 --- a/cmd/subshell.go +++ b/cmd/subshell.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "io" "os" "os/exec" "syscall" @@ -49,7 +50,7 @@ func runCommandOrSubshell(roxieConfig deployer.RoxieConfig, centralDeploymentInf if subShellMode(args) { shellPath := resolveShellPath() log.Infof("Spawning sub-shell: %s", shellPath) - printBanner(centralDeploymentInfo) + printBanner(color.Output, centralDeploymentInfo) cmd = exec.Command(shellPath, "-l", "-i") } else { // args is non-empty. @@ -192,23 +193,27 @@ func isHAProxyAvailable() bool { return err == nil } -func printBanner(centralDeploymentInfo types.CentralDeploymentInfo) { +func printExports(centralDeploymentInfo types.CentralDeploymentInfo) { + for name, val := range roxieenv.AssembleRoxieEnvironment(centralDeploymentInfo).Export() { + fmt.Fprintf(os.Stdout, "export %s=%q\n", name, val) + } +} + +func printBanner(w io.Writer, centralDeploymentInfo types.CentralDeploymentInfo) { cyan := color.New(color.FgCyan, color.Bold) - cyan.Println("\n[roxie] Entering a subshell with ACS environment variables set.") - cyan.Println("[roxie]") - cyan.Println("[roxie] Environment is set up for talking to ACS Central. Examples:") - cyan.Println("[roxie]") - cyan.Println("[roxie] * roxctl central whoami") - cyan.Println("[roxie] * roxcurl /v1/clusters") - cyan.Println("[roxie]") + cyan.Fprintln(w, "\n[roxie] Environment is set up for talking to ACS Central. Examples:") + cyan.Fprintln(w, "[roxie]") + cyan.Fprintln(w, "[roxie] * roxctl central whoami") + cyan.Fprintln(w, "[roxie] * roxcurl /v1/clusters") + cyan.Fprintln(w, "[roxie]") if port := centralDeploymentInfo.HAProxyPort; port > 0 { - cyan.Printf("[roxie] Central UI: http://localhost:%d (username: admin, password: see $ROX_ADMIN_PASSWORD)\n", port) + cyan.Fprintf(w, "[roxie] Central UI: http://localhost:%d (username: admin, password: see $ROX_ADMIN_PASSWORD)\n", port) } else if centralDeploymentInfo.Exposure != types.ExposureNone { - cyan.Printf("[roxie] Central UI: https://%s", centralDeploymentInfo.Endpoint) + cyan.Fprintf(w, "[roxie] Central UI: https://%s\n", centralDeploymentInfo.Endpoint) } else if !env.RunningInRoxieContainer { - cyan.Println("[roxie] Note: Installing haproxy enables automatic HTTP access to Central through a forwarded local port") + cyan.Fprintln(w, "[roxie] Note: Installing haproxy enables automatic HTTP access to Central through a forwarded local port") } - cyan.Println("") + cyan.Fprintln(w, "") } From a08428655bcdb54e6a8c3f11da0b3372c2a0e73d Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 2 Nov 2026 12:34:32 +0100 Subject: [PATCH 06/15] Don't allow command arguments in --eval --- cmd/shell.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmd/shell.go b/cmd/shell.go index dcbe6fb8..0ebeca34 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -53,6 +53,10 @@ Examples: } func runShell(args []string) error { + if evalMode && len(args) > 0 { + return errors.New("--eval cannot be combined with command arguments") + } + if err := env.Initialize(); err != nil { return err } From a2eda40001458c5440bd56afe67450b6f94e5ae7 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 2 Nov 2026 12:43:10 +0100 Subject: [PATCH 07/15] Safer shell quoting --- cmd/subshell.go | 3 ++- internal/deployer/deployer.go | 3 ++- internal/helpers/helpers.go | 6 ++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/cmd/subshell.go b/cmd/subshell.go index f3ef8bfb..1495b4ec 100644 --- a/cmd/subshell.go +++ b/cmd/subshell.go @@ -12,6 +12,7 @@ import ( "github.com/stackrox/roxie/internal/deployer" "github.com/stackrox/roxie/internal/env" "github.com/stackrox/roxie/internal/haproxy" + "github.com/stackrox/roxie/internal/helpers" log "github.com/stackrox/roxie/internal/logger" "github.com/stackrox/roxie/internal/roxieenv" "github.com/stackrox/roxie/internal/types" @@ -195,7 +196,7 @@ func isHAProxyAvailable() bool { func printExports(centralDeploymentInfo types.CentralDeploymentInfo) { for name, val := range roxieenv.AssembleRoxieEnvironment(centralDeploymentInfo).Export() { - fmt.Fprintf(os.Stdout, "export %s=%q\n", name, val) + fmt.Fprintf(os.Stdout, "export %s=%s\n", name, helpers.ShellQuote(val)) } } diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index a54b2408..cf8db77e 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -16,6 +16,7 @@ import ( "github.com/fatih/color" "github.com/stackrox/roxie/internal/component" + "github.com/stackrox/roxie/internal/helpers" "github.com/stackrox/roxie/internal/containerrt" "github.com/stackrox/roxie/internal/dockerauth" "github.com/stackrox/roxie/internal/env" @@ -775,7 +776,7 @@ func (d *Deployer) cleanupTempDir(path string, description string) { func (d *Deployer) writeEnvrcFile(ctx context.Context, filename string) error { var content strings.Builder for name, val := range roxieenv.AssembleRoxieEnvironment(d.GetCentralDeploymentInfo()).Export() { - fmt.Fprintf(&content, "export %s=%q\n", name, val) + fmt.Fprintf(&content, "export %s=%s\n", name, helpers.ShellQuote(val)) } if d.portForwardPID != 0 { diff --git a/internal/helpers/helpers.go b/internal/helpers/helpers.go index 4c086058..2a4602d0 100644 --- a/internal/helpers/helpers.go +++ b/internal/helpers/helpers.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "reflect" + "strings" "gopkg.in/yaml.v3" ) @@ -150,3 +151,8 @@ func IsNil(i interface{}) bool { } return false } + +// ShellQuote wraps s in POSIX single quotes, safe for use in shell eval contexts. +func ShellQuote(s string) string { + return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'" +} From ad536a938e768ca9e1d2b1d68e9c4a799339e86d Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 2 Nov 2026 12:44:42 +0100 Subject: [PATCH 08/15] go mod tidy --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index af447ad0..86aad35a 100644 --- a/go.mod +++ b/go.mod @@ -114,7 +114,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.19.2 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.20 github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/moby/api v1.56.1 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect From 52a1ca4c91c4dc9c809d6089001af3873839cc00 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 2 Nov 2026 13:09:36 +0100 Subject: [PATCH 09/15] Fix pre-existing issue: start a port-forwarder for 'roxie shell' command --- cmd/shell.go | 21 +++++++++++++++++++++ internal/deployer/deployer.go | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/cmd/shell.go b/cmd/shell.go index 0ebeca34..9d79503c 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -10,8 +10,10 @@ import ( "github.com/spf13/cobra" "github.com/stackrox/roxie/internal/env" + "github.com/stackrox/roxie/internal/k8s" log "github.com/stackrox/roxie/internal/logger" "github.com/stackrox/roxie/internal/manifest" + "github.com/stackrox/roxie/internal/portforward" ) func newShellCmd() *cobra.Command { @@ -96,6 +98,25 @@ func runShell(args []string) error { return fmt.Errorf("extracting central deployment info from manifest: %w", err) } + if config.Central.PortForwardingEnabled() { + portForwarder := portforward.New(k8s.GetKubectl()) + if evalMode { + endpoint, _, err := portForwarder.StartDetached(config.Central.Namespace, "central", 443, 8443) + if err != nil { + return fmt.Errorf("failed to start port-forward: %w", err) + } + centralDeploymentInfo.Endpoint = endpoint + } else { + endpoint, err := portForwarder.Start(config.Central.Namespace, "central", 443, 8443) + if err != nil { + return fmt.Errorf("failed to start port-forward: %w", err) + } + defer portForwarder.Stop() + centralDeploymentInfo.Endpoint = endpoint + } + log.Infof("Port-forwarding Central at %s", centralDeploymentInfo.Endpoint) + } + if evalMode { printBanner(os.Stderr, centralDeploymentInfo) printExports(centralDeploymentInfo) diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index cf8db77e..19c71b20 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -16,10 +16,10 @@ import ( "github.com/fatih/color" "github.com/stackrox/roxie/internal/component" - "github.com/stackrox/roxie/internal/helpers" "github.com/stackrox/roxie/internal/containerrt" "github.com/stackrox/roxie/internal/dockerauth" "github.com/stackrox/roxie/internal/env" + "github.com/stackrox/roxie/internal/helpers" "github.com/stackrox/roxie/internal/imagecache" "github.com/stackrox/roxie/internal/k8s" log "github.com/stackrox/roxie/internal/logger" From 38f193d2aaa5d3ca0d1194f80c4ebb41705179a2 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 2 Nov 2026 13:20:11 +0100 Subject: [PATCH 10/15] Only enable evalMode if val is true --- cmd/deploy.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/deploy.go b/cmd/deploy.go index 4ddd5863..3c31be5e 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -215,8 +215,10 @@ this flag can be used to tell roxie how to pre-load images for the current clust registerFlag(cmd, settings, "eval", "Print environment as eval-able export statements to stdout", withNoOptDefVal("true"), withApplyFnBool(func(config *deployer.Config, val bool) error { - evalMode = true - config.Roxie.Envrc = new("/dev/stdout") + if val { + evalMode = true + config.Roxie.Envrc = new("/dev/stdout") + } return nil }), ) From cb92a21d9183ee07061ab04a9f1326b36b394825 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 2 Nov 2026 21:00:42 +0100 Subject: [PATCH 11/15] Fix e2e helper after the shell quotation changes --- tests/e2e/helpers.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/e2e/helpers.go b/tests/e2e/helpers.go index 05fb2262..fdf4accc 100644 --- a/tests/e2e/helpers.go +++ b/tests/e2e/helpers.go @@ -249,8 +249,9 @@ func loadEnvrcFile(path string) (map[string]string, error) { key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) - // Remove quotes - value = strings.Trim(value, "\"") + // Remove quotes added by roxie. + value = strings.TrimPrefix(value, "'") + value = strings.TrimSuffix(value, "'") env[key] = value } From a621caf141b01e83421a8bc90aecbe1df5cb58b0 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 2 Nov 2026 21:06:03 +0100 Subject: [PATCH 12/15] Export port forwarder pid in roxie shell --eval mode --- cmd/shell.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cmd/shell.go b/cmd/shell.go index 9d79503c..121242e1 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -98,14 +98,16 @@ func runShell(args []string) error { return fmt.Errorf("extracting central deployment info from manifest: %w", err) } + var portForwardPID int if config.Central.PortForwardingEnabled() { portForwarder := portforward.New(k8s.GetKubectl()) if evalMode { - endpoint, _, err := portForwarder.StartDetached(config.Central.Namespace, "central", 443, 8443) + endpoint, pid, err := portForwarder.StartDetached(config.Central.Namespace, "central", 443, 8443) if err != nil { return fmt.Errorf("failed to start port-forward: %w", err) } centralDeploymentInfo.Endpoint = endpoint + portForwardPID = pid } else { endpoint, err := portForwarder.Start(config.Central.Namespace, "central", 443, 8443) if err != nil { @@ -120,6 +122,9 @@ func runShell(args []string) error { if evalMode { printBanner(os.Stderr, centralDeploymentInfo) printExports(centralDeploymentInfo) + if portForwardPID != 0 { + fmt.Fprintf(os.Stdout, "export ROXIE_PORT_FORWARD_PID=%d\n", portForwardPID) + } return nil } From 520695947b62c0c941729c80a51442a79613260c Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Tue, 3 Nov 2026 08:49:11 +0100 Subject: [PATCH 13/15] Make loadEnvrcFile more reliable --- tests/e2e/basic_test.go | 3 +-- tests/e2e/custom_tls_test.go | 3 +-- tests/e2e/helpers.go | 43 ++++++++++++------------------------ tests/e2e/olm_switch_test.go | 5 +---- 4 files changed, 17 insertions(+), 37 deletions(-) diff --git a/tests/e2e/basic_test.go b/tests/e2e/basic_test.go index 99eed025..7e5c8428 100644 --- a/tests/e2e/basic_test.go +++ b/tests/e2e/basic_test.go @@ -58,8 +58,7 @@ func TestDetachedPortForwarding(t *testing.T) { args := append([]string{roxieBinary, "deploy", "--early-readiness", "central", "--exposure=none", "--port-forwarding", "--envrc", envrcPath}, commonDeployArgs...) runCommand(t, deployTimeout, nil, args...) - env, err := loadEnvrcFile(envrcPath) - require.NoError(t, err, "Failed to load envrc file") + env := loadEnvrcFile(t, envrcPath) pidStr, ok := env["ROXIE_PORT_FORWARD_PID"] require.True(t, ok, "ROXIE_PORT_FORWARD_PID not set in envrc") pid, err := strconv.Atoi(pidStr) diff --git a/tests/e2e/custom_tls_test.go b/tests/e2e/custom_tls_test.go index 3bb9fff1..0085bf65 100644 --- a/tests/e2e/custom_tls_test.go +++ b/tests/e2e/custom_tls_test.go @@ -48,8 +48,7 @@ func TestDeployWithCustomTLSCert(t *testing.T) { verifyCentralInstalled(t, sharedNamespace) verifySecuredClusterInstalled(t, sharedNamespace) - env, err := loadEnvrcFile(envrcPath) - require.NoError(t, err, "Failed to load envrc file") + env := loadEnvrcFile(t, envrcPath) require.NotEmpty(t, env["ROX_CA_CERT_FILE"], "ROX_CA_CERT_FILE should be set in envrc") verifyCACertFileContainsCustomCA(t, env["ROX_CA_CERT_FILE"], caCertPEM) diff --git a/tests/e2e/helpers.go b/tests/e2e/helpers.go index fdf4accc..db160aa9 100644 --- a/tests/e2e/helpers.go +++ b/tests/e2e/helpers.go @@ -223,40 +223,25 @@ func doesDeploymentExist(t *testing.T, namespace string, name string) bool { return strings.Contains(string(output), name) } -func loadEnvrcFile(path string) (map[string]string, error) { - content, err := os.ReadFile(path) - if err != nil { - return nil, err - } - - env := make(map[string]string) - lines := strings.Split(string(content), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" || !strings.HasPrefix(line, "export ") { - continue - } +// loadEnvrcFile sources the envrc file in a real shell and returns the +// exported variables. +func loadEnvrcFile(t *testing.T, path string) map[string]string { + t.Helper() - // Remove "export " prefix - line = strings.TrimPrefix(line, "export ") + // Extract shell's environment after loading roxie's envrc file. + cmd := exec.Command("sh", "-c", fmt.Sprintf(". %q >&2 && env", path)) + out, err := cmd.Output() + require.NoError(t, err, "Failed to source envrc file %s", path) - // Split on first = + // Collect the environment set by the shell. + env := make(map[string]string) + for _, line := range strings.Split(string(out), "\n") { parts := strings.SplitN(line, "=", 2) - if len(parts) != 2 { - continue + if len(parts) == 2 { + env[parts[0]] = parts[1] } - - key := strings.TrimSpace(parts[0]) - value := strings.TrimSpace(parts[1]) - - // Remove quotes added by roxie. - value = strings.TrimPrefix(value, "'") - value = strings.TrimSuffix(value, "'") - - env[key] = value } - - return env, nil + return env } func verifyCentralInstalled(t *testing.T, namespace string) { diff --git a/tests/e2e/olm_switch_test.go b/tests/e2e/olm_switch_test.go index 850ad758..d7103a45 100644 --- a/tests/e2e/olm_switch_test.go +++ b/tests/e2e/olm_switch_test.go @@ -215,10 +215,7 @@ func TestSecuredClusterWithOLMSwitch(t *testing.T) { verifyNamespaceExists(t, centralNamespace) // Load envrc for secured-cluster - envrcEnv, err := loadEnvrcFile(envrcPath) - if err != nil { - t.Fatalf("Failed to load envrc file: %v", err) - } + envrcEnv := loadEnvrcFile(t, envrcPath) // Step 2: Deploy secured-cluster (should reuse OLM operator) t.Log("=== Step 2: Deploy secured-cluster (should reuse OLM operator) ===") From e4c511fe87ee0f9dbe4a64b588f7d83bafdb072c Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Tue, 3 Nov 2026 11:42:41 +0100 Subject: [PATCH 14/15] (CI trigger) From c402976a2a955f483e223605893a22127c6a6ada Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier <111092021+mclasmeier@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:06:51 +0100 Subject: [PATCH 15/15] Update tests/e2e/helpers.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- tests/e2e/helpers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/helpers.go b/tests/e2e/helpers.go index db160aa9..6ed694db 100644 --- a/tests/e2e/helpers.go +++ b/tests/e2e/helpers.go @@ -229,7 +229,7 @@ func loadEnvrcFile(t *testing.T, path string) map[string]string { t.Helper() // Extract shell's environment after loading roxie's envrc file. - cmd := exec.Command("sh", "-c", fmt.Sprintf(". %q >&2 && env", path)) + cmd := exec.Command("sh", "-c", `. "$1" >&2 && env`, "sh", path) out, err := cmd.Output() require.NoError(t, err, "Failed to source envrc file %s", path)