Skip to content

Commit df574a8

Browse files
committed
fix(cli/ssh): retry on autostart conflict
Fixes #13032
1 parent fc7a7df commit df574a8

File tree

2 files changed

+71
-5
lines changed

2 files changed

+71
-5
lines changed

cli/ssh.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -657,12 +657,19 @@ func getWorkspaceAndAgent(ctx context.Context, inv *serpent.Invocation, client *
657657
// workspaces with the active version.
658658
_, _ = fmt.Fprintf(inv.Stderr, "Workspace was stopped, starting workspace to allow connecting to %q...\n", workspace.Name)
659659
_, err = startWorkspace(inv, client, workspace, workspaceParameterFlags{}, buildFlags{}, WorkspaceStart)
660-
if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusForbidden {
661-
_, err = startWorkspace(inv, client, workspace, workspaceParameterFlags{}, buildFlags{}, WorkspaceUpdate)
662-
if err != nil {
663-
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, xerrors.Errorf("start workspace with active template version: %w", err)
660+
if cerr, ok := codersdk.AsError(err); ok {
661+
switch cerr.StatusCode() {
662+
case http.StatusConflict:
663+
_, _ = fmt.Fprintln(inv.Stderr, "Unable to start the workspace due to conflict, the workspace may be starting, retrying without autostart...")
664+
return getWorkspaceAndAgent(ctx, inv, client, false, input)
665+
666+
case http.StatusForbidden:
667+
_, err = startWorkspace(inv, client, workspace, workspaceParameterFlags{}, buildFlags{}, WorkspaceUpdate)
668+
if err != nil {
669+
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, xerrors.Errorf("start workspace with active template version: %w", err)
670+
}
671+
_, _ = fmt.Fprintln(inv.Stdout, "Unable to start the workspace with template version from last build. Your workspace has been updated to the current active template version.")
664672
}
665-
_, _ = fmt.Fprintln(inv.Stdout, "Unable to start the workspace with template version from last build. Your workspace has been updated to the current active template version.")
666673
} else if err != nil {
667674
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, xerrors.Errorf("start workspace with current template version: %w", err)
668675
}

cli/ssh_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,65 @@ func TestSSH(t *testing.T) {
145145
pty.WriteLine("exit")
146146
<-cmdDone
147147
})
148+
t.Run("StartStoppedWorkspaceConflict", func(t *testing.T) {
149+
t.Parallel()
150+
151+
authToken := uuid.NewString()
152+
ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
153+
owner := coderdtest.CreateFirstUser(t, ownerClient)
154+
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
155+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
156+
Parse: echo.ParseComplete,
157+
ProvisionPlan: echo.PlanComplete,
158+
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
159+
})
160+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
161+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
162+
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
163+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
164+
// Stop the workspace
165+
workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop)
166+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceBuild.ID)
167+
168+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitSuperLong)
169+
defer cancel()
170+
171+
type proc struct {
172+
stdout bytes.Buffer
173+
pty *ptytest.PTY
174+
err chan error
175+
}
176+
procs := make([]proc, 3)
177+
for i := range procs {
178+
// SSH to the workspace which should autostart it
179+
inv, root := clitest.New(t, "ssh", workspace.Name)
180+
181+
proc := proc{
182+
pty: ptytest.New(t).Attach(inv),
183+
err: make(chan error, 1),
184+
}
185+
procs[i] = proc
186+
inv.Stdout = io.MultiWriter(proc.pty.Output(), &proc.stdout)
187+
clitest.SetupConfig(t, client, root)
188+
clitest.StartWithAssert(t, inv, func(*testing.T, error) {
189+
// Noop.
190+
})
191+
}
192+
193+
var foundConflict int
194+
for _, proc := range procs {
195+
// Either allow the command to start the workspace or fail
196+
// due to conflict (race), in which case it retries.
197+
match := proc.pty.ExpectRegexMatchContext(ctx, "(Waiting for the workspace agent to connect|Unable to start the workspace due to conflict, the workspace may be starting, retrying without autostart...)")
198+
if strings.Contains(match, "Unable to start the workspace due to conflict, the workspace may be starting, retrying without autostart...") {
199+
foundConflict++
200+
// It should retry without autostart.
201+
proc.pty.ExpectMatchContext(ctx, "Waiting for the workspace agent to connect")
202+
}
203+
}
204+
// TODO(mafredri): Remove this if it's racy.
205+
require.Greater(t, foundConflict, 0, "expected at least one conflict")
206+
})
148207
t.Run("RequireActiveVersion", func(t *testing.T) {
149208
t.Parallel()
150209

0 commit comments

Comments
 (0)