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/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/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/coderd/workspaces_test.go b/coderd/workspaces_test.go index 73fed89c80c79..e6857be3745eb 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,43 @@ 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) + + // 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) }) diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 67db13a422459..fbc1be91ab0e5 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:"include_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..ce0fae95ce4b0 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 @@ -478,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 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