@@ -2,6 +2,7 @@ package cli
2
2
3
3
import (
4
4
"context"
5
+ "errors"
5
6
"fmt"
6
7
"io"
7
8
"os"
@@ -30,6 +31,7 @@ const welcome = `
30
31
31
32
func ShellCmd (ctx context.Context , appTitle string ) * cobra.Command {
32
33
var rc runtimeconfig.RuntimeConfig
34
+ var command string
33
35
34
36
cmd := & cobra.Command {
35
37
Use : "shell" ,
@@ -55,72 +57,121 @@ func ShellCmd(ctx context.Context, appTitle string) *cobra.Command {
55
57
shpath = "/bin/bash"
56
58
}
57
59
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 )
67
63
}
68
64
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
+ }
73
69
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" )
82
71
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
+ }
115
74
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 )
121
92
}
122
93
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
124
175
}
125
176
126
177
// handleResize is a helper function to handle pty resizes.
0 commit comments