mirror of
https://github.com/coder/coder.git
synced 2025-04-06 12:23:57 +00:00
feat: Single query for all workspaces with optional filter (#1537)
* feat: Add single query for all workspaces using a filter
This commit is contained in:
@ -29,7 +29,7 @@ func list() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
workspaces, err := client.WorkspacesByUser(cmd.Context(), codersdk.Me)
|
||||
workspaces, err := client.Workspaces(cmd.Context(), codersdk.WorkspaceFilter{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -253,7 +253,6 @@ func New(options *Options) (http.Handler, func()) {
|
||||
})
|
||||
r.Get("/gitsshkey", api.gitSSHKey)
|
||||
r.Put("/gitsshkey", api.regenerateGitSSHKey)
|
||||
r.Get("/workspaces", api.workspacesByUser)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -289,23 +288,28 @@ func New(options *Options) (http.Handler, func()) {
|
||||
)
|
||||
r.Get("/", api.workspaceResource)
|
||||
})
|
||||
r.Route("/workspaces/{workspace}", func(r chi.Router) {
|
||||
r.Route("/workspaces", func(r chi.Router) {
|
||||
r.Use(
|
||||
apiKeyMiddleware,
|
||||
authRolesMiddleware,
|
||||
httpmw.ExtractWorkspaceParam(options.Database),
|
||||
)
|
||||
r.Get("/", api.workspace)
|
||||
r.Route("/builds", func(r chi.Router) {
|
||||
r.Get("/", api.workspaceBuilds)
|
||||
r.Post("/", api.postWorkspaceBuilds)
|
||||
r.Get("/{workspacebuildname}", api.workspaceBuildByName)
|
||||
})
|
||||
r.Route("/autostart", func(r chi.Router) {
|
||||
r.Put("/", api.putWorkspaceAutostart)
|
||||
})
|
||||
r.Route("/autostop", func(r chi.Router) {
|
||||
r.Put("/", api.putWorkspaceAutostop)
|
||||
r.Get("/", api.workspaces)
|
||||
r.Route("/{workspace}", func(r chi.Router) {
|
||||
r.Use(
|
||||
httpmw.ExtractWorkspaceParam(options.Database),
|
||||
)
|
||||
r.Get("/", api.workspace)
|
||||
r.Route("/builds", func(r chi.Router) {
|
||||
r.Get("/", api.workspaceBuilds)
|
||||
r.Post("/", api.postWorkspaceBuilds)
|
||||
r.Get("/{workspacebuildname}", api.workspaceBuildByName)
|
||||
})
|
||||
r.Route("/autostart", func(r chi.Router) {
|
||||
r.Put("/", api.putWorkspaceAutostart)
|
||||
})
|
||||
r.Route("/autostop", func(r chi.Router) {
|
||||
r.Put("/", api.putWorkspaceAutostop)
|
||||
})
|
||||
})
|
||||
})
|
||||
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
|
||||
|
@ -78,7 +78,6 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
|
||||
"GET:/api/v2/workspaceagents/me/metadata": {NoAuthorize: true},
|
||||
"GET:/api/v2/workspaceagents/me/turn": {NoAuthorize: true},
|
||||
"GET:/api/v2/workspaceagents/{workspaceagent}": {NoAuthorize: true},
|
||||
"GET:/api/v2/workspaceagents/{workspaceagent}/": {NoAuthorize: true},
|
||||
"GET:/api/v2/workspaceagents/{workspaceagent}/dial": {NoAuthorize: true},
|
||||
"GET:/api/v2/workspaceagents/{workspaceagent}/iceservers": {NoAuthorize: true},
|
||||
"GET:/api/v2/workspaceagents/{workspaceagent}/pty": {NoAuthorize: true},
|
||||
@ -95,7 +94,6 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
|
||||
|
||||
"GET:/api/v2/users/oauth2/github/callback": {NoAuthorize: true},
|
||||
|
||||
"POST:/api/v2/users/{user}/organizations/": {NoAuthorize: true},
|
||||
"PUT:/api/v2/organizations/{organization}/members/{user}/roles": {NoAuthorize: true},
|
||||
"GET:/api/v2/organizations/{organization}/provisionerdaemons": {NoAuthorize: true},
|
||||
"POST:/api/v2/organizations/{organization}/templates": {NoAuthorize: true},
|
||||
@ -143,11 +141,21 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
|
||||
AssertObject: rbac.ResourceWorkspace.InOrg(organization.ID).WithID(workspace.ID.String()).WithOwner(workspace.OwnerID.String()),
|
||||
},
|
||||
"GET:/api/v2/organizations/{organization}/workspaces": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceWorkspace},
|
||||
"GET:/api/v2/workspaces": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceWorkspace},
|
||||
|
||||
// These endpoints need payloads to get to the auth part.
|
||||
"PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
|
||||
}
|
||||
|
||||
for k, v := range assertRoute {
|
||||
noTrailSlash := strings.TrimRight(k, "/")
|
||||
if _, ok := assertRoute[noTrailSlash]; ok && noTrailSlash != k {
|
||||
t.Errorf("route %q & %q is declared twice", noTrailSlash, k)
|
||||
t.FailNow()
|
||||
}
|
||||
assertRoute[noTrailSlash] = v
|
||||
}
|
||||
|
||||
c, _ := srv.Config.Handler.(*chi.Mux)
|
||||
err = chi.Walk(c, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||
name := method + ":" + route
|
||||
|
@ -291,6 +291,27 @@ func (q *fakeQuerier) GetAllUserRoles(_ context.Context, userID uuid.UUID) (data
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspacesWithFilter(_ context.Context, arg database.GetWorkspacesWithFilterParams) ([]database.Workspace, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
workspaces := make([]database.Workspace, 0)
|
||||
for _, workspace := range q.workspaces {
|
||||
if arg.OrganizationID != uuid.Nil && workspace.OrganizationID != arg.OrganizationID {
|
||||
continue
|
||||
}
|
||||
if arg.OwnerID != uuid.Nil && workspace.OwnerID != arg.OwnerID {
|
||||
continue
|
||||
}
|
||||
if !arg.Deleted && workspace.Deleted {
|
||||
continue
|
||||
}
|
||||
workspaces = append(workspaces, workspace)
|
||||
}
|
||||
|
||||
return workspaces, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspacesByTemplateID(_ context.Context, arg database.GetWorkspacesByTemplateIDParams) ([]database.Workspace, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
@ -486,26 +507,6 @@ func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndName(_ context.Context, a
|
||||
return database.WorkspaceBuild{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspacesByOrganizationID(_ context.Context, req database.GetWorkspacesByOrganizationIDParams) ([]database.Workspace, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
workspaces := make([]database.Workspace, 0)
|
||||
for _, workspace := range q.workspaces {
|
||||
if workspace.OrganizationID != req.OrganizationID {
|
||||
continue
|
||||
}
|
||||
if workspace.Deleted != req.Deleted {
|
||||
continue
|
||||
}
|
||||
workspaces = append(workspaces, workspace)
|
||||
}
|
||||
if len(workspaces) == 0 {
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
return workspaces, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspacesByOrganizationIDs(_ context.Context, req database.GetWorkspacesByOrganizationIDsParams) ([]database.Workspace, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
@ -525,23 +526,6 @@ func (q *fakeQuerier) GetWorkspacesByOrganizationIDs(_ context.Context, req data
|
||||
return workspaces, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspacesByOwnerID(_ context.Context, req database.GetWorkspacesByOwnerIDParams) ([]database.Workspace, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
workspaces := make([]database.Workspace, 0)
|
||||
for _, workspace := range q.workspaces {
|
||||
if workspace.OwnerID != req.OwnerID {
|
||||
continue
|
||||
}
|
||||
if workspace.Deleted != req.Deleted {
|
||||
continue
|
||||
}
|
||||
workspaces = append(workspaces, workspace)
|
||||
}
|
||||
return workspaces, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetOrganizations(_ context.Context) ([]database.Organization, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
@ -71,10 +71,9 @@ type querier interface {
|
||||
GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error)
|
||||
GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error)
|
||||
GetWorkspacesAutostartAutostop(ctx context.Context) ([]Workspace, error)
|
||||
GetWorkspacesByOrganizationID(ctx context.Context, arg GetWorkspacesByOrganizationIDParams) ([]Workspace, error)
|
||||
GetWorkspacesByOrganizationIDs(ctx context.Context, arg GetWorkspacesByOrganizationIDsParams) ([]Workspace, error)
|
||||
GetWorkspacesByOwnerID(ctx context.Context, arg GetWorkspacesByOwnerIDParams) ([]Workspace, error)
|
||||
GetWorkspacesByTemplateID(ctx context.Context, arg GetWorkspacesByTemplateIDParams) ([]Workspace, error)
|
||||
GetWorkspacesWithFilter(ctx context.Context, arg GetWorkspacesWithFilterParams) ([]Workspace, error)
|
||||
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)
|
||||
InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) (AuditLog, error)
|
||||
InsertFile(ctx context.Context, arg InsertFileParams) (File, error)
|
||||
|
@ -3314,49 +3314,6 @@ func (q *sqlQuerier) GetWorkspacesAutostartAutostop(ctx context.Context) ([]Work
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspacesByOrganizationID = `-- name: GetWorkspacesByOrganizationID :many
|
||||
SELECT id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule FROM workspaces WHERE organization_id = $1 AND deleted = $2
|
||||
`
|
||||
|
||||
type GetWorkspacesByOrganizationIDParams struct {
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetWorkspacesByOrganizationID(ctx context.Context, arg GetWorkspacesByOrganizationIDParams) ([]Workspace, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspacesByOrganizationID, arg.OrganizationID, arg.Deleted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Workspace
|
||||
for rows.Next() {
|
||||
var i Workspace
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
&i.OrganizationID,
|
||||
&i.TemplateID,
|
||||
&i.Deleted,
|
||||
&i.Name,
|
||||
&i.AutostartSchedule,
|
||||
&i.AutostopSchedule,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspacesByOrganizationIDs = `-- name: GetWorkspacesByOrganizationIDs :many
|
||||
SELECT id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule FROM workspaces WHERE organization_id = ANY($1 :: uuid [ ]) AND deleted = $2
|
||||
`
|
||||
@ -3400,55 +3357,6 @@ func (q *sqlQuerier) GetWorkspacesByOrganizationIDs(ctx context.Context, arg Get
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspacesByOwnerID = `-- name: GetWorkspacesByOwnerID :many
|
||||
SELECT
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
owner_id = $1
|
||||
AND deleted = $2
|
||||
`
|
||||
|
||||
type GetWorkspacesByOwnerIDParams struct {
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetWorkspacesByOwnerID(ctx context.Context, arg GetWorkspacesByOwnerIDParams) ([]Workspace, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspacesByOwnerID, arg.OwnerID, arg.Deleted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Workspace
|
||||
for rows.Next() {
|
||||
var i Workspace
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
&i.OrganizationID,
|
||||
&i.TemplateID,
|
||||
&i.Deleted,
|
||||
&i.Name,
|
||||
&i.AutostartSchedule,
|
||||
&i.AutostopSchedule,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspacesByTemplateID = `-- name: GetWorkspacesByTemplateID :many
|
||||
SELECT
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule
|
||||
@ -3498,6 +3406,68 @@ func (q *sqlQuerier) GetWorkspacesByTemplateID(ctx context.Context, arg GetWorks
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspacesWithFilter = `-- name: GetWorkspacesWithFilter :many
|
||||
SELECT
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
-- Optionally include deleted workspaces
|
||||
deleted = $1
|
||||
-- Filter by organization_id
|
||||
AND CASE
|
||||
WHEN $2 :: uuid != '00000000-00000000-00000000-00000000' THEN
|
||||
organization_id = $2
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by owner_id
|
||||
AND CASE
|
||||
WHEN $3 :: uuid != '00000000-00000000-00000000-00000000' THEN
|
||||
owner_id = $3
|
||||
ELSE true
|
||||
END
|
||||
`
|
||||
|
||||
type GetWorkspacesWithFilterParams struct {
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetWorkspacesWithFilter(ctx context.Context, arg GetWorkspacesWithFilterParams) ([]Workspace, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspacesWithFilter, arg.Deleted, arg.OrganizationID, arg.OwnerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Workspace
|
||||
for rows.Next() {
|
||||
var i Workspace
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
&i.OrganizationID,
|
||||
&i.TemplateID,
|
||||
&i.Deleted,
|
||||
&i.Name,
|
||||
&i.AutostartSchedule,
|
||||
&i.AutostopSchedule,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertWorkspace = `-- name: InsertWorkspace :one
|
||||
INSERT INTO
|
||||
workspaces (
|
||||
|
@ -8,8 +8,27 @@ WHERE
|
||||
LIMIT
|
||||
1;
|
||||
|
||||
-- name: GetWorkspacesByOrganizationID :many
|
||||
SELECT * FROM workspaces WHERE organization_id = $1 AND deleted = $2;
|
||||
-- name: GetWorkspacesWithFilter :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
-- Optionally include deleted workspaces
|
||||
deleted = @deleted
|
||||
-- Filter by organization_id
|
||||
AND CASE
|
||||
WHEN @organization_id :: uuid != '00000000-00000000-00000000-00000000' THEN
|
||||
organization_id = @organization_id
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by owner_id
|
||||
AND CASE
|
||||
WHEN @owner_id :: uuid != '00000000-00000000-00000000-00000000' THEN
|
||||
owner_id = @owner_id
|
||||
ELSE true
|
||||
END
|
||||
;
|
||||
|
||||
-- name: GetWorkspacesByOrganizationIDs :many
|
||||
SELECT * FROM workspaces WHERE organization_id = ANY(@ids :: uuid [ ]) AND deleted = @deleted;
|
||||
@ -37,15 +56,6 @@ WHERE
|
||||
template_id = $1
|
||||
AND deleted = $2;
|
||||
|
||||
-- name: GetWorkspacesByOwnerID :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
owner_id = $1
|
||||
AND deleted = $2;
|
||||
|
||||
-- name: GetWorkspaceByOwnerIDAndName :one
|
||||
SELECT
|
||||
*
|
||||
|
@ -599,7 +599,9 @@ func TestWorkspacesByUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
workspaces, err := client.WorkspacesByUser(context.Background(), codersdk.Me)
|
||||
workspaces, err := client.Workspaces(context.Background(), codersdk.WorkspaceFilter{
|
||||
Owner: codersdk.Me,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workspaces, 0)
|
||||
})
|
||||
@ -628,11 +630,11 @@ func TestWorkspacesByUser(t *testing.T) {
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
|
||||
workspaces, err := newUserClient.WorkspacesByUser(context.Background(), codersdk.Me)
|
||||
workspaces, err := newUserClient.Workspaces(context.Background(), codersdk.WorkspaceFilter{Owner: codersdk.Me})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workspaces, 0)
|
||||
|
||||
workspaces, err = client.WorkspacesByUser(context.Background(), codersdk.Me)
|
||||
workspaces, err = client.Workspaces(context.Background(), codersdk.WorkspaceFilter{Owner: codersdk.Me})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workspaces, 1)
|
||||
})
|
||||
|
@ -70,7 +70,7 @@ func (api *api) workspace(rw http.ResponseWriter, r *http.Request) {
|
||||
func (api *api) workspacesByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
roles := httpmw.UserRoles(r)
|
||||
workspaces, err := api.Database.GetWorkspacesByOrganizationID(r.Context(), database.GetWorkspacesByOrganizationIDParams{
|
||||
workspaces, err := api.Database.GetWorkspacesWithFilter(r.Context(), database.GetWorkspacesWithFilterParams{
|
||||
OrganizationID: organization.ID,
|
||||
Deleted: false,
|
||||
})
|
||||
@ -104,30 +104,67 @@ func (api *api) workspacesByOrganization(rw http.ResponseWriter, r *http.Request
|
||||
httpapi.Write(rw, http.StatusOK, apiWorkspaces)
|
||||
}
|
||||
|
||||
func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) {
|
||||
user := httpmw.UserParam(r)
|
||||
// workspaces returns all workspaces a user can read.
|
||||
// Optional filters with query params
|
||||
func (api *api) workspaces(rw http.ResponseWriter, r *http.Request) {
|
||||
roles := httpmw.UserRoles(r)
|
||||
apiKey := httpmw.APIKey(r)
|
||||
|
||||
allWorkspaces := make([]database.Workspace, 0)
|
||||
userWorkspaces, err := api.Database.GetWorkspacesByOwnerID(r.Context(), database.GetWorkspacesByOwnerIDParams{
|
||||
OwnerID: user.ID,
|
||||
})
|
||||
// Empty strings mean no filter
|
||||
orgFilter := r.URL.Query().Get("organization_id")
|
||||
ownerFilter := r.URL.Query().Get("owner_id")
|
||||
|
||||
filter := database.GetWorkspacesWithFilterParams{Deleted: false}
|
||||
if orgFilter != "" {
|
||||
orgID, err := uuid.Parse(orgFilter)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: fmt.Sprintf("organization_id must be a uuid: %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
filter.OrganizationID = orgID
|
||||
}
|
||||
if ownerFilter == "me" {
|
||||
filter.OwnerID = apiKey.UserID
|
||||
} else if ownerFilter != "" {
|
||||
userID, err := uuid.Parse(ownerFilter)
|
||||
if err != nil {
|
||||
// Maybe it's a username
|
||||
user, err := api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
|
||||
// Why not just accept 1 arg and use it for both in the sql?
|
||||
Username: ownerFilter,
|
||||
Email: ownerFilter,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: "owner must be a uuid or username",
|
||||
})
|
||||
return
|
||||
}
|
||||
userID = user.ID
|
||||
}
|
||||
filter.OwnerID = userID
|
||||
}
|
||||
|
||||
allowedWorkspaces := make([]database.Workspace, 0)
|
||||
allWorkspaces, err := api.Database.GetWorkspacesWithFilter(r.Context(), filter)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspaces for user: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
for _, ws := range userWorkspaces {
|
||||
for _, ws := range allWorkspaces {
|
||||
ws := ws
|
||||
err = api.Authorizer.ByRoleName(r.Context(), user.ID.String(), roles.Roles, rbac.ActionRead,
|
||||
err = api.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, rbac.ActionRead,
|
||||
rbac.ResourceWorkspace.InOrg(ws.OrganizationID).WithOwner(ws.OwnerID.String()).WithID(ws.ID.String()))
|
||||
if err == nil {
|
||||
allWorkspaces = append(allWorkspaces, ws)
|
||||
allowedWorkspaces = append(allowedWorkspaces, ws)
|
||||
}
|
||||
}
|
||||
|
||||
apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, allWorkspaces)
|
||||
apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, allowedWorkspaces)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("convert workspaces: %s", err),
|
||||
@ -140,8 +177,9 @@ func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) {
|
||||
func (api *api) workspacesByOwner(rw http.ResponseWriter, r *http.Request) {
|
||||
owner := httpmw.UserParam(r)
|
||||
roles := httpmw.UserRoles(r)
|
||||
workspaces, err := api.Database.GetWorkspacesByOwnerID(r.Context(), database.GetWorkspacesByOwnerIDParams{
|
||||
workspaces, err := api.Database.GetWorkspacesWithFilter(r.Context(), database.GetWorkspacesWithFilterParams{
|
||||
OwnerID: owner.ID,
|
||||
Deleted: false,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
|
@ -134,16 +134,29 @@ func TestWorkspacesByOwner(t *testing.T) {
|
||||
_, err := client.WorkspacesByOwner(context.Background(), user.OrganizationID, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("List", func(t *testing.T) {
|
||||
|
||||
t.Run("ListMine", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
me, err := client.User(context.Background(), codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
workspaces, err := client.WorkspacesByOwner(context.Background(), user.OrganizationID, codersdk.Me)
|
||||
|
||||
// Create noise workspace that should be filtered out
|
||||
other := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
|
||||
_ = coderdtest.CreateWorkspace(t, other, user.OrganizationID, template.ID)
|
||||
|
||||
// Use a username
|
||||
workspaces, err := client.Workspaces(context.Background(), codersdk.WorkspaceFilter{
|
||||
OrganizationID: user.OrganizationID,
|
||||
Owner: me.Username,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workspaces, 1)
|
||||
})
|
||||
|
@ -438,19 +438,3 @@ func (c *Client) AuthMethods(ctx context.Context) (AuthMethods, error) {
|
||||
var userAuth AuthMethods
|
||||
return userAuth, json.NewDecoder(res.Body).Decode(&userAuth)
|
||||
}
|
||||
|
||||
// WorkspacesByUser returns all workspaces a user has access to.
|
||||
func (c *Client) WorkspacesByUser(ctx context.Context, user string) ([]Workspace, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspaces", user), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
|
||||
var workspaces []Workspace
|
||||
return workspaces, json.NewDecoder(res.Body).Decode(&workspaces)
|
||||
}
|
||||
|
@ -131,3 +131,41 @@ func (c *Client) UpdateWorkspaceAutostop(ctx context.Context, id uuid.UUID, req
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type WorkspaceFilter struct {
|
||||
OrganizationID uuid.UUID
|
||||
// Owner can be a user_id (uuid), "me", or a username
|
||||
Owner string
|
||||
}
|
||||
|
||||
// asRequestOption returns a function that can be used in (*Client).Request.
|
||||
// It modifies the request query parameters.
|
||||
func (f WorkspaceFilter) asRequestOption() requestOption {
|
||||
return func(r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
if f.OrganizationID != uuid.Nil {
|
||||
q.Set("organization_id", f.OrganizationID.String())
|
||||
}
|
||||
if f.Owner != "" {
|
||||
q.Set("owner_id", f.Owner)
|
||||
}
|
||||
r.URL.RawQuery = q.Encode()
|
||||
}
|
||||
}
|
||||
|
||||
// Workspaces returns all workspaces the authenticated user has access to.
|
||||
func (c *Client) Workspaces(ctx context.Context, filter WorkspaceFilter) ([]Workspace, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, "/api/v2/workspaces", nil, filter.asRequestOption())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
|
||||
var workspaces []Workspace
|
||||
return workspaces, json.NewDecoder(res.Body).Decode(&workspaces)
|
||||
}
|
||||
|
@ -120,8 +120,10 @@ export const getWorkspace = async (workspaceId: string): Promise<TypesGen.Worksp
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const getWorkspaces = async (userID = "me"): Promise<TypesGen.Workspace[]> => {
|
||||
const response = await axios.get<TypesGen.Workspace[]>(`/api/v2/users/${userID}/workspaces`)
|
||||
// TODO: @emyrk add query params as arguments. Supports 'organization_id' and 'owner'
|
||||
// 'owner' can be a username, user_id, or 'me'
|
||||
export const getWorkspaces = async (): Promise<TypesGen.Workspace[]> => {
|
||||
const response = await axios.get<TypesGen.Workspace[]>(`/api/v2/workspaces`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
|
@ -430,6 +430,12 @@ export interface WorkspaceBuild {
|
||||
readonly job: ProvisionerJob
|
||||
}
|
||||
|
||||
// From codersdk/workspaces.go:135:6
|
||||
export interface WorkspaceFilter {
|
||||
readonly OrganizationID: string
|
||||
readonly OwnerID: string
|
||||
}
|
||||
|
||||
// From codersdk/workspaceresources.go:23:6
|
||||
export interface WorkspaceResource {
|
||||
readonly id: string
|
||||
|
@ -15,7 +15,7 @@ describe("WorkspacesPage", () => {
|
||||
it("renders an empty workspaces page", async () => {
|
||||
// Given
|
||||
server.use(
|
||||
rest.get("/api/v2/users/me/workspaces", async (req, res, ctx) => {
|
||||
rest.get("/api/v2/workspaces", async (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json([]))
|
||||
}),
|
||||
)
|
||||
|
@ -36,7 +36,7 @@ export const handlers = [
|
||||
rest.post("/api/v2/users/me/workspaces", async (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(M.MockWorkspace))
|
||||
}),
|
||||
rest.get("/api/v2/users/me/workspaces", async (req, res, ctx) => {
|
||||
rest.get("/api/v2/workspaces", async (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json([M.MockWorkspace]))
|
||||
}),
|
||||
rest.get("/api/v2/users/me/organizations", (req, res, ctx) => {
|
||||
|
Reference in New Issue
Block a user