Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitc8f8c95

Browse files
authored
feat: Add support for renaming workspaces (#3409)
* feat: Implement workspace renaming* feat: Add hidden rename command (and data loss warning)* feat: Implement database.IsUniqueViolation
1 parent623fc5b commitc8f8c95

File tree

13 files changed

+343
-13
lines changed

13 files changed

+343
-13
lines changed

‎cli/rename.go‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
"golang.org/x/xerrors"
8+
9+
"github.com/coder/coder/cli/cliui"
10+
"github.com/coder/coder/codersdk"
11+
)
12+
13+
funcrename()*cobra.Command {
14+
cmd:=&cobra.Command{
15+
Annotations:workspaceCommand,
16+
Use:"rename <workspace> <new name>",
17+
Short:"Rename a workspace",
18+
Args:cobra.ExactArgs(2),
19+
// Keep hidden until renaming is safe, see:
20+
// * https://github.com/coder/coder/issues/3000
21+
// * https://github.com/coder/coder/issues/3386
22+
Hidden:true,
23+
RunE:func(cmd*cobra.Command,args []string)error {
24+
client,err:=CreateClient(cmd)
25+
iferr!=nil {
26+
returnerr
27+
}
28+
workspace,err:=namedWorkspace(cmd,client,args[0])
29+
iferr!=nil {
30+
returnxerrors.Errorf("get workspace: %w",err)
31+
}
32+
33+
_,_=fmt.Fprintf(cmd.OutOrStdout(),"%s\n\n",
34+
cliui.Styles.Wrap.Render("WARNING: A rename can result in data loss if a resource references the workspace name in the template (e.g volumes)."),
35+
)
36+
_,err=cliui.Prompt(cmd, cliui.PromptOptions{
37+
Text:fmt.Sprintf("Type %q to confirm rename:",workspace.Name),
38+
Validate:func(sstring)error {
39+
ifs==workspace.Name {
40+
returnnil
41+
}
42+
returnxerrors.Errorf("Input %q does not match %q",s,workspace.Name)
43+
},
44+
})
45+
iferr!=nil {
46+
returnerr
47+
}
48+
49+
err=client.UpdateWorkspace(cmd.Context(),workspace.ID, codersdk.UpdateWorkspaceRequest{
50+
Name:args[1],
51+
})
52+
iferr!=nil {
53+
returnxerrors.Errorf("rename workspace: %w",err)
54+
}
55+
returnnil
56+
},
57+
}
58+
59+
cliui.AllowSkipPrompt(cmd)
60+
61+
returncmd
62+
}

‎cli/rename_test.go‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package cli_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/coder/coder/cli/clitest"
11+
"github.com/coder/coder/coderd/coderdtest"
12+
"github.com/coder/coder/pty/ptytest"
13+
"github.com/coder/coder/testutil"
14+
)
15+
16+
funcTestRename(t*testing.T) {
17+
t.Parallel()
18+
19+
client:=coderdtest.New(t,&coderdtest.Options{IncludeProvisionerD:true})
20+
user:=coderdtest.CreateFirstUser(t,client)
21+
version:=coderdtest.CreateTemplateVersion(t,client,user.OrganizationID,nil)
22+
coderdtest.AwaitTemplateVersionJob(t,client,version.ID)
23+
template:=coderdtest.CreateTemplate(t,client,user.OrganizationID,version.ID)
24+
workspace:=coderdtest.CreateWorkspace(t,client,user.OrganizationID,template.ID)
25+
coderdtest.AwaitWorkspaceBuildJob(t,client,workspace.LatestBuild.ID)
26+
27+
ctx,cancel:=context.WithTimeout(context.Background(),testutil.WaitLong)
28+
defercancel()
29+
30+
want:=workspace.Name+"-test"
31+
cmd,root:=clitest.New(t,"rename",workspace.Name,want,"--yes")
32+
clitest.SetupConfig(t,client,root)
33+
pty:=ptytest.New(t)
34+
cmd.SetIn(pty.Input())
35+
cmd.SetOut(pty.Output())
36+
37+
errC:=make(chanerror,1)
38+
gofunc() {
39+
errC<-cmd.ExecuteContext(ctx)
40+
}()
41+
42+
pty.ExpectMatch("confirm rename:")
43+
pty.WriteLine(workspace.Name)
44+
45+
require.NoError(t,<-errC)
46+
47+
ws,err:=client.Workspace(ctx,workspace.ID)
48+
assert.NoError(t,err)
49+
50+
got:=ws.Name
51+
assert.Equal(t,want,got,"workspace name did not change")
52+
}

‎cli/root.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ func Core() []*cobra.Command {
7979
start(),
8080
state(),
8181
stop(),
82+
rename(),
8283
templates(),
8384
update(),
8485
users(),

‎coderd/coderd.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ func New(options *Options) *API {
374374
httpmw.ExtractWorkspaceParam(options.Database),
375375
)
376376
r.Get("/",api.workspace)
377+
r.Patch("/",api.patchWorkspace)
377378
r.Route("/builds",func(r chi.Router) {
378379
r.Get("/",api.workspaceBuilds)
379380
r.Post("/",api.postWorkspaceBuilds)

‎coderd/database/databasefake/databasefake.go‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
"github.com/google/uuid"
12+
"github.com/lib/pq"
1213
"golang.org/x/exp/slices"
1314

1415
"github.com/coder/coder/coderd/database"
@@ -2086,6 +2087,32 @@ func (q *fakeQuerier) UpdateProvisionerJobWithCompleteByID(_ context.Context, ar
20862087
returnsql.ErrNoRows
20872088
}
20882089

2090+
func (q*fakeQuerier)UpdateWorkspace(_ context.Context,arg database.UpdateWorkspaceParams) (database.Workspace,error) {
2091+
q.mutex.Lock()
2092+
deferq.mutex.Unlock()
2093+
2094+
fori,workspace:=rangeq.workspaces {
2095+
ifworkspace.Deleted||workspace.ID!=arg.ID {
2096+
continue
2097+
}
2098+
for_,other:=rangeq.workspaces {
2099+
ifother.Deleted||other.ID==workspace.ID||workspace.OwnerID!=other.OwnerID {
2100+
continue
2101+
}
2102+
ifother.Name==arg.Name {
2103+
return database.Workspace{},&pq.Error{Code:"23505",Message:"duplicate key value violates unique constraint"}
2104+
}
2105+
}
2106+
2107+
workspace.Name=arg.Name
2108+
q.workspaces[i]=workspace
2109+
2110+
returnworkspace,nil
2111+
}
2112+
2113+
return database.Workspace{},sql.ErrNoRows
2114+
}
2115+
20892116
func (q*fakeQuerier)UpdateWorkspaceAutostart(_ context.Context,arg database.UpdateWorkspaceAutostartParams)error {
20902117
q.mutex.Lock()
20912118
deferq.mutex.Unlock()

‎coderd/database/errors.go‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package database
2+
3+
import (
4+
"errors"
5+
6+
"github.com/lib/pq"
7+
)
8+
9+
// UniqueConstraint represents a named unique constraint on a table.
10+
typeUniqueConstraintstring
11+
12+
// UniqueConstraint enums.
13+
// TODO(mafredri): Generate these from the database schema.
14+
const (
15+
UniqueWorkspacesOwnerIDLowerIdxUniqueConstraint="workspaces_owner_id_lower_idx"
16+
)
17+
18+
// IsUniqueViolation checks if the error is due to a unique violation.
19+
// If one or more specific unique constraints are given as arguments,
20+
// the error must be caused by one of them. If no constraints are given,
21+
// this function returns true for any unique violation.
22+
funcIsUniqueViolation(errerror,uniqueConstraints...UniqueConstraint)bool {
23+
varpqErr*pq.Error
24+
iferrors.As(err,&pqErr) {
25+
ifpqErr.Code.Name()=="unique_violation" {
26+
iflen(uniqueConstraints)==0 {
27+
returntrue
28+
}
29+
for_,uc:=rangeuniqueConstraints {
30+
ifpqErr.Constraint==string(uc) {
31+
returntrue
32+
}
33+
}
34+
}
35+
}
36+
37+
returnfalse
38+
}

‎coderd/database/querier.go‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/database/queries.sql.go‎

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/database/queries/workspaces.sql‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,16 @@ SET
112112
WHERE
113113
id= $1;
114114

115+
-- name: UpdateWorkspace :one
116+
UPDATE
117+
workspaces
118+
SET
119+
name= $2
120+
WHERE
121+
id= $1
122+
AND deleted= false
123+
RETURNING*;
124+
115125
-- name: UpdateWorkspaceAutostart :exec
116126
UPDATE
117127
workspaces

‎coderd/workspaces.go‎

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -316,17 +316,8 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
316316
})
317317
iferr==nil {
318318
// If the workspace already exists, don't allow creation.
319-
template,err:=api.Database.GetTemplateByID(r.Context(),workspace.TemplateID)
320-
iferr!=nil {
321-
httpapi.Write(rw,http.StatusInternalServerError, codersdk.Response{
322-
Message:fmt.Sprintf("Find template for conflicting workspace name %q.",createWorkspace.Name),
323-
Detail:err.Error(),
324-
})
325-
return
326-
}
327-
// The template is fetched for clarity to the user on where the conflicting name may be.
328319
httpapi.Write(rw,http.StatusConflict, codersdk.Response{
329-
Message:fmt.Sprintf("Workspace %q already exists in the %q template.",createWorkspace.Name,template.Name),
320+
Message:fmt.Sprintf("Workspace %q already exists.",createWorkspace.Name),
330321
Validations: []codersdk.ValidationError{{
331322
Field:"name",
332323
Detail:"This value is already in use and should be unique.",
@@ -479,6 +470,68 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
479470
findUser(apiKey.UserID,users),findUser(workspaceBuild.InitiatorID,users)))
480471
}
481472

473+
func (api*API)patchWorkspace(rw http.ResponseWriter,r*http.Request) {
474+
workspace:=httpmw.WorkspaceParam(r)
475+
if!api.Authorize(r,rbac.ActionUpdate,workspace) {
476+
httpapi.ResourceNotFound(rw)
477+
return
478+
}
479+
480+
varreq codersdk.UpdateWorkspaceRequest
481+
if!httpapi.Read(rw,r,&req) {
482+
return
483+
}
484+
485+
ifreq.Name==""||req.Name==workspace.Name {
486+
// Nothing changed, optionally this could be an error.
487+
rw.WriteHeader(http.StatusNoContent)
488+
return
489+
}
490+
// The reason we double check here is in case more fields can be
491+
// patched in the future, it's enough if one changes.
492+
name:=workspace.Name
493+
ifreq.Name!=""||req.Name!=workspace.Name {
494+
name=req.Name
495+
}
496+
497+
_,err:=api.Database.UpdateWorkspace(r.Context(), database.UpdateWorkspaceParams{
498+
ID:workspace.ID,
499+
Name:name,
500+
})
501+
iferr!=nil {
502+
// The query protects against updating deleted workspaces and
503+
// the existence of the workspace is checked in the request,
504+
// if we get ErrNoRows it means the workspace was deleted.
505+
//
506+
// We could do this check earlier but we'd need to start a
507+
// transaction.
508+
iferrors.Is(err,sql.ErrNoRows) {
509+
httpapi.Write(rw,http.StatusMethodNotAllowed, codersdk.Response{
510+
Message:fmt.Sprintf("Workspace %q is deleted and cannot be updated.",workspace.Name),
511+
})
512+
return
513+
}
514+
// Check if the name was already in use.
515+
ifdatabase.IsUniqueViolation(err,database.UniqueWorkspacesOwnerIDLowerIdx) {
516+
httpapi.Write(rw,http.StatusConflict, codersdk.Response{
517+
Message:fmt.Sprintf("Workspace %q already exists.",req.Name),
518+
Validations: []codersdk.ValidationError{{
519+
Field:"name",
520+
Detail:"This value is already in use and should be unique.",
521+
}},
522+
})
523+
return
524+
}
525+
httpapi.Write(rw,http.StatusInternalServerError, codersdk.Response{
526+
Message:"Internal error updating workspace.",
527+
Detail:err.Error(),
528+
})
529+
return
530+
}
531+
532+
rw.WriteHeader(http.StatusNoContent)
533+
}
534+
482535
func (api*API)putWorkspaceAutostart(rw http.ResponseWriter,r*http.Request) {
483536
workspace:=httpmw.WorkspaceParam(r)
484537
if!api.Authorize(r,rbac.ActionUpdate,workspace) {
@@ -556,7 +609,6 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
556609

557610
returnnil
558611
})
559-
560612
iferr!=nil {
561613
resp:= codersdk.Response{
562614
Message:"Error updating workspace time until shutdown.",
@@ -656,7 +708,6 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
656708

657709
returnnil
658710
})
659-
660711
iferr!=nil {
661712
api.Logger.Info(r.Context(),"extending workspace",slog.Error(err))
662713
}
@@ -861,6 +912,7 @@ func convertWorkspaces(ctx context.Context, db database.Store, workspaces []data
861912
}
862913
returnapiWorkspaces,nil
863914
}
915+
864916
funcconvertWorkspace(
865917
workspace database.Workspace,
866918
workspaceBuild database.WorkspaceBuild,

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp