8
8
"strings"
9
9
"time"
10
10
11
+ "github.com/manifoldco/promptui"
11
12
"github.com/spf13/cobra"
12
13
"golang.org/x/crypto/ssh/terminal"
13
14
"golang.org/x/time/rate"
@@ -65,9 +66,11 @@ func shValidArgs(cmd *cobra.Command, args []string) error {
65
66
66
67
func shCmd () * cobra.Command {
67
68
return & cobra.Command {
68
- Use : "sh [environment_name] [<command [args...]>]" ,
69
- Short : "Open a shell and execute commands in a Coder environment" ,
70
- Long : "Execute a remote command on the environment\\ nIf no command is specified, the default shell is opened." ,
69
+ Use : "sh [environment_name] [<command [args...]>]" ,
70
+ Short : "Open a shell and execute commands in a Coder environment" ,
71
+ Long : `Execute a remote command on the environment
72
+ If no command is specified, the default shell is opened.
73
+ If the command is run in an interactive shell, a user prompt will occur if the environment needs to be rebuilt.` ,
71
74
Args : shValidArgs ,
72
75
DisableFlagParsing : true ,
73
76
ValidArgsFunction : getEnvsForCompletion (coder .Me ),
@@ -92,7 +95,28 @@ func shell(cmd *cobra.Command, cmdArgs []string) error {
92
95
93
96
envName := cmdArgs [0 ]
94
97
95
- if err := runCommand (ctx , envName , command , args ); err != nil {
98
+ // Before the command is run, ensure the workspace is on and ready to accept
99
+ // an ssh connection.
100
+ client , err := newClient (ctx )
101
+ if err != nil {
102
+ return err
103
+ }
104
+
105
+ env , err := findEnv (ctx , client , envName , coder .Me )
106
+ if err != nil {
107
+ return err
108
+ }
109
+
110
+ // TODO: Verify this is the correct behavior
111
+ isInteractive := terminal .IsTerminal (int (os .Stdout .Fd ()))
112
+ if isInteractive { // checkAndRebuildEnvironment requires an interactive shell
113
+ // Checks & Rebuilds the environment if needed.
114
+ if err := checkAndRebuildEnvironment (ctx , client , env ); err != nil {
115
+ return err
116
+ }
117
+ }
118
+
119
+ if err := runCommand (ctx , client , env , command , args ); err != nil {
96
120
if exitErr , ok := err .(wsep.ExitError ); ok {
97
121
os .Exit (exitErr .Code )
98
122
}
@@ -101,6 +125,132 @@ func shell(cmd *cobra.Command, cmdArgs []string) error {
101
125
return nil
102
126
}
103
127
128
+ // rebuildPrompt returns function that prompts the user if they wish to
129
+ // rebuild the selected environment if a rebuild is needed. The returned prompt function will
130
+ // return an error if the user selects "no".
131
+ // This functions returns `nil` if there is no reason to prompt the user to rebuild
132
+ // the environment.
133
+ func rebuildPrompt (env * coder.Environment ) (prompt func () error ) {
134
+ // Option 1: If the environment is off, the rebuild is needed
135
+ if env .LatestStat .ContainerStatus == coder .EnvironmentOff {
136
+ confirm := promptui.Prompt {
137
+ Label : fmt .Sprintf ("Environment %q is \" OFF\" . Rebuild it now? (this can take several minutes" , env .Name ),
138
+ IsConfirm : true ,
139
+ }
140
+ return func () (err error ) {
141
+ _ , err = confirm .Run ()
142
+ return
143
+ }
144
+ }
145
+
146
+ // Option 2: If there are required rebuild messages, the rebuild is needed
147
+ var lines []string
148
+ for _ , r := range env .RebuildMessages {
149
+ if r .Required {
150
+ lines = append (lines , clog .Causef (r .Text ))
151
+ }
152
+ }
153
+
154
+ if len (lines ) > 0 {
155
+ confirm := promptui.Prompt {
156
+ Label : fmt .Sprintf ("Environment %q requires a rebuild to work correctly. Do you wish to rebuild it now? (this will take a moment)" , env .Name ),
157
+ IsConfirm : true ,
158
+ }
159
+ // This function also prints the reasons in a log statement.
160
+ // The confirm prompt does not handle new lines well in the label.
161
+ return func () (err error ) {
162
+ clog .LogWarn ("rebuild required" , lines ... )
163
+ _ , err = confirm .Run ()
164
+ return
165
+ }
166
+ }
167
+
168
+ // Environment looks good, no need to prompt the user.
169
+ return nil
170
+ }
171
+
172
+ // checkAndRebuildEnvironment will:
173
+ // 1. Check if an environment needs to be rebuilt to be used
174
+ // 2. Prompt the user if they want to rebuild the environment (returns an error if they do not)
175
+ // 3. Rebuilds the environment and waits for it to be 'ON'
176
+ // Conditions for rebuilding are:
177
+ // - Environment is offline
178
+ // - Environment has rebuild messages requiring a rebuild
179
+ func checkAndRebuildEnvironment (ctx context.Context , client * coder.Client , env * coder.Environment ) error {
180
+ var err error
181
+ rebuildPrompt := rebuildPrompt (env ) // Fetch the prompt for rebuilding envs w/ reason
182
+
183
+ switch {
184
+ // If this conditonal is true, a rebuild is **required** to make the sh command work.
185
+ case rebuildPrompt != nil :
186
+ // TODO: (@emyrk) I'd like to add a --force and --verbose flags to this command,
187
+ // but currently DisableFlagParsing is set to true.
188
+ // To enable force/verbose, we'd have to parse the flags ourselves,
189
+ // or make the user `coder sh <env> -- [args]`
190
+ //
191
+ if err := rebuildPrompt (); err != nil {
192
+ // User selected not to rebuild :(
193
+ return clog .Fatal (
194
+ "environment is not ready for use" ,
195
+ "environment requires a rebuild" ,
196
+ fmt .Sprintf ("its current status is %q" , env .LatestStat .ContainerStatus ),
197
+ clog .BlankLine ,
198
+ clog .Tipf ("run \" coder envs rebuild %s --follow\" to start the environment" , env .Name ),
199
+ )
200
+ }
201
+
202
+ // Start the rebuild
203
+ if err := client .RebuildEnvironment (ctx , env .ID ); err != nil {
204
+ return err
205
+ }
206
+
207
+ fallthrough // Fallthrough to watching the logs
208
+ case env .LatestStat .ContainerStatus == coder .EnvironmentCreating :
209
+ // Environment is in the process of being created, just trail the logs
210
+ // and wait until it is done
211
+ clog .LogInfo (fmt .Sprintf ("Rebuilding %q" , env .Name ))
212
+
213
+ // Watch the rebuild.
214
+ if err := trailBuildLogs (ctx , client , env .ID ); err != nil {
215
+ return err
216
+ }
217
+
218
+ // newline after trailBuildLogs to place user on a fresh line for their shell
219
+ fmt .Println ()
220
+
221
+ // At this point the buildlog is complete, and the status of the env should be 'ON'
222
+ env , err = client .EnvironmentByID (ctx , env .ID )
223
+ if err != nil {
224
+ // If this api call failed, it will likely fail again, no point to retry and make the user wait
225
+ return err
226
+ }
227
+
228
+ if env .LatestStat .ContainerStatus != coder .EnvironmentOn {
229
+ // This means we had a timeout
230
+ return clog .Fatal ("the environment rebuild ran into an issue" ,
231
+ fmt .Sprintf ("environment %q rebuild has failed and will not come online" , env .Name ),
232
+ fmt .Sprintf ("its current status is %q" , env .LatestStat .ContainerStatus ),
233
+ clog .BlankLine ,
234
+ // TODO: (@emyrk) can they check these logs from the cli? Isn't this the logs that
235
+ // I just showed them? I'm trying to decide what exactly to tell a user.
236
+ clog .Tipf ("take a look at the build logs to determine what went wrong" ),
237
+ )
238
+ }
239
+
240
+ case env .LatestStat .ContainerStatus == coder .EnvironmentFailed :
241
+ // A failed container might just keep re-failing. I think it should be investigated by the user
242
+ return clog .Fatal ("the environment has failed to come online" ,
243
+ fmt .Sprintf ("environment %q is not running" , env .Name ),
244
+ fmt .Sprintf ("its current status is %q" , env .LatestStat .ContainerStatus ),
245
+
246
+ clog .BlankLine ,
247
+ clog .Tipf ("take a look at the build logs to determine what went wrong" ),
248
+ clog .Tipf ("run \" coder envs rebuild %s --follow\" to attempt to rebuild the environment" , env .Name ),
249
+ )
250
+ }
251
+ return nil
252
+ }
253
+
104
254
// sendResizeEvents starts watching for the client's terminal resize signals
105
255
// and sends the event to the server so the remote tty can match the client.
106
256
func sendResizeEvents (ctx context.Context , termFD uintptr , process wsep.Process ) {
@@ -121,28 +271,7 @@ func sendResizeEvents(ctx context.Context, termFD uintptr, process wsep.Process)
121
271
}
122
272
}
123
273
124
- func runCommand (ctx context.Context , envName , command string , args []string ) error {
125
- client , err := newClient (ctx )
126
- if err != nil {
127
- return err
128
- }
129
- env , err := findEnv (ctx , client , envName , coder .Me )
130
- if err != nil {
131
- return xerrors .Errorf ("find environment: %w" , err )
132
- }
133
-
134
- // check if a rebuild is required before attempting to open a shell
135
- for _ , r := range env .RebuildMessages {
136
- // use the first rebuild message that is required
137
- if r .Required {
138
- return clog .Error (
139
- fmt .Sprintf (`environment "%s" requires a rebuild` , env .Name ),
140
- clog .Causef (r .Text ), clog .BlankLine ,
141
- clog .Tipf (`run "coder envs rebuild %s" to rebuild` , env .Name ),
142
- )
143
- }
144
- }
145
-
274
+ func runCommand (ctx context.Context , client * coder.Client , env * coder.Environment , command string , args []string ) error {
146
275
termFD := os .Stdout .Fd ()
147
276
148
277
isInteractive := terminal .IsTerminal (int (termFD ))
0 commit comments