diff --git a/coderd/x/chatd/chattool/createworkspace.go b/coderd/x/chatd/chattool/createworkspace.go index de0d617218..f65247fd02 100644 --- a/coderd/x/chatd/chattool/createworkspace.go +++ b/coderd/x/chatd/chattool/createworkspace.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "net/http" "strings" "sync" "time" @@ -238,7 +239,7 @@ func CreateWorkspace(db database.Store, organizationID, chatID uuid.UUID, option ) } - workspace, err := options.CreateFn(ctx, ownerID, createReq) + workspace, err := createWorkspaceWithNameRetry(ctx, ownerID, createReq, options.CreateFn) if err != nil { if responseErr, ok := httperror.IsResponder(err); ok { _, resp := responseErr.Response() @@ -703,6 +704,41 @@ func waitForAgentReady( } } +func createWorkspaceWithNameRetry( + ctx context.Context, + ownerID uuid.UUID, + req codersdk.CreateWorkspaceRequest, + createFn CreateWorkspaceFn, +) (codersdk.Workspace, error) { + workspace, err := createFn(ctx, ownerID, req) + if err == nil { + return workspace, nil + } + if !isWorkspaceNameConflict(err) { + return codersdk.Workspace{}, err + } + + req.Name = generatedWorkspaceName(req.Name) + return createFn(ctx, ownerID, req) +} + +func isWorkspaceNameConflict(err error) bool { + responseErr, ok := httperror.IsResponder(err) + if !ok { + return false + } + status, resp := responseErr.Response() + if status != http.StatusConflict { + return false + } + for _, validation := range resp.Validations { + if validation.Field == "name" { + return true + } + } + return false +} + func generatedWorkspaceName(seed string) string { base := codersdk.UsernameFrom(strings.TrimSpace(strings.ToLower(seed))) if strings.TrimSpace(base) == "" { diff --git a/coderd/x/chatd/chattool/createworkspace_internal_test.go b/coderd/x/chatd/chattool/createworkspace_internal_test.go index 06c4a95a84..13f009d668 100644 --- a/coderd/x/chatd/chattool/createworkspace_internal_test.go +++ b/coderd/x/chatd/chattool/createworkspace_internal_test.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/json" "fmt" + "net/http" "sync" "testing" "time" @@ -855,6 +856,70 @@ func TestCreateWorkspace_ResponderErrorPreservesStructuredFields(t *testing.T) { }}, result.Validations) } +func TestCreateWorkspaceWithNameRetry(t *testing.T) { + t.Parallel() + + t.Run("NameConflictRetriesWithGeneratedName", func(t *testing.T) { + t.Parallel() + + var names []string + workspace, err := createWorkspaceWithNameRetry( + context.Background(), + uuid.New(), + codersdk.CreateWorkspaceRequest{Name: "fun-dashboard"}, + func(_ context.Context, _ uuid.UUID, req codersdk.CreateWorkspaceRequest) (codersdk.Workspace, error) { + names = append(names, req.Name) + if len(names) == 1 { + return codersdk.Workspace{}, workspaceNameConflictError(req.Name) + } + + require.Regexp(t, `^fun-dashboard-[0-9a-f]{4}$`, req.Name) + return codersdk.Workspace{Name: req.Name}, nil + }, + ) + + require.NoError(t, err) + require.Len(t, names, 2) + require.Equal(t, "fun-dashboard", names[0]) + require.Equal(t, names[1], workspace.Name) + }) + + t.Run("OtherConflictDoesNotRetry", func(t *testing.T) { + t.Parallel() + + calls := 0 + wantErr := httperror.NewResponseError(http.StatusConflict, codersdk.Response{ + Message: "quota exceeded", + Validations: []codersdk.ValidationError{{ + Field: "quota", + Detail: "quota exceeded", + }}, + }) + _, err := createWorkspaceWithNameRetry( + context.Background(), + uuid.New(), + codersdk.CreateWorkspaceRequest{Name: "fun-dashboard"}, + func(context.Context, uuid.UUID, codersdk.CreateWorkspaceRequest) (codersdk.Workspace, error) { + calls++ + return codersdk.Workspace{}, wantErr + }, + ) + + require.Same(t, wantErr, err) + require.Equal(t, 1, calls) + }) +} + +func workspaceNameConflictError(name string) error { + return httperror.NewResponseError(http.StatusConflict, codersdk.Response{ + Message: fmt.Sprintf("Workspace %q already exists.", name), + Validations: []codersdk.ValidationError{{ + Field: "name", + Detail: "This value is already in use and should be unique.", + }}, + }) +} + func TestCreateWorkspace_GlobalTTL(t *testing.T) { t.Parallel()