mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: Add connection_timeout and troubleshooting_url to agent (#4937)
* feat: Add connection_timeout and troubleshooting_url to agent This commit adds the connection timeout and troubleshooting url fields to coder agents. If an initial connection cannot be established within connection timeout seconds, then the agent status will be marked as `"timeout"`. The troubleshooting URL will be present, if configured in the Terraform template, it can be presented to the user when the agent state is either `"timeout"` or `"disconnected"`. Fixes #4678
This commit is contained in:
+48
-22
@@ -35,12 +35,11 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
|
||||
if err != nil {
|
||||
return xerrors.Errorf("fetch: %w", err)
|
||||
}
|
||||
|
||||
if agent.Status == codersdk.WorkspaceAgentConnected {
|
||||
return nil
|
||||
}
|
||||
if agent.Status == codersdk.WorkspaceAgentDisconnected {
|
||||
opts.WarnInterval = 0
|
||||
}
|
||||
|
||||
spin := spinner.New(spinner.CharSets[78], 100*time.Millisecond, spinner.WithColor("fgHiGreen"))
|
||||
spin.Writer = writer
|
||||
spin.ForceOutput = true
|
||||
@@ -65,43 +64,70 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
|
||||
os.Exit(1)
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(opts.FetchInterval)
|
||||
defer ticker.Stop()
|
||||
timer := time.NewTimer(opts.WarnInterval)
|
||||
defer timer.Stop()
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
}
|
||||
warningShown := false
|
||||
warnAfter := time.NewTimer(opts.WarnInterval)
|
||||
defer warnAfter.Stop()
|
||||
showWarning := func() {
|
||||
warnAfter.Stop()
|
||||
|
||||
resourceMutex.Lock()
|
||||
defer resourceMutex.Unlock()
|
||||
message := "Don't panic, your workspace is booting up!"
|
||||
if agent.Status == codersdk.WorkspaceAgentDisconnected {
|
||||
message = "The workspace agent lost connection! Wait for it to reconnect or restart your workspace."
|
||||
if warningShown {
|
||||
return
|
||||
}
|
||||
warningShown = true
|
||||
|
||||
message := waitingMessage(agent)
|
||||
// This saves the cursor position, then defers clearing from the cursor
|
||||
// position to the end of the screen.
|
||||
_, _ = fmt.Fprintf(writer, "\033[s\r\033[2K%s\n\n", Styles.Paragraph.Render(Styles.Prompt.String()+message))
|
||||
defer fmt.Fprintf(writer, "\033[u\033[J")
|
||||
}
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-warnAfter.C:
|
||||
showWarning()
|
||||
}
|
||||
}()
|
||||
|
||||
fetchInterval := time.NewTicker(opts.FetchInterval)
|
||||
defer fetchInterval.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
case <-fetchInterval.C:
|
||||
}
|
||||
resourceMutex.Lock()
|
||||
agent, err = opts.Fetch(ctx)
|
||||
if err != nil {
|
||||
resourceMutex.Unlock()
|
||||
return xerrors.Errorf("fetch: %w", err)
|
||||
}
|
||||
if agent.Status != codersdk.WorkspaceAgentConnected {
|
||||
resourceMutex.Unlock()
|
||||
continue
|
||||
}
|
||||
resourceMutex.Unlock()
|
||||
return nil
|
||||
switch agent.Status {
|
||||
case codersdk.WorkspaceAgentConnected:
|
||||
return nil
|
||||
case codersdk.WorkspaceAgentTimeout, codersdk.WorkspaceAgentDisconnected:
|
||||
showWarning()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func waitingMessage(agent codersdk.WorkspaceAgent) string {
|
||||
var m string
|
||||
switch agent.Status {
|
||||
case codersdk.WorkspaceAgentTimeout:
|
||||
m = "The workspace agent is having trouble connecting."
|
||||
case codersdk.WorkspaceAgentDisconnected:
|
||||
m = "The workspace agent lost connection!"
|
||||
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 fmt.Sprintf("%s Wait for it to (re)connect or restart your workspace.", m)
|
||||
}
|
||||
|
||||
@@ -127,6 +127,13 @@ func renderAgentStatus(agent codersdk.WorkspaceAgent) string {
|
||||
since := database.Now().Sub(*agent.DisconnectedAt)
|
||||
return Styles.Error.Render("⦾ disconnected") + " " +
|
||||
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
|
||||
case codersdk.WorkspaceAgentTimeout:
|
||||
since := database.Now().Sub(agent.CreatedAt)
|
||||
return fmt.Sprintf(
|
||||
"%s %s",
|
||||
Styles.Warn.Render("⦾ timeout"),
|
||||
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]"),
|
||||
)
|
||||
case codersdk.WorkspaceAgentConnected:
|
||||
return Styles.Keyword.Render("⦿ connected")
|
||||
default:
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestSpeedtest(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("This test takes a minimum of 5ms per a hardcoded value in Tailscale!")
|
||||
}
|
||||
client, workspace, agentToken := setupWorkspaceForAgent(t)
|
||||
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
||||
agentClient := codersdk.New(client.URL)
|
||||
agentClient.SetSessionToken(agentToken)
|
||||
agentCloser := agent.New(agent.Options{
|
||||
|
||||
+40
-6
@@ -31,8 +31,13 @@ import (
|
||||
"github.com/coder/coder/testutil"
|
||||
)
|
||||
|
||||
func setupWorkspaceForAgent(t *testing.T) (*codersdk.Client, codersdk.Workspace, string) {
|
||||
func setupWorkspaceForAgent(t *testing.T, mutate func([]*proto.Agent) []*proto.Agent) (*codersdk.Client, codersdk.Workspace, string) {
|
||||
t.Helper()
|
||||
if mutate == nil {
|
||||
mutate = func(a []*proto.Agent) []*proto.Agent {
|
||||
return a
|
||||
}
|
||||
}
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
agentToken := uuid.NewString()
|
||||
@@ -45,12 +50,12 @@ func setupWorkspaceForAgent(t *testing.T) (*codersdk.Client, codersdk.Workspace,
|
||||
Resources: []*proto.Resource{{
|
||||
Name: "dev",
|
||||
Type: "google_compute_instance",
|
||||
Agents: []*proto.Agent{{
|
||||
Agents: mutate([]*proto.Agent{{
|
||||
Id: uuid.NewString(),
|
||||
Auth: &proto.Agent_Token{
|
||||
Token: agentToken,
|
||||
},
|
||||
}},
|
||||
}}),
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -69,7 +74,7 @@ func TestSSH(t *testing.T) {
|
||||
t.Run("ImmediateExit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, workspace, agentToken := setupWorkspaceForAgent(t)
|
||||
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
||||
cmd, root := clitest.New(t, "ssh", workspace.Name)
|
||||
clitest.SetupConfig(t, client, root)
|
||||
pty := ptytest.New(t)
|
||||
@@ -100,9 +105,38 @@ func TestSSH(t *testing.T) {
|
||||
pty.WriteLine("exit")
|
||||
<-cmdDone
|
||||
})
|
||||
t.Run("ShowTroubleshootingURLAfterTimeout", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wantURL := "https://example.com/troubleshoot"
|
||||
client, workspace, _ := setupWorkspaceForAgent(t, func(a []*proto.Agent) []*proto.Agent {
|
||||
// Unfortunately, one second is the lowest
|
||||
// we can go because 0 disables the feature.
|
||||
a[0].ConnectionTimeoutSeconds = 1
|
||||
a[0].TroubleshootingUrl = wantURL
|
||||
return a
|
||||
})
|
||||
cmd, root := clitest.New(t, "ssh", workspace.Name)
|
||||
clitest.SetupConfig(t, client, root)
|
||||
pty := ptytest.New(t)
|
||||
cmd.SetIn(pty.Input())
|
||||
cmd.SetErr(pty.Output())
|
||||
cmd.SetOut(pty.Output())
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
cmdDone := tGo(t, func() {
|
||||
err := cmd.ExecuteContext(ctx)
|
||||
assert.ErrorIs(t, err, context.Canceled)
|
||||
})
|
||||
pty.ExpectMatch(wantURL)
|
||||
cancel()
|
||||
<-cmdDone
|
||||
})
|
||||
t.Run("Stdio", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client, workspace, agentToken := setupWorkspaceForAgent(t)
|
||||
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
||||
_, _ = tGoContext(t, func(ctx context.Context) {
|
||||
// Run this async so the SSH command has to wait for
|
||||
// the build and agent to connect!
|
||||
@@ -171,7 +205,7 @@ func TestSSH(t *testing.T) {
|
||||
|
||||
t.Parallel()
|
||||
|
||||
client, workspace, agentToken := setupWorkspaceForAgent(t)
|
||||
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
||||
|
||||
agentClient := codersdk.New(client.URL)
|
||||
agentClient.SetSessionToken(agentToken)
|
||||
|
||||
Reference in New Issue
Block a user