Skip to content
Open
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
15 changes: 14 additions & 1 deletion cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"github.com/stackrox/roxie/internal/env"
"github.com/stackrox/roxie/internal/helpers"
"github.com/stackrox/roxie/internal/logger"
"github.com/stackrox/roxie/internal/manifest"
"github.com/stackrox/roxie/internal/roxieenv"
"github.com/stackrox/roxie/internal/types"

"github.com/stackrox/roxie/internal/stackroxversions"
Expand Down Expand Up @@ -337,8 +339,19 @@ func runDeploy(cmd *cobra.Command, args []string) error {
d.WaitForCentral(5 * time.Minute)
}

if components.IncludesCentral() && !dryRun {
roxieEnv := roxieenv.AssembleRoxieEnvironment(d.GetCentralDeploymentInfo())
m := manifest.RoxieManifest{
RoxieEnvironment: roxieEnv,
Config: deploySettings,
}
if err := manifest.CreateManifestSecretOnCluster(ctx, log, m); err != nil {
log.Warningf("Failed to save roxie manifest: %v", err)
}
}

if components.IncludesCentral() && envrc == "" {
if err := spawnSubshell(d, log); err != nil {
if err := spawnSubshellForDeployerEnv(d, log); err != nil {
return fmt.Errorf("failed to spawn subshell: %w", err)
}
}
Expand Down
1 change: 1 addition & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func init() {
rootCmd.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "Do not actually modify cluster")
rootCmd.AddCommand(newDeployCmd(&deploySettings))
rootCmd.AddCommand(newTeardownCmd(&deploySettings))
rootCmd.AddCommand(newShellCmd())
rootCmd.AddCommand(newVersionCmd())
rootCmd.AddCommand(newEnvCmd())
rootCmd.AddCommand(newLogsCmd())
Expand Down
88 changes: 88 additions & 0 deletions cmd/shell.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package main

import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"time"

"github.com/spf13/cobra"
"github.com/stackrox/roxie/internal/env"
"github.com/stackrox/roxie/internal/logger"
"github.com/stackrox/roxie/internal/manifest"
)

func newShellCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "shell [-- command [args...]]",
Short: "Open a subshell for an existing ACS Central deployment",
Long: `Open an interactive subshell with ACS environment variables
set for an existing ACS Central deployment.

This command reads the roxie manifest secret from the cluster,
re-fetches the CA certificate, and spawns an interactive subshell
with the environment variables set.

If a command is given after "--", it is executed in the modified environment
instead of spawning a subshell.

Examples:
roxie shell
roxie shell -- roxctl central whoami
roxie shell -- bash -c 'echo $ROX_ENDPOINT'`,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Run: func(cmd *cobra.Command, args []string) {
err := runShell(cmd, args)
if err != nil {
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok {
// Propagate exit error from the child process.
os.Exit(exitErr.ExitCode())
}
cmd.PrintErrln(err)
os.Exit(1)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
},
DisableFlagParsing: false,
}

cmd.Flags().StringVar(&shell, "shell", "", "Shell to spawn")

return cmd
}

func runShell(cmd *cobra.Command, args []string) error {
log := logger.New()
if err := env.Initialize(log); err != nil {
return err
}

if os.Getenv("ROXIE_SHELL") != "" {
return errors.New("already in a roxie sub-shell (ROXIE_SHELL environment variable is set), please exit the shell and try again")
}

log.Info("Loading manifest from cluster...")

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()

m, err := manifest.LoadManifestSecret(ctx, log)
if err != nil {
return fmt.Errorf("failed to load roxie manifest: %w", err)
}
log.Dim("roxie manifest loaded")

// We need this for the setup of the CA cert.
tempDir, err := os.MkdirTemp("", "roxie-shell-*")
if err != nil {
return fmt.Errorf("failed to create temp dir: %w", err)
}
defer os.RemoveAll(tempDir)

centralDeploymentInfo, err := manifest.ManifestToCentralDeploymentInfo(ctx, log, tempDir, m)
if err != nil {
return fmt.Errorf("extracting central deployment info from manifest: %w", err)
}

return runCommandOrSubshell(centralDeploymentInfo, log, args)
}
136 changes: 71 additions & 65 deletions cmd/subshell.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,94 +11,100 @@ import (
"github.com/stackrox/roxie/internal/deployer"
"github.com/stackrox/roxie/internal/env"
"github.com/stackrox/roxie/internal/logger"
"github.com/stackrox/roxie/internal/roxieenv"
"github.com/stackrox/roxie/internal/types"
)

func spawnSubshell(d *deployer.Deployer, log *logger.Logger) error {
shellPath := shell
if shellPath == "" {
shellPath = os.Getenv("ROXIE_USER_SHELL")
}
if shellPath == "" {
shellPath = os.Getenv("SHELL")
}
if shellPath == "" {
shellPath = "/bin/bash"
}

log.Infof("Spawning sub-shell: %s", shellPath)

env := os.Environ()

centralDeploymentInfo := d.GetCentralDeploymentInfo()

if centralDeploymentInfo.Endpoint != "" {
env = append(env, fmt.Sprintf("API_ENDPOINT=%s", centralDeploymentInfo.Endpoint))
env = append(env, fmt.Sprintf("ROX_ENDPOINT=%s", centralDeploymentInfo.Endpoint))
env = append(env, fmt.Sprintf("ROX_BASE_URL=https://%s", centralDeploymentInfo.Endpoint))
}

if centralDeploymentInfo.Password != "" {
env = append(env, fmt.Sprintf("ROX_ADMIN_PASSWORD=%s", centralDeploymentInfo.Password))
}
// spawnSubshellForDeployerEnv assembles the roxie environment from a Deployer and invokes an interactive subshell.
func spawnSubshellForDeployerEnv(d *deployer.Deployer, log *logger.Logger) error {
return runCommandOrSubshell(d.GetCentralDeploymentInfo(), log, nil)
}

if centralDeploymentInfo.CACertFile != "" {
env = append(env, fmt.Sprintf("ROX_CA_CERT_FILE=%s", centralDeploymentInfo.CACertFile))
// runCommandOrSubshell spawns an interactive subshell or runs the provided command using the given
// central deployment info.
// It handles HAProxy setup, prints the connection banner, and manages shell lifecycle.
func runCommandOrSubshell(centralDeploymentInfo types.CentralDeploymentInfo, log *logger.Logger, args []string) error {
cmdEnv := os.Environ()
for name, val := range roxieenv.AssembleRoxieEnvironment(centralDeploymentInfo).Export() {
cmdEnv = append(cmdEnv, fmt.Sprintf("%s=%s", name, val))
}

env = append(env, fmt.Sprintf("ROX_USERNAME=%s", deployer.AdminUsername))
env = append(env, "ROXIE_SHELL=1")
env = append(env, fmt.Sprintf("name=acs@%s", centralDeploymentInfo.KubeContext))
cmdEnv = append(cmdEnv, "ROXIE_SHELL=1")
cmdEnv = append(cmdEnv, fmt.Sprintf("name=acs@%s", centralDeploymentInfo.KubeContext))

haproxyAvailable := isHAProxyAvailable()

var haproxyCmd *exec.Cmd
var haproxyConfigPath string

if haproxyAvailable && centralDeploymentInfo.Endpoint != "" && centralDeploymentInfo.CACertFile != "" {
var err error
haproxyCmd, haproxyConfigPath, err = startHAProxy(centralDeploymentInfo.Endpoint, centralDeploymentInfo.CACertFile, log)
haproxyCmd, haproxyConfigPath, err := startHAProxy(centralDeploymentInfo.Endpoint, centralDeploymentInfo.CACertFile, log)
if err != nil {
log.Warningf("Failed to start HAProxy: %v", err)
} else {
env = append(env, fmt.Sprintf("ROXIE_HAPROXY_CFG_FILE=%s", haproxyConfigPath))
cmdEnv = append(cmdEnv, "ROXIE_HAPROXY_CFG_FILE="+haproxyConfigPath)
centralDeploymentInfo.HAProxyStarted = true
defer cleanupHAProxy(haproxyCmd, haproxyConfigPath)
}
}

printBanner(centralDeploymentInfo)

shellCmd := exec.Command(shellPath, "-i")
shellCmd.Env = env
shellCmd.Stdin = os.Stdin
shellCmd.Stdout = os.Stdout
shellCmd.Stderr = os.Stderr

err := shellCmd.Run()
var cmd *exec.Cmd

// Print exit message
cyan := color.New(color.FgCyan, color.Bold)
cyan.Println("\n[roxie] Exited subshell. You are now back in your original shell.")
cyan.Println("")

// Don't treat shell exit as an error - shells can exit with non-zero status
// for various reasons (like the last command failing) which is normal behavior
if err != nil {
// Check if it's a normal exit (exit code from the shell)
if exitErr, ok := err.(*exec.ExitError); ok {
// Shell exited (could be normal exit or last command failed)
// This is not an error condition for roxie - the subshell worked fine
_ = exitErr // Acknowledge we handled this
return nil
if subShellMode(args) {
shellPath := resolveShellPath()
log.Infof("Spawning sub-shell: %s", shellPath)
printBanner(centralDeploymentInfo)
cmd = exec.Command(shellPath, "-i")
} else {
// args is non-empty.
cmd = exec.Command(args[0], args[1:]...)
}
cmd.Env = cmdEnv
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

err := cmd.Run()

if subShellMode(args) {
cyan := color.New(color.FgCyan, color.Bold)
cyan.Println("")
cyan.Println("[roxie] Exited subshell. You are now back in your original shell.")
cyan.Println("[roxie] If you accidentally closed the roxie subshell, you can use `roxie shell` to re-open it.")
cyan.Println("")

// Don't treat shell exit as an error - shells can exit with non-zero status
// for various reasons (like the last command failing) which is normal behavior
if err != nil {
// Check if it's a normal exit (exit code from the shell)
if _, ok := err.(*exec.ExitError); ok {
return nil
}
// Only return error if we couldn't even start the shell
return fmt.Errorf("failed to run subshell: %w", err)
}
} else {
if err != nil {
return fmt.Errorf("failed to execute command: %w", err)
}
// Only return error if we couldn't even start the shell
return fmt.Errorf("failed to run subshell: %w", err)
}

return nil
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func subShellMode(args []string) bool {
return len(args) == 0
}

func resolveShellPath() string {
if shell != "" {
return shell
}
if s := os.Getenv("ROXIE_USER_SHELL"); s != "" {
return s
}
if s := os.Getenv("SHELL"); s != "" {
return s
}
return "/bin/bash"
}

func startHAProxy(endpoint, caCertFile string, log *logger.Logger) (*exec.Cmd, string, error) {
configFile, err := os.CreateTemp("", "roxie-haproxy-*.cfg")
if err != nil {
Expand Down Expand Up @@ -171,7 +177,7 @@ func isHAProxyAvailable() bool {
return err == nil
}

func printBanner(centralDeploymentInfo deployer.CentralDeploymentInfo) {
func printBanner(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]")
Expand Down
12 changes: 12 additions & 0 deletions cmd/teardown.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/stackrox/roxie/internal/deployer"
"github.com/stackrox/roxie/internal/env"
"github.com/stackrox/roxie/internal/logger"
"github.com/stackrox/roxie/internal/manifest"
)

func newTeardownCmd(settings *deployer.Config) *cobra.Command {
Expand Down Expand Up @@ -70,6 +71,17 @@ func runTeardown(cmd *cobra.Command, args []string) error {
return fmt.Errorf("teardown failed: %w", err)
}

if components.IncludesCentral() {
if err := manifest.DeleteManifestSecret(ctx, log); err != nil {
log.Warningf("Failed to delete roxie manifest: %v", err)
}
}
if components == component.All {
if err := manifest.DeleteRoxieNamespace(ctx, log); err != nil {
log.Warningf("Failed to delete roxie namespace: %v", err)
}
}

log.Success("🎉 Teardown complete!")

return nil
Expand Down
25 changes: 8 additions & 17 deletions internal/deployer/deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/stackrox/roxie/internal/k8s"
"github.com/stackrox/roxie/internal/logger"
"github.com/stackrox/roxie/internal/portforward"
"github.com/stackrox/roxie/internal/roxieenv"
"github.com/stackrox/roxie/internal/types"
)

Expand Down Expand Up @@ -704,12 +705,10 @@ func (d *Deployer) cleanupTempDir(path string, description string) {

func (d *Deployer) writeEnvrcFile(ctx context.Context) error {
var content strings.Builder
fmt.Fprintf(&content, "export API_ENDPOINT=%q\n", d.centralEndpoint)
fmt.Fprintf(&content, "export ROX_ENDPOINT=%q\n", d.centralEndpoint)
fmt.Fprintf(&content, "export ROX_BASE_URL='https://%s'\n", d.centralEndpoint)
fmt.Fprintf(&content, "export ROX_USERNAME=%q\n", AdminUsername)
fmt.Fprintf(&content, "export ROX_ADMIN_PASSWORD=%q\n", d.centralPassword)
fmt.Fprintf(&content, "export ROX_CA_CERT_FILE=%q\n", d.roxCACertFile)
for name, val := range roxieenv.AssembleRoxieEnvironment(d.GetCentralDeploymentInfo()).Export() {
fmt.Fprintf(&content, "export %s=%q\n", name, val)
}

if d.portForwardPID != 0 {
fmt.Fprintf(&content, "export ROXIE_PORT_FORWARD_PID=%d\n", d.portForwardPID)
}
Expand Down Expand Up @@ -969,18 +968,10 @@ func (d *Deployer) PrintSecuredClusterDeploymentSummary() {
log.Info("")
}

type CentralDeploymentInfo struct {
Endpoint string
Password string
KubeContext string
Exposure types.Exposure
CACertFile string
HAProxyStarted bool
}

func (d *Deployer) GetCentralDeploymentInfo() CentralDeploymentInfo {
return CentralDeploymentInfo{
func (d *Deployer) GetCentralDeploymentInfo() types.CentralDeploymentInfo {
return types.CentralDeploymentInfo{
Endpoint: d.centralEndpoint,
Username: AdminUsername,
Password: d.centralPassword,
KubeContext: d.kubeContext,
Exposure: d.config.Central.GetExposure(),
Expand Down
Loading
Loading