From f56b73d9aaecfb875e63822baf50a505dd6d93fb Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 9 Jun 2022 13:30:40 -0500 Subject: [PATCH 1/4] fix: Sort workspace by name by created_at Fix bug where deleting workspaces with the same name returns the oldest deleted workspace --- coderd/database/queries.sql.go | 1 + coderd/database/queries/workspaces.sql | 3 ++- coderd/workspaces.go | 6 ++---- codersdk/workspaces.go | 14 +++++--------- site/src/api/api.ts | 2 +- site/src/api/typesGenerated.ts | 5 ----- 6 files changed, 11 insertions(+), 20 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 06e5f61105269..3eafdd09a9c9c 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3480,6 +3480,7 @@ WHERE owner_id = $1 AND deleted = $2 AND LOWER("name") = LOWER($3) +ORDER BY created_at DESC ` type GetWorkspaceByOwnerIDAndNameParams struct { diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 8c17c323b091d..1b9f6a88f6256 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -70,7 +70,8 @@ FROM WHERE owner_id = @owner_id AND deleted = @deleted - AND LOWER("name") = LOWER(@name); + AND LOWER("name") = LOWER(@name) +ORDER BY created_at DESC; -- name: GetWorkspaceOwnerCountsByTemplateIDs :many SELECT diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 255685a05901e..df84af742e66c 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -35,10 +35,8 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) { return } - // The `deleted` query parameter (which defaults to `false`) MUST match the - // `Deleted` field on the workspace otherwise you will get a 410 Gone. var ( - deletedStr = r.URL.Query().Get("deleted") + deletedStr = r.URL.Query().Get("include_deleted") showDeleted = false ) if deletedStr != "" { @@ -46,7 +44,7 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) { showDeleted, err = strconv.ParseBool(deletedStr) if err != nil { httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: fmt.Sprintf("Invalid boolean value %q for \"deleted\" query param.", deletedStr), + Message: fmt.Sprintf("Invalid boolean value %q for \"include_deleted\" query param.", deletedStr), Validations: []httpapi.Error{ {Field: "deleted", Detail: "Must be a valid boolean"}, }, diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 67db13a422459..1bc4f67d7a40f 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -39,7 +39,7 @@ type CreateWorkspaceBuildRequest struct { } type WorkspaceOptions struct { - Deleted bool `json:"deleted,omitempty"` + IncludeDeleted bool `json:"deleted,omitempty"` } // asRequestOption returns a function that can be used in (*Client).Request. @@ -47,8 +47,8 @@ type WorkspaceOptions struct { func (o WorkspaceOptions) asRequestOption() requestOption { return func(r *http.Request) { q := r.URL.Query() - if o.Deleted { - q.Set("deleted", "true") + if o.IncludeDeleted { + q.Set("include_deleted", "true") } r.URL.RawQuery = q.Encode() } @@ -62,7 +62,7 @@ func (c *Client) Workspace(ctx context.Context, id uuid.UUID) (Workspace, error) // DeletedWorkspace returns a single workspace that was deleted. func (c *Client) DeletedWorkspace(ctx context.Context, id uuid.UUID) (Workspace, error) { o := WorkspaceOptions{ - Deleted: true, + IncludeDeleted: true, } return c.getWorkspace(ctx, id, o.asRequestOption()) } @@ -258,12 +258,8 @@ func (c *Client) Workspaces(ctx context.Context, filter WorkspaceFilter) ([]Work return workspaces, json.NewDecoder(res.Body).Decode(&workspaces) } -type WorkspaceByOwnerAndNameParams struct { - IncludeDeleted bool `json:"include_deleted,omitempty"` -} - // WorkspaceByOwnerAndName returns a workspace by the owner's UUID and the workspace's name. -func (c *Client) WorkspaceByOwnerAndName(ctx context.Context, owner string, name string, params WorkspaceByOwnerAndNameParams) (Workspace, error) { +func (c *Client) WorkspaceByOwnerAndName(ctx context.Context, owner string, name string, params WorkspaceOptions) (Workspace, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspace/%s", owner, name), nil, func(r *http.Request) { q := r.URL.Query() q.Set("include_deleted", fmt.Sprintf("%t", params.IncludeDeleted)) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 39de3d7aff046..2e32b1ffbe707 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -144,7 +144,7 @@ export const getWorkspaces = async (filter?: TypesGen.WorkspaceFilter): Promise< export const getWorkspaceByOwnerAndName = async ( username = "me", workspaceName: string, - params?: TypesGen.WorkspaceByOwnerAndNameParams, + params?: TypesGen.WorkspaceOptions, ): Promise => { const response = await axios.get(`/api/v2/users/${username}/workspace/${workspaceName}`, { params, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 90c651c61cd9a..810135f8b7ff6 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -464,11 +464,6 @@ export interface WorkspaceBuildsRequest extends Pagination { readonly WorkspaceID: string } -// From codersdk/workspaces.go:261:6 -export interface WorkspaceByOwnerAndNameParams { - readonly include_deleted?: boolean -} - // From codersdk/workspaces.go:219:6 export interface WorkspaceFilter { readonly organization_id?: string From b47bd6f41d882113e1738a794f3a7efd037f8a2f Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 9 Jun 2022 13:33:37 -0500 Subject: [PATCH 2/4] Fix compile errors --- cli/create.go | 4 ++-- cli/root.go | 2 +- coderd/workspaces_test.go | 8 ++++---- codersdk/workspaces.go | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cli/create.go b/cli/create.go index 351e4bb7eee6b..1a0c89cc62a27 100644 --- a/cli/create.go +++ b/cli/create.go @@ -49,7 +49,7 @@ func create() *cobra.Command { workspaceName, err = cliui.Prompt(cmd, cliui.PromptOptions{ Text: "Specify a name for your workspace:", Validate: func(workspaceName string) error { - _, err = client.WorkspaceByOwnerAndName(cmd.Context(), codersdk.Me, workspaceName, codersdk.WorkspaceByOwnerAndNameParams{}) + _, err = client.WorkspaceByOwnerAndName(cmd.Context(), codersdk.Me, workspaceName, codersdk.WorkspaceOptions{}) if err == nil { return xerrors.Errorf("A workspace already exists named %q!", workspaceName) } @@ -61,7 +61,7 @@ func create() *cobra.Command { } } - _, err = client.WorkspaceByOwnerAndName(cmd.Context(), codersdk.Me, workspaceName, codersdk.WorkspaceByOwnerAndNameParams{}) + _, err = client.WorkspaceByOwnerAndName(cmd.Context(), codersdk.Me, workspaceName, codersdk.WorkspaceOptions{}) if err == nil { return xerrors.Errorf("A workspace already exists named %q!", workspaceName) } diff --git a/cli/root.go b/cli/root.go index 2e56ab280d880..cb59816fe10ec 100644 --- a/cli/root.go +++ b/cli/root.go @@ -214,7 +214,7 @@ func namedWorkspace(cmd *cobra.Command, client *codersdk.Client, identifier stri return codersdk.Workspace{}, xerrors.Errorf("invalid workspace name: %q", identifier) } - return client.WorkspaceByOwnerAndName(cmd.Context(), owner, name, codersdk.WorkspaceByOwnerAndNameParams{}) + return client.WorkspaceByOwnerAndName(cmd.Context(), owner, name, codersdk.WorkspaceOptions{}) } // createConfig consumes the global configuration flag to produce a config root. diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 73fed89c80c79..0c7706e941c23 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -258,7 +258,7 @@ func TestWorkspaceByOwnerAndName(t *testing.T) { t.Run("NotFound", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _, err := client.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, "something", codersdk.WorkspaceByOwnerAndNameParams{}) + _, err := client.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, "something", codersdk.WorkspaceOptions{}) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) @@ -271,7 +271,7 @@ func TestWorkspaceByOwnerAndName(t *testing.T) { coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) - _, err := client.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, workspace.Name, codersdk.WorkspaceByOwnerAndNameParams{}) + _, err := client.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, workspace.Name, codersdk.WorkspaceOptions{}) require.NoError(t, err) }) t.Run("Deleted", func(t *testing.T) { @@ -294,12 +294,12 @@ func TestWorkspaceByOwnerAndName(t *testing.T) { // Then: // When we call without includes_deleted, we don't expect to get the workspace back - _, err = client.WorkspaceByOwnerAndName(context.Background(), workspace.OwnerName, workspace.Name, codersdk.WorkspaceByOwnerAndNameParams{}) + _, err = client.WorkspaceByOwnerAndName(context.Background(), workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{}) require.ErrorContains(t, err, "403") // Then: // When we call with includes_deleted, we should get the workspace back - workspaceNew, err := client.WorkspaceByOwnerAndName(context.Background(), workspace.OwnerName, workspace.Name, codersdk.WorkspaceByOwnerAndNameParams{IncludeDeleted: true}) + workspaceNew, err := client.WorkspaceByOwnerAndName(context.Background(), workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{IncludeDeleted: true}) require.NoError(t, err) require.Equal(t, workspace.ID, workspaceNew.ID) }) diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 1bc4f67d7a40f..fbc1be91ab0e5 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -39,7 +39,7 @@ type CreateWorkspaceBuildRequest struct { } type WorkspaceOptions struct { - IncludeDeleted bool `json:"deleted,omitempty"` + IncludeDeleted bool `json:"include_deleted,omitempty"` } // asRequestOption returns a function that can be used in (*Client).Request. From 506f18c84b393f266dfc706554aab7e180a23b0c Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 9 Jun 2022 13:40:15 -0500 Subject: [PATCH 3/4] Make gen --- site/src/api/typesGenerated.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 810135f8b7ff6..ce0fae95ce4b0 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -473,7 +473,7 @@ export interface WorkspaceFilter { // From codersdk/workspaces.go:41:6 export interface WorkspaceOptions { - readonly deleted?: boolean + readonly include_deleted?: boolean } // From codersdk/workspaceresources.go:21:6 From 5b4e13310c0597bacd9d4cf84352ab96d9b71c2d Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 9 Jun 2022 13:47:57 -0500 Subject: [PATCH 4/4] Update fake to return most recent workspace --- coderd/database/databasefake/databasefake.go | 11 ++++++- coderd/workspaces_test.go | 31 ++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 78f85e71c597c..8bee0158d1a11 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -375,7 +375,9 @@ func (q *fakeQuerier) GetWorkspaceByOwnerIDAndName(_ context.Context, arg databa q.mutex.RLock() defer q.mutex.RUnlock() + var found *database.Workspace for _, workspace := range q.workspaces { + workspace := workspace if workspace.OwnerID != arg.OwnerID { continue } @@ -385,7 +387,14 @@ func (q *fakeQuerier) GetWorkspaceByOwnerIDAndName(_ context.Context, arg databa if workspace.Deleted != arg.Deleted { continue } - return workspace, nil + + // Return the most recent workspace with the given name + if found == nil || workspace.CreatedAt.After(found.CreatedAt) { + found = &workspace + } + } + if found != nil { + return *found, nil } return database.Workspace{}, sql.ErrNoRows } diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 0c7706e941c23..e6857be3745eb 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -302,6 +302,37 @@ func TestWorkspaceByOwnerAndName(t *testing.T) { workspaceNew, err := client.WorkspaceByOwnerAndName(context.Background(), workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{IncludeDeleted: true}) require.NoError(t, err) require.Equal(t, workspace.ID, workspaceNew.ID) + + // Given: + // We recreate the workspace with the same name + workspace, err = client.CreateWorkspace(context.Background(), user.OrganizationID, codersdk.CreateWorkspaceRequest{ + TemplateID: workspace.TemplateID, + Name: workspace.Name, + AutostartSchedule: workspace.AutostartSchedule, + TTLMillis: workspace.TTLMillis, + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + + // Then: + // We can fetch the most recent workspace + workspaceNew, err = client.WorkspaceByOwnerAndName(context.Background(), workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{}) + require.NoError(t, err) + require.Equal(t, workspace.ID, workspaceNew.ID) + + // Given: + // We delete the workspace again + build, err = client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionDelete, + }) + require.NoError(t, err, "delete the workspace") + coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID) + + // Then: + // When we fetch the deleted workspace, we get the most recently deleted one + workspaceNew, err = client.WorkspaceByOwnerAndName(context.Background(), workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{IncludeDeleted: true}) + require.NoError(t, err) + require.Equal(t, workspace.ID, workspaceNew.ID) }) } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy