-
Notifications
You must be signed in to change notification settings - Fork 929
feat(cli): Add support for delay_login_until_ready
#5851
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
Changes from all commits
7004c5d
fd4a596
b7a69a6
4f46181
489ed6a
8ded3c8
cdbf70c
00ab3ba
e9552c2
68d4c4a
b441de2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,16 +10,21 @@ import ( | |
"time" | ||
|
||
"github.com/briandowns/spinner" | ||
"github.com/muesli/reflow/indent" | ||
"github.com/muesli/reflow/wordwrap" | ||
"golang.org/x/xerrors" | ||
|
||
"github.com/coder/coder/codersdk" | ||
) | ||
|
||
var AgentStartError = xerrors.New("agent startup exited with non-zero exit status") | ||
|
||
type AgentOptions struct { | ||
WorkspaceName string | ||
Fetch func(context.Context) (codersdk.WorkspaceAgent, error) | ||
FetchInterval time.Duration | ||
WarnInterval time.Duration | ||
NoWait bool // If true, don't wait for the agent to be ready. | ||
} | ||
|
||
// Agent displays a spinning indicator that waits for a workspace agent to connect. | ||
|
@@ -36,48 +41,33 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error { | |
return xerrors.Errorf("fetch: %w", err) | ||
} | ||
|
||
if agent.Status == codersdk.WorkspaceAgentConnected { | ||
// Fast path if the agent is ready (avoid showing connecting prompt). | ||
// We don't take the fast path for opts.NoWait yet because we want to | ||
// show the message. | ||
if agent.Status == codersdk.WorkspaceAgentConnected && | ||
(!agent.DelayLoginUntilReady || agent.LifecycleState == codersdk.WorkspaceAgentLifecycleReady) { | ||
return nil | ||
} | ||
|
||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) | ||
defer cancel() | ||
|
||
spin := spinner.New(spinner.CharSets[78], 100*time.Millisecond, spinner.WithColor("fgHiGreen")) | ||
spin.Writer = writer | ||
spin.ForceOutput = true | ||
spin.Suffix = " Waiting for connection from " + Styles.Field.Render(agent.Name) + "..." | ||
spin.Start() | ||
defer spin.Stop() | ||
spin.Suffix = waitingMessage(agent, opts).Spin | ||
|
||
ctx, cancelFunc := context.WithCancel(ctx) | ||
defer cancelFunc() | ||
stopSpin := make(chan os.Signal, 1) | ||
signal.Notify(stopSpin, os.Interrupt) | ||
defer signal.Stop(stopSpin) | ||
go func() { | ||
select { | ||
case <-ctx.Done(): | ||
return | ||
case <-stopSpin: | ||
} | ||
cancelFunc() | ||
signal.Stop(stopSpin) | ||
spin.Stop() | ||
// nolint:revive | ||
os.Exit(1) | ||
}() | ||
|
||
var waitMessage string | ||
messageAfter := time.NewTimer(opts.WarnInterval) | ||
defer messageAfter.Stop() | ||
waitMessage := &message{} | ||
showMessage := func() { | ||
resourceMutex.Lock() | ||
defer resourceMutex.Unlock() | ||
|
||
m := waitingMessage(agent) | ||
if m == waitMessage { | ||
m := waitingMessage(agent, opts) | ||
if m.Prompt == waitMessage.Prompt { | ||
return | ||
} | ||
moveUp := "" | ||
if waitMessage != "" { | ||
if waitMessage.Prompt != "" { | ||
// If this is an update, move a line up | ||
// to keep it tidy and aligned. | ||
moveUp = "\033[1A" | ||
|
@@ -86,20 +76,43 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error { | |
|
||
// Stop the spinner while we write our message. | ||
spin.Stop() | ||
spin.Suffix = waitMessage.Spin | ||
// Clear the line and (if necessary) move up a line to write our message. | ||
_, _ = fmt.Fprintf(writer, "\033[2K%s%s\n\n", moveUp, Styles.Paragraph.Render(Styles.Prompt.String()+waitMessage)) | ||
_, _ = fmt.Fprintf(writer, "\033[2K%s\n%s\n", moveUp, waitMessage.Prompt) | ||
select { | ||
case <-ctx.Done(): | ||
default: | ||
// Safe to resume operation. | ||
spin.Start() | ||
if spin.Suffix != "" { | ||
spin.Start() | ||
} | ||
} | ||
} | ||
|
||
// Fast path for showing the error message even when using no wait, | ||
// we do this just before starting the spinner to avoid needless | ||
// spinning. | ||
if agent.Status == codersdk.WorkspaceAgentConnected && | ||
agent.DelayLoginUntilReady && opts.NoWait { | ||
showMessage() | ||
return nil | ||
} | ||
|
||
// Start spinning after fast paths are handled. | ||
if spin.Suffix != "" { | ||
spin.Start() | ||
} | ||
defer spin.Stop() | ||
|
||
warnAfter := time.NewTimer(opts.WarnInterval) | ||
defer warnAfter.Stop() | ||
warningShown := make(chan struct{}) | ||
go func() { | ||
select { | ||
case <-ctx.Done(): | ||
case <-messageAfter.C: | ||
messageAfter.Stop() | ||
close(warningShown) | ||
case <-warnAfter.C: | ||
close(warningShown) | ||
showMessage() | ||
} | ||
}() | ||
|
@@ -121,26 +134,108 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error { | |
resourceMutex.Unlock() | ||
switch agent.Status { | ||
case codersdk.WorkspaceAgentConnected: | ||
// NOTE(mafredri): Once we have access to the workspace agent's | ||
// startup script logs, we can show them here. | ||
// https://github.com/coder/coder/issues/2957 | ||
if agent.DelayLoginUntilReady && !opts.NoWait { | ||
switch agent.LifecycleState { | ||
case codersdk.WorkspaceAgentLifecycleReady: | ||
return nil | ||
case codersdk.WorkspaceAgentLifecycleStartTimeout: | ||
showMessage() | ||
case codersdk.WorkspaceAgentLifecycleStartError: | ||
showMessage() | ||
return AgentStartError | ||
default: | ||
select { | ||
case <-warningShown: | ||
showMessage() | ||
default: | ||
// This state is normal, we don't want | ||
// to show a message prematurely. | ||
} | ||
} | ||
continue | ||
} | ||
return nil | ||
case codersdk.WorkspaceAgentTimeout, codersdk.WorkspaceAgentDisconnected: | ||
showMessage() | ||
} | ||
} | ||
} | ||
|
||
func waitingMessage(agent codersdk.WorkspaceAgent) string { | ||
var m string | ||
type message struct { | ||
Spin string | ||
Prompt string | ||
Troubleshoot bool | ||
} | ||
|
||
func waitingMessage(agent codersdk.WorkspaceAgent, opts AgentOptions) (m *message) { | ||
m = &message{ | ||
Spin: fmt.Sprintf("Waiting for connection from %s...", Styles.Field.Render(agent.Name)), | ||
Prompt: "Don't panic, your workspace is booting up!", | ||
} | ||
defer func() { | ||
if opts.NoWait { | ||
m.Spin = "" | ||
} | ||
if m.Spin != "" { | ||
m.Spin = " " + m.Spin | ||
} | ||
|
||
// We don't want to wrap the troubleshooting URL, so we'll handle word | ||
// wrapping ourselves (vs using lipgloss). | ||
w := wordwrap.NewWriter(Styles.Paragraph.GetWidth() - Styles.Paragraph.GetMarginLeft()*2) | ||
w.Breakpoints = []rune{' ', '\n'} | ||
|
||
_, _ = fmt.Fprint(w, m.Prompt) | ||
if m.Troubleshoot { | ||
if agent.TroubleshootingURL != "" { | ||
_, _ = fmt.Fprintf(w, " See troubleshooting instructions at:\n%s", agent.TroubleshootingURL) | ||
} else { | ||
_, _ = fmt.Fprint(w, " Wait for it to (re)connect or restart your workspace.") | ||
} | ||
} | ||
_, _ = fmt.Fprint(w, "\n") | ||
|
||
// We want to prefix the prompt with a caret, but we want text on the | ||
// following lines to align with the text on the first line (i.e. added | ||
// spacing). | ||
ind := " " + Styles.Prompt.String() | ||
iw := indent.NewWriter(1, func(w io.Writer) { | ||
_, _ = w.Write([]byte(ind)) | ||
ind = " " // Set indentation to space after initial prompt. | ||
}) | ||
_, _ = fmt.Fprint(iw, w.String()) | ||
m.Prompt = iw.String() | ||
}() | ||
|
||
switch agent.Status { | ||
case codersdk.WorkspaceAgentTimeout: | ||
m = "The workspace agent is having trouble connecting." | ||
m.Prompt = "The workspace agent is having trouble connecting." | ||
case codersdk.WorkspaceAgentDisconnected: | ||
m = "The workspace agent lost connection!" | ||
m.Prompt = "The workspace agent lost connection!" | ||
case codersdk.WorkspaceAgentConnected: | ||
m.Spin = fmt.Sprintf("Waiting for %s to become ready...", Styles.Field.Render(agent.Name)) | ||
m.Prompt = "Don't panic, your workspace agent has connected and the workspace is getting ready!" | ||
if opts.NoWait { | ||
m.Prompt = "Your workspace is still getting ready, it may be in an incomplete state." | ||
} | ||
|
||
switch agent.LifecycleState { | ||
case codersdk.WorkspaceAgentLifecycleStartTimeout: | ||
m.Prompt = "The workspace is taking longer than expected to get ready, the agent startup script is still executing." | ||
case codersdk.WorkspaceAgentLifecycleStartError: | ||
m.Spin = "" | ||
m.Prompt = "The workspace ran into a problem while getting ready, the agent startup script exited with non-zero status." | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this copy! Once we start streaming build logs, I imagine we can also link to view the logs + the troubleshooting URL? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes definitely! We can also stream the log in the terminal while the user is waiting, like 5 latest rows (keeps updating). Maybe that'd enabled/disabled via flag. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's even better! |
||
default: | ||
// Not a failure state, no troubleshooting necessary. | ||
return m | ||
} | ||
default: | ||
// Not a failure state, no troubleshooting necessary. | ||
return "Don't panic, your workspace is booting up!" | ||
} | ||
if agent.TroubleshootingURL != "" { | ||
return fmt.Sprintf("%s See troubleshooting instructions at: %s", m, agent.TroubleshootingURL) | ||
return m | ||
} | ||
return fmt.Sprintf("%s Wait for it to (re)connect or restart your workspace.", m) | ||
m.Troubleshoot = true | ||
return m | ||
} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add directions on how to bypass this e.g.
(Or whatever we end up calling it)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not for this message (this state is before agent is connected so we can't connect yet).
We could add it for the other message, but I wonder if it would encourage bypassing the prompt, esp. without understanding what the flag does. You just go
Ctrl+C -> --no-wait
yay I'm in (and not everything is working). Would it suffice to explain this in the troubleshooting link?I think this need will also be alleviated by streaming the startup log once we have it available.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fine either