Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,19 @@ 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 {
if val {
evalMode = true
config.Roxie.Envrc = new("/dev/stdout")
Comment thread
AlexVulaj marked this conversation as resolved.
Resolved
}
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" {
Expand Down Expand Up @@ -361,11 +374,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)
}
}
}
}

Expand Down Expand Up @@ -497,7 +527,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() {
Expand Down
9 changes: 9 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -24,6 +25,8 @@ var (
shell string
dryRun bool

evalMode bool

skipUserConfig bool

// We need this set up before command line flags are parsed.
Expand Down Expand Up @@ -78,6 +81,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,
Expand Down
41 changes: 40 additions & 1 deletion cmd/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -47,11 +49,16 @@ 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
}

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
}
Expand Down Expand Up @@ -82,12 +89,44 @@ 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)
}

var portForwardPID int
if config.Central.PortForwardingEnabled() {
portForwarder := portforward.New(k8s.GetKubectl())
if evalMode {
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 {
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)
if portForwardPID != 0 {
fmt.Fprintf(os.Stdout, "export ROXIE_PORT_FORWARD_PID=%d\n", portForwardPID)
}
return nil
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

return runCommandOrSubshell(config.Roxie, centralDeploymentInfo, args)
}
32 changes: 19 additions & 13 deletions cmd/subshell.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"io"
"os"
"os/exec"
"syscall"
Expand All @@ -11,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"
Expand Down Expand Up @@ -49,7 +51,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.
Expand Down Expand Up @@ -192,23 +194,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() {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
fmt.Fprintf(os.Stdout, "export %s=%s\n", name, helpers.ShellQuote(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, "")
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion internal/deployer/deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"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"
Expand Down Expand Up @@ -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))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

if d.portForwardPID != 0 {
Expand Down
6 changes: 6 additions & 0 deletions internal/helpers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"os/exec"
"reflect"
"strings"

"gopkg.in/yaml.v3"
)
Expand Down Expand Up @@ -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, "'", "'\\''") + "'"
}
17 changes: 5 additions & 12 deletions internal/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -172,7 +171,6 @@ type handler struct {
mu sync.Mutex
level *slog.LevelVar
startTime time.Time
stdout io.Writer
stderr io.Writer
}

Expand All @@ -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
}

Expand Down
18 changes: 9 additions & 9 deletions internal/logger/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:])
}
}

Expand Down
3 changes: 1 addition & 2 deletions tests/e2e/basic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 1 addition & 2 deletions tests/e2e/custom_tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading