mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
fix(cli/ssh): retry on autostart conflict (#16058)
This commit is contained in:
committed by
GitHub
parent
53d9c7ebe4
commit
ba6e84dec3
@ -17,6 +17,7 @@ import (
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -145,6 +146,87 @@ func TestSSH(t *testing.T) {
|
||||
pty.WriteLine("exit")
|
||||
<-cmdDone
|
||||
})
|
||||
t.Run("StartStoppedWorkspaceConflict", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Intercept builds to synchronize execution of the SSH command.
|
||||
// The purpose here is to make sure all commands try to trigger
|
||||
// a start build of the workspace.
|
||||
isFirstBuild := true
|
||||
buildURL := regexp.MustCompile("/api/v2/workspaces/.*/builds")
|
||||
buildReq := make(chan struct{})
|
||||
buildResume := make(chan struct{})
|
||||
buildSyncMW := func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost && buildURL.MatchString(r.URL.Path) {
|
||||
if !isFirstBuild {
|
||||
t.Log("buildSyncMW: blocking build")
|
||||
buildReq <- struct{}{}
|
||||
<-buildResume
|
||||
t.Log("buildSyncMW: resuming build")
|
||||
} else {
|
||||
isFirstBuild = false
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
authToken := uuid.NewString()
|
||||
ownerClient := coderdtest.New(t, &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
APIMiddleware: buildSyncMW,
|
||||
})
|
||||
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.PlanComplete,
|
||||
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
// Stop the workspace
|
||||
workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceBuild.ID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
|
||||
defer cancel()
|
||||
|
||||
var ptys []*ptytest.PTY
|
||||
for i := 0; i < 3; i++ {
|
||||
// SSH to the workspace which should autostart it
|
||||
inv, root := clitest.New(t, "ssh", workspace.Name)
|
||||
|
||||
pty := ptytest.New(t).Attach(inv)
|
||||
ptys = append(ptys, pty)
|
||||
clitest.SetupConfig(t, client, root)
|
||||
testutil.Go(t, func() {
|
||||
_ = inv.WithContext(ctx).Run()
|
||||
})
|
||||
}
|
||||
|
||||
for _, pty := range ptys {
|
||||
pty.ExpectMatchContext(ctx, "Workspace was stopped, starting workspace to allow connecting to")
|
||||
}
|
||||
for range ptys {
|
||||
testutil.RequireRecvCtx(ctx, t, buildReq)
|
||||
}
|
||||
close(buildResume)
|
||||
|
||||
var foundConflict int
|
||||
for _, pty := range ptys {
|
||||
// Either allow the command to start the workspace or fail
|
||||
// due to conflict (race), in which case it retries.
|
||||
match := pty.ExpectRegexMatchContext(ctx, "Waiting for the workspace agent to connect")
|
||||
if strings.Contains(match, "Unable to start the workspace due to conflict, the workspace may be starting, retrying without autostart...") {
|
||||
foundConflict++
|
||||
}
|
||||
}
|
||||
require.Equal(t, 2, foundConflict, "expected 2 conflicts")
|
||||
})
|
||||
t.Run("RequireActiveVersion", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
Reference in New Issue
Block a user