Skip to content

Commit 08a0245

Browse files
ajp-ioclaude
andauthored
Add -c/--command flag to shell command (#2997)
* Add -c/--command flag to shell command for non-interactive execution This enables running commands in the embedded cluster environment without opening an interactive shell, making it easier to use in CI/automation. Usage: # Interactive mode (existing behavior) embedded-cluster shell # Command execution mode (new) embedded-cluster shell -c "kubectl get po" embedded-cluster shell -c "kubectl get po && kubectl get nodes" Exit codes are properly preserved for CI/CD pipelines. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * Use errors.As() for better error handling Replace type assertion with errors.As() when checking for exec.ExitError. This properly handles wrapped errors and follows Go 1.13+ best practices. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent 4e06c3c commit 08a0245

File tree

1 file changed

+110
-59
lines changed

1 file changed

+110
-59
lines changed

cmd/installer/cli/shell.go

Lines changed: 110 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"io"
78
"os"
@@ -30,6 +31,7 @@ const welcome = `
3031

3132
func ShellCmd(ctx context.Context, appTitle string) *cobra.Command {
3233
var rc runtimeconfig.RuntimeConfig
34+
var command string
3335

3436
cmd := &cobra.Command{
3537
Use: "shell",
@@ -55,72 +57,121 @@ func ShellCmd(ctx context.Context, appTitle string) *cobra.Command {
5557
shpath = "/bin/bash"
5658
}
5759

58-
fmt.Printf(welcome, runtimeconfig.AppSlug())
59-
shell := exec.Command(shpath)
60-
shell.Env = os.Environ()
61-
62-
// get the current working directory
63-
var err error
64-
shell.Dir, err = os.Getwd()
65-
if err != nil {
66-
return fmt.Errorf("unable to get current working directory: %w", err)
60+
// Command execution mode
61+
if command != "" {
62+
return executeCommand(shpath, command, rc)
6763
}
6864

69-
shellpty, err := pty.Start(shell)
70-
if err != nil {
71-
return fmt.Errorf("unable to start shell: %w", err)
72-
}
65+
// Interactive shell mode
66+
return openInteractiveShell(shpath, rc)
67+
},
68+
}
7369

74-
sigch := make(chan os.Signal, 1)
75-
signal.Notify(sigch, syscall.SIGWINCH)
76-
go handleResize(sigch, shellpty)
77-
sigch <- syscall.SIGWINCH
78-
state, err := term.MakeRaw(int(os.Stdin.Fd()))
79-
if err != nil {
80-
return fmt.Errorf("unable to make raw terminal: %w", err)
81-
}
70+
cmd.Flags().StringVarP(&command, "command", "c", "", "Command to execute in the shell environment instead of opening an interactive shell")
8271

83-
defer func() {
84-
signal.Stop(sigch)
85-
close(sigch)
86-
fd := int(os.Stdin.Fd())
87-
_ = term.Restore(fd, state)
88-
}()
89-
90-
kcpath := rc.PathToKubeConfig()
91-
config := fmt.Sprintf("export KUBECONFIG=%q\n", kcpath)
92-
_, _ = shellpty.WriteString(config)
93-
_, _ = io.CopyN(io.Discard, shellpty, int64(len(config)+1))
94-
95-
bindir := rc.EmbeddedClusterBinsSubDir()
96-
config = fmt.Sprintf("export PATH=\"$PATH:%s\"\n", bindir)
97-
_, _ = shellpty.WriteString(config)
98-
_, _ = io.CopyN(io.Discard, shellpty, int64(len(config)+1))
99-
100-
// if /etc/bash_completion is present enable kubectl auto completion.
101-
if _, err := os.Stat("/etc/bash_completion"); err == nil {
102-
config = fmt.Sprintf("source <(k0s completion %s)\n", filepath.Base(shpath))
103-
_, _ = shellpty.WriteString(config)
104-
_, _ = io.CopyN(io.Discard, shellpty, int64(len(config)+1))
105-
106-
comppath := rc.PathToEmbeddedClusterBinary("kubectl_completion_bash.sh")
107-
config = fmt.Sprintf("source <(cat %s)\n", comppath)
108-
_, _ = shellpty.WriteString(config)
109-
_, _ = io.CopyN(io.Discard, shellpty, int64(len(config)+1))
110-
111-
config = "source /etc/bash_completion\n"
112-
_, _ = shellpty.WriteString(config)
113-
_, _ = io.CopyN(io.Discard, shellpty, int64(len(config)+1))
114-
}
72+
return cmd
73+
}
11574

116-
go func() { _, _ = io.Copy(shellpty, os.Stdin) }()
117-
go func() { _, _ = io.Copy(os.Stdout, shellpty) }()
118-
_ = shell.Wait()
119-
return nil
120-
},
75+
// executeCommand executes a command in the shell with the embedded cluster environment configured.
76+
func executeCommand(shpath string, command string, rc runtimeconfig.RuntimeConfig) error {
77+
// Build the command with environment setup
78+
shell := exec.Command(shpath, "-c", command)
79+
80+
// Set environment variables
81+
shell.Env = os.Environ()
82+
kcpath := rc.PathToKubeConfig()
83+
shell.Env = append(shell.Env, fmt.Sprintf("KUBECONFIG=%s", kcpath))
84+
bindir := rc.EmbeddedClusterBinsSubDir()
85+
shell.Env = append(shell.Env, fmt.Sprintf("PATH=%s:%s", os.Getenv("PATH"), bindir))
86+
87+
// Set working directory
88+
var err error
89+
shell.Dir, err = os.Getwd()
90+
if err != nil {
91+
return fmt.Errorf("unable to get current working directory: %w", err)
12192
}
12293

123-
return cmd
94+
// Connect stdio
95+
shell.Stdin = os.Stdin
96+
shell.Stdout = os.Stdout
97+
shell.Stderr = os.Stderr
98+
99+
// Execute and return exit code
100+
if err := shell.Run(); err != nil {
101+
// Preserve exit code from the command
102+
var exitErr *exec.ExitError
103+
if errors.As(err, &exitErr) {
104+
os.Exit(exitErr.ExitCode())
105+
}
106+
return err
107+
}
108+
109+
return nil
110+
}
111+
112+
// openInteractiveShell opens an interactive shell with the embedded cluster environment configured.
113+
func openInteractiveShell(shpath string, rc runtimeconfig.RuntimeConfig) error {
114+
fmt.Printf(welcome, runtimeconfig.AppSlug())
115+
shell := exec.Command(shpath)
116+
shell.Env = os.Environ()
117+
118+
var err error
119+
shell.Dir, err = os.Getwd()
120+
if err != nil {
121+
return fmt.Errorf("unable to get current working directory: %w", err)
122+
}
123+
124+
shellpty, err := pty.Start(shell)
125+
if err != nil {
126+
return fmt.Errorf("unable to start shell: %w", err)
127+
}
128+
129+
sigch := make(chan os.Signal, 1)
130+
signal.Notify(sigch, syscall.SIGWINCH)
131+
go handleResize(sigch, shellpty)
132+
sigch <- syscall.SIGWINCH
133+
state, err := term.MakeRaw(int(os.Stdin.Fd()))
134+
if err != nil {
135+
return fmt.Errorf("unable to make raw terminal: %w", err)
136+
}
137+
138+
defer func() {
139+
signal.Stop(sigch)
140+
close(sigch)
141+
fd := int(os.Stdin.Fd())
142+
_ = term.Restore(fd, state)
143+
}()
144+
145+
kcpath := rc.PathToKubeConfig()
146+
config := fmt.Sprintf("export KUBECONFIG=%q\n", kcpath)
147+
_, _ = shellpty.WriteString(config)
148+
_, _ = io.CopyN(io.Discard, shellpty, int64(len(config)+1))
149+
150+
bindir := rc.EmbeddedClusterBinsSubDir()
151+
config = fmt.Sprintf("export PATH=\"$PATH:%s\"\n", bindir)
152+
_, _ = shellpty.WriteString(config)
153+
_, _ = io.CopyN(io.Discard, shellpty, int64(len(config)+1))
154+
155+
// if /etc/bash_completion is present enable kubectl auto completion.
156+
if _, err := os.Stat("/etc/bash_completion"); err == nil {
157+
config = fmt.Sprintf("source <(k0s completion %s)\n", filepath.Base(shpath))
158+
_, _ = shellpty.WriteString(config)
159+
_, _ = io.CopyN(io.Discard, shellpty, int64(len(config)+1))
160+
161+
comppath := rc.PathToEmbeddedClusterBinary("kubectl_completion_bash.sh")
162+
config = fmt.Sprintf("source <(cat %s)\n", comppath)
163+
_, _ = shellpty.WriteString(config)
164+
_, _ = io.CopyN(io.Discard, shellpty, int64(len(config)+1))
165+
166+
config = "source /etc/bash_completion\n"
167+
_, _ = shellpty.WriteString(config)
168+
_, _ = io.CopyN(io.Discard, shellpty, int64(len(config)+1))
169+
}
170+
171+
go func() { _, _ = io.Copy(shellpty, os.Stdin) }()
172+
go func() { _, _ = io.Copy(os.Stdout, shellpty) }()
173+
_ = shell.Wait()
174+
return nil
124175
}
125176

126177
// handleResize is a helper function to handle pty resizes.

0 commit comments

Comments
 (0)