Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit 288197a

Browse files
authored
feat: Prompt the user to rebuild workspace on coder sh (#223)
If the workspace is OFF or a rebuild is required, prompt the user to rebuild right away. This prompt only occurs in an interactive shell
1 parent daa3f7a commit 288197a

File tree

3 files changed

+167
-27
lines changed

3 files changed

+167
-27
lines changed

coder-sdk/env.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,12 @@ func (c Client) WaitForEnvironmentReady(ctx context.Context, envID string) error
274274
}
275275
}
276276
}
277+
278+
// EnvironmentByID get the details of an environment by its id.
279+
func (c Client) EnvironmentByID(ctx context.Context, id string) (*Environment, error) {
280+
var env Environment
281+
if err := c.requestBody(ctx, http.MethodGet, "/api/v0/environments/"+id, nil, &env); err != nil {
282+
return nil, err
283+
}
284+
return &env, nil
285+
}

docs/coder_sh.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ Open a shell and execute commands in a Coder environment
44

55
### Synopsis
66

7-
Execute a remote command on the environment\nIf no command is specified, the default shell is opened.
7+
Execute a remote command on the environment
8+
If no command is specified, the default shell is opened.
9+
If the command is run in an interactive shell, a user prompt will occur if the environment needs to be rebuilt.
810

911
```
1012
coder sh [environment_name] [<command [args...]>] [flags]

internal/cmd/shell.go

Lines changed: 155 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99
"time"
1010

11+
"github.com/manifoldco/promptui"
1112
"github.com/spf13/cobra"
1213
"golang.org/x/crypto/ssh/terminal"
1314
"golang.org/x/time/rate"
@@ -65,9 +66,11 @@ func shValidArgs(cmd *cobra.Command, args []string) error {
6566

6667
func shCmd() *cobra.Command {
6768
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.`,
7174
Args: shValidArgs,
7275
DisableFlagParsing: true,
7376
ValidArgsFunction: getEnvsForCompletion(coder.Me),
@@ -92,7 +95,28 @@ func shell(cmd *cobra.Command, cmdArgs []string) error {
9295

9396
envName := cmdArgs[0]
9497

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 {
96120
if exitErr, ok := err.(wsep.ExitError); ok {
97121
os.Exit(exitErr.Code)
98122
}
@@ -101,6 +125,132 @@ func shell(cmd *cobra.Command, cmdArgs []string) error {
101125
return nil
102126
}
103127

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+
104254
// sendResizeEvents starts watching for the client's terminal resize signals
105255
// and sends the event to the server so the remote tty can match the client.
106256
func sendResizeEvents(ctx context.Context, termFD uintptr, process wsep.Process) {
@@ -121,28 +271,7 @@ func sendResizeEvents(ctx context.Context, termFD uintptr, process wsep.Process)
121271
}
122272
}
123273

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 {
146275
termFD := os.Stdout.Fd()
147276

148277
isInteractive := terminal.IsTerminal(int(termFD))

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy