diff --git a/.vscode/settings.json b/.vscode/settings.json index 396b551029288..a7c4e8086ffdc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -83,6 +83,7 @@ "workspaceapp", "workspaceapps", "workspacebuilds", + "workspacename", "wsconncache", "xerrors", "xstate", diff --git a/cli/create.go b/cli/create.go index 4a178bf1f5a2c..0b4f042a959fe 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) + _, err = client.WorkspaceByOwnerAndName(cmd.Context(), codersdk.Me, workspaceName, codersdk.WorkspaceByOwnerAndNameParams{}) 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) + _, err = client.WorkspaceByOwnerAndName(cmd.Context(), codersdk.Me, workspaceName, codersdk.WorkspaceByOwnerAndNameParams{}) if err == nil { return xerrors.Errorf("A workspace already exists named %q!", workspaceName) } diff --git a/cli/root.go b/cli/root.go index abf69c84f54cd..2e56ab280d880 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) + return client.WorkspaceByOwnerAndName(cmd.Context(), owner, name, codersdk.WorkspaceByOwnerAndNameParams{}) } // createConfig consumes the global configuration flag to produce a config root. diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 25fd67d581cba..079f880ed44f3 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -165,10 +165,32 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) owner := httpmw.UserParam(r) workspaceName := chi.URLParam(r, "workspacename") + includeDeleted := false + if s := r.URL.Query().Get("include_deleted"); s != "" { + var err error + includeDeleted, err = strconv.ParseBool(s) + if err != nil { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("Invalid boolean value %q for \"include_deleted\" query param.", s), + Validations: []httpapi.Error{ + {Field: "include_deleted", Detail: "Must be a valid boolean"}, + }, + }) + return + } + } + workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{ OwnerID: owner.ID, Name: workspaceName, }) + if includeDeleted && errors.Is(err, sql.ErrNoRows) { + workspace, err = api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{ + OwnerID: owner.ID, + Name: workspaceName, + Deleted: includeDeleted, + }) + } if errors.Is(err, sql.ErrNoRows) { // Do not leak information if the workspace exists or not httpapi.Forbidden(rw) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 665c2d0a49d88..73fed89c80c79 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") + _, err := client.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, "something", codersdk.WorkspaceByOwnerAndNameParams{}) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) @@ -271,9 +271,38 @@ 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) + _, err := client.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, workspace.Name, codersdk.WorkspaceByOwnerAndNameParams{}) require.NoError(t, err) }) + t.Run("Deleted", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + + // Given: + // We delete the workspace + 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 call without includes_deleted, we don't expect to get the workspace back + _, err = client.WorkspaceByOwnerAndName(context.Background(), workspace.OwnerName, workspace.Name, codersdk.WorkspaceByOwnerAndNameParams{}) + 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}) + require.NoError(t, err) + require.Equal(t, workspace.ID, workspaceNew.ID) + }) } func TestWorkspaceFilter(t *testing.T) { diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 3e81645957314..67db13a422459 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -258,9 +258,17 @@ 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) (Workspace, error) { - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspace/%s", owner, name), nil) +func (c *Client) WorkspaceByOwnerAndName(ctx context.Context, owner string, name string, params WorkspaceByOwnerAndNameParams) (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)) + r.URL.RawQuery = q.Encode() + }) if err != nil { return Workspace{}, err } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index e403fe80fddff..61395af7c6d55 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -456,6 +456,11 @@ 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 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