Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1
Implement 'roxie shell' command for (re-)connecting to central deployment#201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
4d164d9dc73a007cd91692eccf1f02646014e6e23b873f2631c4c01738a8c668740688004f44e0ac32d3059c3e3ae911831560e3eef94028File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| 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'`, | ||
| 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) | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| }, | ||
| 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) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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 { | ||
| @@ -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]") | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.