Skip to content

Commit 9827831

Browse files
committed
feat: Implement workspace renaming
1 parent 6e426cf commit 9827831

File tree

11 files changed

+185
-16
lines changed

11 files changed

+185
-16
lines changed

cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ func Root() *cobra.Command {
129129
start(),
130130
state(),
131131
stop(),
132+
rename(),
132133
templates(),
133134
update(),
134135
users(),

coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ func New(options *Options) *API {
359359
httpmw.ExtractWorkspaceParam(options.Database),
360360
)
361361
r.Get("/", api.workspace)
362+
r.Patch("/", api.patchWorkspace)
362363
r.Route("/builds", func(r chi.Router) {
363364
r.Get("/", api.workspaceBuilds)
364365
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"
@@ -2044,6 +2045,32 @@ func (q *fakeQuerier) UpdateProvisionerJobWithCompleteByID(_ context.Context, ar
20442045
return sql.ErrNoRows
20452046
}
20462047

2048+
func (q *fakeQuerier) UpdateWorkspace(_ context.Context, arg database.UpdateWorkspaceParams) error {
2049+
q.mutex.Lock()
2050+
defer q.mutex.Unlock()
2051+
2052+
for i, workspace := range q.workspaces {
2053+
if workspace.Deleted || workspace.ID != arg.ID {
2054+
continue
2055+
}
2056+
for _, other := range q.workspaces {
2057+
if other.Deleted || other.ID == workspace.ID || workspace.OwnerID != other.OwnerID {
2058+
continue
2059+
}
2060+
if other.Name == arg.Name {
2061+
return &pq.Error{Code: "23505", Message: "duplicate key value violates unique constraint"}
2062+
}
2063+
}
2064+
2065+
workspace.Name = arg.Name
2066+
q.workspaces[i] = workspace
2067+
2068+
return nil
2069+
}
2070+
2071+
return sql.ErrNoRows
2072+
}
2073+
20472074
func (q *fakeQuerier) UpdateWorkspaceAutostart(_ context.Context, arg database.UpdateWorkspaceAutostartParams) error {
20482075
q.mutex.Lock()
20492076
defer q.mutex.Unlock()

coderd/database/querier.go

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

coderd/database/queries.sql.go

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

coderd/database/queries/workspaces.sql

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

115+
-- name: UpdateWorkspace :exec
116+
UPDATE
117+
workspaces
118+
SET
119+
name = $2
120+
WHERE
121+
id = $1
122+
AND deleted = false;
123+
115124
-- name: UpdateWorkspaceAutostart :exec
116125
UPDATE
117126
workspaces

coderd/workspaces.go

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/go-chi/chi/v5"
1616
"github.com/google/uuid"
17+
"github.com/lib/pq"
1718
"github.com/moby/moby/pkg/namesgenerator"
1819
"golang.org/x/sync/errgroup"
1920
"golang.org/x/xerrors"
@@ -316,17 +317,8 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
316317
})
317318
if err == nil {
318319
// If the workspace already exists, don't allow creation.
319-
template, err := api.Database.GetTemplateByID(r.Context(), workspace.TemplateID)
320-
if err != 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.
328320
httpapi.Write(rw, http.StatusConflict, codersdk.Response{
329-
Message: fmt.Sprintf("Workspace %q already exists in the %q template.", createWorkspace.Name, template.Name),
321+
Message: fmt.Sprintf("Workspace %q already exists.", createWorkspace.Name),
330322
Validations: []codersdk.ValidationError{{
331323
Field: "name",
332324
Detail: "This value is already in use and should be unique.",
@@ -479,6 +471,72 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
479471
findUser(apiKey.UserID, users), findUser(workspaceBuild.InitiatorID, users)))
480472
}
481473

474+
func (api *API) patchWorkspace(rw http.ResponseWriter, r *http.Request) {
475+
workspace := httpmw.WorkspaceParam(r)
476+
if !api.Authorize(r, rbac.ActionUpdate, workspace) {
477+
httpapi.ResourceNotFound(rw)
478+
return
479+
}
480+
481+
var req codersdk.UpdateWorkspaceRequest
482+
if !httpapi.Read(rw, r, &req) {
483+
return
484+
}
485+
486+
if req.Name == "" || req.Name == workspace.Name {
487+
// Nothing changed, optionally this could be an error.
488+
rw.WriteHeader(http.StatusNoContent)
489+
return
490+
}
491+
// The reason we double check here is in case more fields can be
492+
// patched in the future, it's enough if one changes.
493+
name := workspace.Name
494+
if req.Name != "" || req.Name != workspace.Name {
495+
name = req.Name
496+
}
497+
498+
err := api.Database.UpdateWorkspace(r.Context(), database.UpdateWorkspaceParams{
499+
ID: workspace.ID,
500+
Name: name,
501+
})
502+
if err != nil {
503+
// The query protects against updating deleted workspaces and
504+
// the existence of the workspace is checked in the request,
505+
// the only conclusion we can make is that we're trying to
506+
// update a deleted workspace.
507+
//
508+
// We could do this check earlier but since we're not in a
509+
// transaction, it's pointless.
510+
if errors.Is(err, sql.ErrNoRows) {
511+
httpapi.Write(rw, http.StatusMethodNotAllowed, codersdk.Response{
512+
Message: fmt.Sprintf("Workspace %q is deleted and cannot be updated.", workspace.Name),
513+
})
514+
return
515+
}
516+
// Check if we triggered the one-unique-name-per-owner
517+
// constraint.
518+
var pqErr *pq.Error
519+
if errors.As(err, &pqErr) && pqErr.Code.Name() == "unique_violation" {
520+
httpapi.Write(rw, http.StatusConflict, codersdk.Response{
521+
Message: fmt.Sprintf("Workspace %q already exists.", req.Name),
522+
Validations: []codersdk.ValidationError{{
523+
Field: "name",
524+
Detail: "This value is already in use and should be unique.",
525+
}},
526+
})
527+
return
528+
}
529+
530+
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
531+
Message: "Internal error updating workspace.",
532+
Detail: err.Error(),
533+
})
534+
return
535+
}
536+
537+
rw.WriteHeader(http.StatusNoContent)
538+
}
539+
482540
func (api *API) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) {
483541
workspace := httpmw.WorkspaceParam(r)
484542
if !api.Authorize(r, rbac.ActionUpdate, workspace) {
@@ -556,7 +614,6 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
556614

557615
return nil
558616
})
559-
560617
if err != nil {
561618
resp := codersdk.Response{
562619
Message: "Error updating workspace time until shutdown.",
@@ -656,7 +713,6 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
656713

657714
return nil
658715
})
659-
660716
if err != nil {
661717
api.Logger.Info(r.Context(), "extending workspace", slog.Error(err))
662718
}
@@ -861,6 +917,7 @@ func convertWorkspaces(ctx context.Context, db database.Store, workspaces []data
861917
}
862918
return apiWorkspaces, nil
863919
}
920+
864921
func convertWorkspace(
865922
workspace database.Workspace,
866923
workspaceBuild database.WorkspaceBuild,

coderd/workspaces_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/coder/coder/cryptorand"
2020
"github.com/coder/coder/provisioner/echo"
2121
"github.com/coder/coder/provisionersdk/proto"
22+
"github.com/coder/coder/testutil"
2223
)
2324

2425
func TestWorkspace(t *testing.T) {
@@ -70,6 +71,37 @@ func TestWorkspace(t *testing.T) {
7071
require.Error(t, err)
7172
require.ErrorContains(t, err, "410") // gone
7273
})
74+
75+
t.Run("Rename", func(t *testing.T) {
76+
t.Parallel()
77+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
78+
user := coderdtest.CreateFirstUser(t, client)
79+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
80+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
81+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
82+
ws1 := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
83+
ws2 := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
84+
coderdtest.AwaitWorkspaceBuildJob(t, client, ws1.LatestBuild.ID)
85+
coderdtest.AwaitWorkspaceBuildJob(t, client, ws2.LatestBuild.ID)
86+
87+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
88+
defer cancel()
89+
90+
want := ws1.Name + "_2"
91+
err := client.UpdateWorkspace(ctx, ws1.ID, codersdk.UpdateWorkspaceRequest{
92+
Name: want,
93+
})
94+
require.NoError(t, err, "workspace rename failed")
95+
96+
ws, err := client.Workspace(ctx, ws1.ID)
97+
require.NoError(t, err)
98+
require.Equal(t, want, ws.Name, "workspace name not updated")
99+
100+
err = client.UpdateWorkspace(ctx, ws1.ID, codersdk.UpdateWorkspaceRequest{
101+
Name: ws2.Name,
102+
})
103+
require.Error(t, err, "workspace rename should have failed")
104+
})
73105
}
74106

75107
func TestAdminViewAllWorkspaces(t *testing.T) {

codersdk/workspaces.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,23 @@ func (c *Client) WatchWorkspace(ctx context.Context, id uuid.UUID) (<-chan Works
161161
return wc, nil
162162
}
163163

164+
type UpdateWorkspaceRequest struct {
165+
Name string `json:"name,omitempty" validate:"username"`
166+
}
167+
168+
func (c *Client) UpdateWorkspace(ctx context.Context, id uuid.UUID, req UpdateWorkspaceRequest) error {
169+
path := fmt.Sprintf("/api/v2/workspaces/%s", id.String())
170+
res, err := c.Request(ctx, http.MethodPatch, path, req)
171+
if err != nil {
172+
return xerrors.Errorf("update workspace: %w", err)
173+
}
174+
defer res.Body.Close()
175+
if res.StatusCode != http.StatusNoContent {
176+
return readBodyAsError(res)
177+
}
178+
return nil
179+
}
180+
164181
// UpdateWorkspaceAutostartRequest is a request to update a workspace's autostart schedule.
165182
type UpdateWorkspaceAutostartRequest struct {
166183
Schedule *string `json:"schedule"`
@@ -262,7 +279,6 @@ func (f WorkspaceFilter) asRequestOption() requestOption {
262279
// Workspaces returns all workspaces the authenticated user has access to.
263280
func (c *Client) Workspaces(ctx context.Context, filter WorkspaceFilter) ([]Workspace, error) {
264281
res, err := c.Request(ctx, http.MethodGet, "/api/v2/workspaces", nil, filter.asRequestOption())
265-
266282
if err != nil {
267283
return nil, err
268284
}

examples/templates/docker/main.tf

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ resource "coder_agent" "main" {
8080
# You can remove this block if you'd prefer to configure Git manually or using
8181
# dotfiles. (see docs/dotfiles.md)
8282
env = {
83-
GIT_AUTHOR_NAME = "${data.coder_workspace.me.owner}"
84-
GIT_COMMITTER_NAME = "${data.coder_workspace.me.owner}"
85-
GIT_AUTHOR_EMAIL = "${data.coder_workspace.me.owner_email}"
83+
GIT_AUTHOR_NAME = "${data.coder_workspace.me.owner}"
84+
GIT_COMMITTER_NAME = "${data.coder_workspace.me.owner}"
85+
GIT_AUTHOR_EMAIL = "${data.coder_workspace.me.owner_email}"
8686
GIT_COMMITTER_EMAIL = "${data.coder_workspace.me.owner_email}"
8787
}
8888
}

site/src/api/typesGenerated.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,11 @@ export interface UpdateWorkspaceAutostartRequest {
333333
readonly schedule?: string
334334
}
335335

336+
// From codersdk/workspaces.go
337+
export interface UpdateWorkspaceRequest {
338+
readonly name?: string
339+
}
340+
336341
// From codersdk/workspaces.go
337342
export interface UpdateWorkspaceTTLRequest {
338343
readonly ttl_ms?: number

0 commit comments

Comments
 (0)