Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: don't return 200 for deleted workspaces #1556

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 19, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: don't return 200 for deleted workspaces
  • Loading branch information
deansheather committed May 18, 2022
commit f7984cd32d0e70e21d9f1933ec87bd57c7e8075d
41 changes: 36 additions & 5 deletions coderd/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"net/http"
"strconv"

"github.com/go-chi/chi/v5"
"github.com/google/uuid"
Expand All @@ -25,6 +26,41 @@ import (
func (api *api) workspace(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r)

if !api.Authorize(rw, r, rbac.ActionRead,
rbac.ResourceWorkspace.InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
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")
deleted = false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: semantics: suggest renaming this to showDeleted

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the query parameter or just this variable name? I'm not a fan of capital letters in query params (or any URL stuff)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I was mainly thinking about the variable name, deleted=1 as a query parameter is fairly standard imo.

)
if deletedStr != "" {
var err error
deleted, err = strconv.ParseBool(deletedStr)
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("invalid bool for 'deleted' query param: %s", err),
})
return
}
}
if workspace.Deleted != deleted {
if workspace.Deleted {
httpapi.Write(rw, http.StatusGone, httpapi.Response{
Message: fmt.Sprintf("workspace %q was deleted, you can view this workspace by specifying '?deleted=true' and trying again", workspace.ID.String()),
})
return
}

httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("workspace %q is not deleted, please remove '?deleted=true' and try again", workspace.ID.String()),
})
return
}

build, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Expand Down Expand Up @@ -58,11 +94,6 @@ func (api *api) workspace(rw http.ResponseWriter, r *http.Request) {
return
}

if !api.Authorize(rw, r, rbac.ActionRead,
rbac.ResourceWorkspace.InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
return
}

httpapi.Write(rw, http.StatusOK,
convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template, owner))
}
Expand Down
57 changes: 48 additions & 9 deletions coderd/workspaces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,54 @@ import (

func TestWorkspace(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(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)
_, err := client.Workspace(context.Background(), workspace.ID)
require.NoError(t, err)

t.Run("OK", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(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)

_, err := client.Workspace(context.Background(), workspace.ID)
require.NoError(t, err)
})

t.Run("Deleted", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(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)

// Getting with deleted=true should fail.
_, err := client.DeletedWorkspace(context.Background(), workspace.ID)
require.Error(t, err)
require.ErrorContains(t, err, "400") // bad request

// Delete the workspace
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: database.WorkspaceTransitionDelete,
})
require.NoError(t, err, "delete the workspace")
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)

// Getting with deleted=true should work.
workspaceNew, err := client.DeletedWorkspace(context.Background(), workspace.ID)
require.NoError(t, err)
require.Equal(t, workspace.ID, workspaceNew.ID)

// Getting with deleted=false should not work.
_, err = client.Workspace(context.Background(), workspace.ID)
require.Error(t, err)
require.ErrorContains(t, err, "410") // gone
})
}

func TestPostWorkspacesByOrganization(t *testing.T) {
Expand Down
8 changes: 8 additions & 0 deletions codersdk/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ type Client struct {

type requestOption func(*http.Request)

func queryParam(k, v string) requestOption {
return func(r *http.Request) {
q := r.URL.Query()
q.Set(k, v)
r.URL.RawQuery = q.Encode()
}
}

// Request performs an HTTP request with the body provided.
// The caller is responsible for closing the response body.
func (c *Client) Request(ctx context.Context, method, path string, body interface{}, opts ...requestOption) (*http.Response, error) {
Expand Down
11 changes: 10 additions & 1 deletion codersdk/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,16 @@ type CreateWorkspaceBuildRequest struct {

// Workspace returns a single workspace.
func (c *Client) Workspace(ctx context.Context, id uuid.UUID) (Workspace, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s", id), nil)
return c.getWorkspace(ctx, id)
}

// DeletedWorkspace returns a single workspace that was deleted.
func (c *Client) DeletedWorkspace(ctx context.Context, id uuid.UUID) (Workspace, error) {
return c.getWorkspace(ctx, id, queryParam("deleted", "true"))
}

func (c *Client) getWorkspace(ctx context.Context, id uuid.UUID, opts ...requestOption) (Workspace, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s", id), nil, opts...)
if err != nil {
return Workspace{}, err
}
Expand Down