From 142ad4eeb8b188577c2dde35cd2ae78734f59c93 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Wed, 8 Jun 2022 13:01:47 +0000 Subject: [PATCH 1/9] feat: added include_deleted relates to #1955 --- coderd/workspaces.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 25fd67d581cba..6e90e56c94e72 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -165,10 +165,37 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) owner := httpmw.UserParam(r) workspaceName := chi.URLParam(r, "workspacename") + var ( + includeDeletedStr = r.URL.Query().Get("include_deleted") + includeDeleted = false + ) + if includeDeletedStr != "" { + var err error + includeDeleted, err = strconv.ParseBool(includeDeletedStr) + if err != nil { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("Invalid boolean value %q for \"include_deleted\" query param.", includeDeletedStr), + 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) From 5c77ea41104c1c0be625697e1efe66bd99a082ac Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Wed, 8 Jun 2022 10:25:04 -0400 Subject: [PATCH 2/9] Update coderd/workspaces.go defining vars in the scope of conditional Co-authored-by: Mathias Fredriksson --- coderd/workspaces.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 6e90e56c94e72..0885053df90ca 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -165,13 +165,10 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) owner := httpmw.UserParam(r) workspaceName := chi.URLParam(r, "workspacename") - var ( - includeDeletedStr = r.URL.Query().Get("include_deleted") - includeDeleted = false - ) - if includeDeletedStr != "" { + includeDeleted := false + if s := r.URL.Query().Get("include_deleted"); s != "" { var err error - includeDeleted, err = strconv.ParseBool(includeDeletedStr) + 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.", includeDeletedStr), From 31f40136cb27dfca966874cd3cb810105df4d17a Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Wed, 8 Jun 2022 10:26:02 -0400 Subject: [PATCH 3/9] Update coderd/workspaces.go avoid newline Co-authored-by: Mathias Fredriksson --- coderd/workspaces.go | 1 - 1 file changed, 1 deletion(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 0885053df90ca..5744d192c20df 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -184,7 +184,6 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) OwnerID: owner.ID, Name: workspaceName, }) - if includeDeleted && errors.Is(err, sql.ErrNoRows) { workspace, err = api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{ OwnerID: owner.ID, From 9389df38dfface97e2c04cbea516337035b10583 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Wed, 8 Jun 2022 10:26:11 -0400 Subject: [PATCH 4/9] Update coderd/workspaces.go Co-authored-by: Mathias Fredriksson --- coderd/workspaces.go | 1 - 1 file changed, 1 deletion(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 5744d192c20df..020fd78d37456 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -191,7 +191,6 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) Deleted: includeDeleted, }) } - if errors.Is(err, sql.ErrNoRows) { // Do not leak information if the workspace exists or not httpapi.Forbidden(rw) From 769ea99882eb7ef96efb23e2f08cce2e687b53dc Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Wed, 8 Jun 2022 15:13:48 +0000 Subject: [PATCH 5/9] PR feedback --- .vscode/settings.json | 1 + coderd/workspaces.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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/coderd/workspaces.go b/coderd/workspaces.go index 020fd78d37456..079f880ed44f3 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -171,7 +171,7 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) 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.", includeDeletedStr), + 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"}, }, From f8a844ce1965584c7490c5106f53e9161d15286d Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Wed, 8 Jun 2022 16:36:26 +0000 Subject: [PATCH 6/9] wrote test, added type --- cli/create.go | 4 ++-- cli/root.go | 2 +- coderd/workspaces_test.go | 33 +++++++++++++++++++++++++++++++-- codersdk/workspaces.go | 12 ++++++++++-- site/src/api/api.ts | 4 +++- site/src/api/typesGenerated.ts | 5 +++++ 6 files changed, 52 insertions(+), 8 deletions(-) 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_test.go b/coderd/workspaces_test.go index 665c2d0a49d88..58b705ea27916 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("deletedGetWorkspaceByOwnerAndName", 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/api.ts b/site/src/api/api.ts index c86748b2b64cd..c2186f8dc4cad 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -142,7 +142,9 @@ export const getWorkspaceByOwnerAndName = async ( username = "me", workspaceName: string, ): Promise => { - const response = await axios.get(`/api/v2/users/${username}/workspace/${workspaceName}`) + const response = await axios.get(`/api/v2/users/${username}/workspace/${workspaceName}`, { + params: { include_deleted: true }, + }) return response.data } 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 From 56bceb614a62535167f9cd3ce5bca2202f12b6cb Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Wed, 8 Jun 2022 13:23:35 -0400 Subject: [PATCH 7/9] Update coderd/workspaces_test.go shortening test name Co-authored-by: Cian Johnston --- coderd/workspaces_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 58b705ea27916..e28a0b77f9c9e 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -274,7 +274,7 @@ func TestWorkspaceByOwnerAndName(t *testing.T) { _, err := client.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, workspace.Name, codersdk.WorkspaceByOwnerAndNameParams{}) require.NoError(t, err) }) - t.Run("deletedGetWorkspaceByOwnerAndName", func(t *testing.T) { + t.Run("Deleted", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) From ee1f417fdadf0d6a7f353beb36e439c3ea258be0 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Wed, 8 Jun 2022 17:38:37 +0000 Subject: [PATCH 8/9] taking out api.ts change for now --- site/src/api/api.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index c2186f8dc4cad..c86748b2b64cd 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -142,9 +142,7 @@ export const getWorkspaceByOwnerAndName = async ( username = "me", workspaceName: string, ): Promise => { - const response = await axios.get(`/api/v2/users/${username}/workspace/${workspaceName}`, { - params: { include_deleted: true }, - }) + const response = await axios.get(`/api/v2/users/${username}/workspace/${workspaceName}`) return response.data } From 538155464921571e46709deae63cd2717295718f Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Wed, 8 Jun 2022 17:40:21 +0000 Subject: [PATCH 9/9] casing --- coderd/workspaces_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index e28a0b77f9c9e..73fed89c80c79 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -293,7 +293,7 @@ func TestWorkspaceByOwnerAndName(t *testing.T) { coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID) // Then: - // when we call without includes_deleted, we don't expect to get the workspace back + // 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")