mirror of
https://github.com/coder/coder.git
synced 2025-07-08 11:39:50 +00:00
chore: refactor workspace count to single route (#4809)
Co-authored-by: Presley Pizzo <presley@coder.com>
This commit is contained in:
@ -524,7 +524,6 @@ func New(options *Options) *API {
|
||||
apiKeyMiddleware,
|
||||
)
|
||||
r.Get("/", api.workspaces)
|
||||
r.Get("/count", api.workspaceCount)
|
||||
r.Route("/{workspace}", func(r chi.Router) {
|
||||
r.Use(
|
||||
httpmw.ExtractWorkspaceParam(options.Database),
|
||||
|
@ -244,9 +244,8 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
|
||||
"POST:/api/v2/organizations/{organization}/templateversions": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
|
||||
|
||||
// Endpoints that use the SQLQuery filter.
|
||||
"GET:/api/v2/workspaces/": {StatusCode: http.StatusOK, NoAuthorize: true},
|
||||
"GET:/api/v2/workspaces/count": {StatusCode: http.StatusOK, NoAuthorize: true},
|
||||
"GET:/api/v2/users/count": {StatusCode: http.StatusOK, NoAuthorize: true},
|
||||
"GET:/api/v2/workspaces/": {StatusCode: http.StatusOK, NoAuthorize: true},
|
||||
"GET:/api/v2/users/count": {StatusCode: http.StatusOK, NoAuthorize: true},
|
||||
}
|
||||
|
||||
// Routes like proxy routes support all HTTP methods. A helper func to expand
|
||||
|
@ -857,156 +857,6 @@ func (q *fakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
|
||||
return workspaces, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceCount(ctx context.Context, arg database.GetWorkspaceCountParams) (int64, error) {
|
||||
count, err := q.GetAuthorizedWorkspaceCount(ctx, arg, nil)
|
||||
return count, err
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func (q *fakeQuerier) GetAuthorizedWorkspaceCount(ctx context.Context, arg database.GetWorkspaceCountParams, authorizedFilter rbac.AuthorizeFilter) (int64, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
workspaces := make([]database.Workspace, 0)
|
||||
for _, workspace := range q.workspaces {
|
||||
if arg.OwnerID != uuid.Nil && workspace.OwnerID != arg.OwnerID {
|
||||
continue
|
||||
}
|
||||
|
||||
if arg.OwnerUsername != "" {
|
||||
owner, err := q.GetUserByID(ctx, workspace.OwnerID)
|
||||
if err == nil && !strings.EqualFold(arg.OwnerUsername, owner.Username) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if arg.TemplateName != "" {
|
||||
template, err := q.GetTemplateByID(ctx, workspace.TemplateID)
|
||||
if err == nil && !strings.EqualFold(arg.TemplateName, template.Name) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if !arg.Deleted && workspace.Deleted {
|
||||
continue
|
||||
}
|
||||
|
||||
if arg.Name != "" && !strings.Contains(strings.ToLower(workspace.Name), strings.ToLower(arg.Name)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if arg.Status != "" {
|
||||
build, err := q.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("get latest build: %w", err)
|
||||
}
|
||||
|
||||
job, err := q.GetProvisionerJobByID(ctx, build.JobID)
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("get provisioner job: %w", err)
|
||||
}
|
||||
|
||||
switch arg.Status {
|
||||
case "pending":
|
||||
if !job.StartedAt.Valid {
|
||||
continue
|
||||
}
|
||||
|
||||
case "starting":
|
||||
if !job.StartedAt.Valid &&
|
||||
!job.CanceledAt.Valid &&
|
||||
job.CompletedAt.Valid &&
|
||||
time.Since(job.UpdatedAt) > 30*time.Second ||
|
||||
build.Transition != database.WorkspaceTransitionStart {
|
||||
continue
|
||||
}
|
||||
|
||||
case "running":
|
||||
if !job.CompletedAt.Valid &&
|
||||
job.CanceledAt.Valid &&
|
||||
job.Error.Valid ||
|
||||
build.Transition != database.WorkspaceTransitionStart {
|
||||
continue
|
||||
}
|
||||
|
||||
case "stopping":
|
||||
if !job.StartedAt.Valid &&
|
||||
!job.CanceledAt.Valid &&
|
||||
job.CompletedAt.Valid &&
|
||||
time.Since(job.UpdatedAt) > 30*time.Second ||
|
||||
build.Transition != database.WorkspaceTransitionStop {
|
||||
continue
|
||||
}
|
||||
|
||||
case "stopped":
|
||||
if !job.CompletedAt.Valid &&
|
||||
job.CanceledAt.Valid &&
|
||||
job.Error.Valid ||
|
||||
build.Transition != database.WorkspaceTransitionStop {
|
||||
continue
|
||||
}
|
||||
|
||||
case "failed":
|
||||
if (!job.CanceledAt.Valid && !job.Error.Valid) ||
|
||||
(!job.CompletedAt.Valid && !job.Error.Valid) {
|
||||
continue
|
||||
}
|
||||
|
||||
case "canceling":
|
||||
if !job.CanceledAt.Valid && job.CompletedAt.Valid {
|
||||
continue
|
||||
}
|
||||
|
||||
case "canceled":
|
||||
if !job.CanceledAt.Valid && !job.CompletedAt.Valid {
|
||||
continue
|
||||
}
|
||||
|
||||
case "deleted":
|
||||
if !job.StartedAt.Valid &&
|
||||
job.CanceledAt.Valid &&
|
||||
!job.CompletedAt.Valid &&
|
||||
time.Since(job.UpdatedAt) > 30*time.Second ||
|
||||
build.Transition != database.WorkspaceTransitionDelete {
|
||||
continue
|
||||
}
|
||||
|
||||
case "deleting":
|
||||
if !job.CompletedAt.Valid &&
|
||||
job.CanceledAt.Valid &&
|
||||
job.Error.Valid &&
|
||||
build.Transition != database.WorkspaceTransitionDelete {
|
||||
continue
|
||||
}
|
||||
|
||||
default:
|
||||
return 0, xerrors.Errorf("unknown workspace status in filter: %q", arg.Status)
|
||||
}
|
||||
}
|
||||
|
||||
if len(arg.TemplateIds) > 0 {
|
||||
match := false
|
||||
for _, id := range arg.TemplateIds {
|
||||
if workspace.TemplateID == id {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// If the filter exists, ensure the object is authorized.
|
||||
if authorizedFilter != nil && !authorizedFilter.Eval(workspace.RBACObject()) {
|
||||
continue
|
||||
}
|
||||
workspaces = append(workspaces, workspace)
|
||||
}
|
||||
|
||||
return int64(len(workspaces)), nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceByID(_ context.Context, id uuid.UUID) (database.Workspace, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
@ -113,7 +113,6 @@ func (q *sqlQuerier) GetTemplateGroupRoles(ctx context.Context, id uuid.UUID) ([
|
||||
|
||||
type workspaceQuerier interface {
|
||||
GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspacesParams, authorizedFilter rbac.AuthorizeFilter) ([]Workspace, error)
|
||||
GetAuthorizedWorkspaceCount(ctx context.Context, arg GetWorkspaceCountParams, authorizedFilter rbac.AuthorizeFilter) (int64, error)
|
||||
}
|
||||
|
||||
// GetAuthorizedWorkspaces returns all workspaces that the user is authorized to access.
|
||||
@ -169,24 +168,6 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetAuthorizedWorkspaceCount(ctx context.Context, arg GetWorkspaceCountParams, authorizedFilter rbac.AuthorizeFilter) (int64, error) {
|
||||
filter := strings.Replace(getWorkspaceCount, "-- @authorize_filter", fmt.Sprintf(" AND %s", authorizedFilter.SQLString(rbac.NoACLConfig())), 1)
|
||||
// The name comment is for metric tracking
|
||||
query := fmt.Sprintf("-- name: GetAuthorizedWorkspaceCount :one\n%s", filter)
|
||||
row := q.db.QueryRowContext(ctx, query,
|
||||
arg.Deleted,
|
||||
arg.Status,
|
||||
arg.OwnerID,
|
||||
arg.OwnerUsername,
|
||||
arg.TemplateName,
|
||||
pq.Array(arg.TemplateIds),
|
||||
arg.Name,
|
||||
)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
type userQuerier interface {
|
||||
GetAuthorizedUserCount(ctx context.Context, arg GetFilteredUserCountParams, authorizedFilter rbac.AuthorizeFilter) (int64, error)
|
||||
}
|
||||
|
@ -112,8 +112,6 @@ type sqlcQuerier interface {
|
||||
GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error)
|
||||
GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Workspace, error)
|
||||
GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWorkspaceByOwnerIDAndNameParams) (Workspace, error)
|
||||
// this duplicates the filtering in GetWorkspaces
|
||||
GetWorkspaceCount(ctx context.Context, arg GetWorkspaceCountParams) (int64, error)
|
||||
GetWorkspaceCountByUserID(ctx context.Context, ownerID uuid.UUID) (int64, error)
|
||||
GetWorkspaceOwnerCountsByTemplateIDs(ctx context.Context, ids []uuid.UUID) ([]GetWorkspaceOwnerCountsByTemplateIDsRow, error)
|
||||
GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error)
|
||||
|
@ -5988,160 +5988,6 @@ func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWo
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getWorkspaceCount = `-- name: GetWorkspaceCount :one
|
||||
SELECT
|
||||
COUNT(*) as count
|
||||
FROM
|
||||
workspaces
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
workspace_builds.transition,
|
||||
provisioner_jobs.started_at,
|
||||
provisioner_jobs.updated_at,
|
||||
provisioner_jobs.canceled_at,
|
||||
provisioner_jobs.completed_at,
|
||||
provisioner_jobs.error
|
||||
FROM
|
||||
workspace_builds
|
||||
LEFT JOIN
|
||||
provisioner_jobs
|
||||
ON
|
||||
provisioner_jobs.id = workspace_builds.job_id
|
||||
WHERE
|
||||
workspace_builds.workspace_id = workspaces.id
|
||||
ORDER BY
|
||||
build_number DESC
|
||||
LIMIT
|
||||
1
|
||||
) latest_build ON TRUE
|
||||
WHERE
|
||||
-- Optionally include deleted workspaces
|
||||
workspaces.deleted = $1
|
||||
AND CASE
|
||||
WHEN $2 :: text != '' THEN
|
||||
CASE
|
||||
WHEN $2 = 'pending' THEN
|
||||
latest_build.started_at IS NULL
|
||||
WHEN $2 = 'starting' THEN
|
||||
latest_build.started_at IS NOT NULL AND
|
||||
latest_build.canceled_at IS NULL AND
|
||||
latest_build.completed_at IS NULL AND
|
||||
latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND
|
||||
latest_build.transition = 'start'::workspace_transition
|
||||
|
||||
WHEN $2 = 'running' THEN
|
||||
latest_build.completed_at IS NOT NULL AND
|
||||
latest_build.canceled_at IS NULL AND
|
||||
latest_build.error IS NULL AND
|
||||
latest_build.transition = 'start'::workspace_transition
|
||||
|
||||
WHEN $2 = 'stopping' THEN
|
||||
latest_build.started_at IS NOT NULL AND
|
||||
latest_build.canceled_at IS NULL AND
|
||||
latest_build.completed_at IS NULL AND
|
||||
latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND
|
||||
latest_build.transition = 'stop'::workspace_transition
|
||||
|
||||
WHEN $2 = 'stopped' THEN
|
||||
latest_build.completed_at IS NOT NULL AND
|
||||
latest_build.canceled_at IS NULL AND
|
||||
latest_build.error IS NULL AND
|
||||
latest_build.transition = 'stop'::workspace_transition
|
||||
|
||||
WHEN $2 = 'failed' THEN
|
||||
(latest_build.canceled_at IS NOT NULL AND
|
||||
latest_build.error IS NOT NULL) OR
|
||||
(latest_build.completed_at IS NOT NULL AND
|
||||
latest_build.error IS NOT NULL)
|
||||
|
||||
WHEN $2 = 'canceling' THEN
|
||||
latest_build.canceled_at IS NOT NULL AND
|
||||
latest_build.completed_at IS NULL
|
||||
|
||||
WHEN $2 = 'canceled' THEN
|
||||
latest_build.canceled_at IS NOT NULL AND
|
||||
latest_build.completed_at IS NOT NULL
|
||||
|
||||
WHEN $2 = 'deleted' THEN
|
||||
latest_build.started_at IS NOT NULL AND
|
||||
latest_build.canceled_at IS NULL AND
|
||||
latest_build.completed_at IS NOT NULL AND
|
||||
latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND
|
||||
latest_build.transition = 'delete'::workspace_transition
|
||||
|
||||
WHEN $2 = 'deleting' THEN
|
||||
latest_build.completed_at IS NOT NULL AND
|
||||
latest_build.canceled_at IS NULL AND
|
||||
latest_build.error IS NULL AND
|
||||
latest_build.transition = 'delete'::workspace_transition
|
||||
|
||||
ELSE
|
||||
true
|
||||
END
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by owner_id
|
||||
AND CASE
|
||||
WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
|
||||
owner_id = $3
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by owner_name
|
||||
AND CASE
|
||||
WHEN $4 :: text != '' THEN
|
||||
owner_id = (SELECT id FROM users WHERE lower(username) = lower($4) AND deleted = false)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by template_name
|
||||
-- There can be more than 1 template with the same name across organizations.
|
||||
-- Use the organization filter to restrict to 1 org if needed.
|
||||
AND CASE
|
||||
WHEN $5 :: text != '' THEN
|
||||
template_id = ANY(SELECT id FROM templates WHERE lower(name) = lower($5) AND deleted = false)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by template_ids
|
||||
AND CASE
|
||||
WHEN array_length($6 :: uuid[], 1) > 0 THEN
|
||||
template_id = ANY($6)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by name, matching on substring
|
||||
AND CASE
|
||||
WHEN $7 :: text != '' THEN
|
||||
name ILIKE '%' || $7 || '%'
|
||||
ELSE true
|
||||
END
|
||||
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaceCount
|
||||
-- @authorize_filter
|
||||
`
|
||||
|
||||
type GetWorkspaceCountParams struct {
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
Status string `db:"status" json:"status"`
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
OwnerUsername string `db:"owner_username" json:"owner_username"`
|
||||
TemplateName string `db:"template_name" json:"template_name"`
|
||||
TemplateIds []uuid.UUID `db:"template_ids" json:"template_ids"`
|
||||
Name string `db:"name" json:"name"`
|
||||
}
|
||||
|
||||
// this duplicates the filtering in GetWorkspaces
|
||||
func (q *sqlQuerier) GetWorkspaceCount(ctx context.Context, arg GetWorkspaceCountParams) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, getWorkspaceCount,
|
||||
arg.Deleted,
|
||||
arg.Status,
|
||||
arg.OwnerID,
|
||||
arg.OwnerUsername,
|
||||
arg.TemplateName,
|
||||
pq.Array(arg.TemplateIds),
|
||||
arg.Name,
|
||||
)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const getWorkspaceCountByUserID = `-- name: GetWorkspaceCountByUserID :one
|
||||
SELECT
|
||||
COUNT(id)
|
||||
|
@ -145,135 +145,6 @@ OFFSET
|
||||
@offset_
|
||||
;
|
||||
|
||||
-- this duplicates the filtering in GetWorkspaces
|
||||
-- name: GetWorkspaceCount :one
|
||||
SELECT
|
||||
COUNT(*) as count
|
||||
FROM
|
||||
workspaces
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
workspace_builds.transition,
|
||||
provisioner_jobs.started_at,
|
||||
provisioner_jobs.updated_at,
|
||||
provisioner_jobs.canceled_at,
|
||||
provisioner_jobs.completed_at,
|
||||
provisioner_jobs.error
|
||||
FROM
|
||||
workspace_builds
|
||||
LEFT JOIN
|
||||
provisioner_jobs
|
||||
ON
|
||||
provisioner_jobs.id = workspace_builds.job_id
|
||||
WHERE
|
||||
workspace_builds.workspace_id = workspaces.id
|
||||
ORDER BY
|
||||
build_number DESC
|
||||
LIMIT
|
||||
1
|
||||
) latest_build ON TRUE
|
||||
WHERE
|
||||
-- Optionally include deleted workspaces
|
||||
workspaces.deleted = @deleted
|
||||
AND CASE
|
||||
WHEN @status :: text != '' THEN
|
||||
CASE
|
||||
WHEN @status = 'pending' THEN
|
||||
latest_build.started_at IS NULL
|
||||
WHEN @status = 'starting' THEN
|
||||
latest_build.started_at IS NOT NULL AND
|
||||
latest_build.canceled_at IS NULL AND
|
||||
latest_build.completed_at IS NULL AND
|
||||
latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND
|
||||
latest_build.transition = 'start'::workspace_transition
|
||||
|
||||
WHEN @status = 'running' THEN
|
||||
latest_build.completed_at IS NOT NULL AND
|
||||
latest_build.canceled_at IS NULL AND
|
||||
latest_build.error IS NULL AND
|
||||
latest_build.transition = 'start'::workspace_transition
|
||||
|
||||
WHEN @status = 'stopping' THEN
|
||||
latest_build.started_at IS NOT NULL AND
|
||||
latest_build.canceled_at IS NULL AND
|
||||
latest_build.completed_at IS NULL AND
|
||||
latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND
|
||||
latest_build.transition = 'stop'::workspace_transition
|
||||
|
||||
WHEN @status = 'stopped' THEN
|
||||
latest_build.completed_at IS NOT NULL AND
|
||||
latest_build.canceled_at IS NULL AND
|
||||
latest_build.error IS NULL AND
|
||||
latest_build.transition = 'stop'::workspace_transition
|
||||
|
||||
WHEN @status = 'failed' THEN
|
||||
(latest_build.canceled_at IS NOT NULL AND
|
||||
latest_build.error IS NOT NULL) OR
|
||||
(latest_build.completed_at IS NOT NULL AND
|
||||
latest_build.error IS NOT NULL)
|
||||
|
||||
WHEN @status = 'canceling' THEN
|
||||
latest_build.canceled_at IS NOT NULL AND
|
||||
latest_build.completed_at IS NULL
|
||||
|
||||
WHEN @status = 'canceled' THEN
|
||||
latest_build.canceled_at IS NOT NULL AND
|
||||
latest_build.completed_at IS NOT NULL
|
||||
|
||||
WHEN @status = 'deleted' THEN
|
||||
latest_build.started_at IS NOT NULL AND
|
||||
latest_build.canceled_at IS NULL AND
|
||||
latest_build.completed_at IS NOT NULL AND
|
||||
latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND
|
||||
latest_build.transition = 'delete'::workspace_transition
|
||||
|
||||
WHEN @status = 'deleting' THEN
|
||||
latest_build.completed_at IS NOT NULL AND
|
||||
latest_build.canceled_at IS NULL AND
|
||||
latest_build.error IS NULL AND
|
||||
latest_build.transition = 'delete'::workspace_transition
|
||||
|
||||
ELSE
|
||||
true
|
||||
END
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by owner_id
|
||||
AND CASE
|
||||
WHEN @owner_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
|
||||
owner_id = @owner_id
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by owner_name
|
||||
AND CASE
|
||||
WHEN @owner_username :: text != '' THEN
|
||||
owner_id = (SELECT id FROM users WHERE lower(username) = lower(@owner_username) AND deleted = false)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by template_name
|
||||
-- There can be more than 1 template with the same name across organizations.
|
||||
-- Use the organization filter to restrict to 1 org if needed.
|
||||
AND CASE
|
||||
WHEN @template_name :: text != '' THEN
|
||||
template_id = ANY(SELECT id FROM templates WHERE lower(name) = lower(@template_name) AND deleted = false)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by template_ids
|
||||
AND CASE
|
||||
WHEN array_length(@template_ids :: uuid[], 1) > 0 THEN
|
||||
template_id = ANY(@template_ids)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by name, matching on substring
|
||||
AND CASE
|
||||
WHEN @name :: text != '' THEN
|
||||
name ILIKE '%' || @name || '%'
|
||||
ELSE true
|
||||
END
|
||||
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaceCount
|
||||
-- @authorize_filter
|
||||
;
|
||||
|
||||
-- name: GetWorkspaceByOwnerIDAndName :one
|
||||
SELECT
|
||||
*
|
||||
|
@ -564,9 +564,9 @@ func TestTemplateMetrics(t *testing.T) {
|
||||
Entries: []codersdk.DAUEntry{},
|
||||
}, daus, "no DAUs when stats are empty")
|
||||
|
||||
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
||||
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
||||
require.NoError(t, err)
|
||||
assert.Zero(t, workspaces[0].LastUsedAt)
|
||||
assert.Zero(t, res.Workspaces[0].LastUsedAt)
|
||||
|
||||
conn, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, &codersdk.DialWorkspaceAgentOptions{
|
||||
Logger: slogtest.Make(t, nil).Named("tailnet"),
|
||||
@ -615,9 +615,9 @@ func TestTemplateMetrics(t *testing.T) {
|
||||
"BuildTimeStats never loaded",
|
||||
)
|
||||
|
||||
workspaces, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
||||
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
||||
require.NoError(t, err)
|
||||
assert.WithinDuration(t,
|
||||
database.Now(), workspaces[0].LastUsedAt, time.Minute,
|
||||
database.Now(), res.Workspaces[0].LastUsedAt, time.Minute,
|
||||
)
|
||||
}
|
||||
|
@ -1331,11 +1331,11 @@ func TestWorkspacesByUser(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
Owner: codersdk.Me,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workspaces, 0)
|
||||
require.Len(t, res.Workspaces, 0)
|
||||
})
|
||||
t.Run("Access", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@ -1365,13 +1365,13 @@ 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.Workspaces(ctx, codersdk.WorkspaceFilter{Owner: codersdk.Me})
|
||||
res, err := newUserClient.Workspaces(ctx, codersdk.WorkspaceFilter{Owner: codersdk.Me})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workspaces, 0)
|
||||
require.Len(t, res.Workspaces, 0)
|
||||
|
||||
workspaces, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{Owner: codersdk.Me})
|
||||
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{Owner: codersdk.Me})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workspaces, 1)
|
||||
require.Len(t, res.Workspaces, 1)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -629,11 +629,11 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) {
|
||||
me, err := client.User(ctx, codersdk.Me)
|
||||
require.NoError(t, err, "get current user details")
|
||||
|
||||
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
Owner: codersdk.Me,
|
||||
})
|
||||
require.NoError(t, err, "get workspaces")
|
||||
require.Len(t, workspaces, 1, "expected 1 workspace")
|
||||
require.Len(t, res.Workspaces, 1, "expected 1 workspace")
|
||||
|
||||
appHost, err := client.GetAppHost(ctx)
|
||||
require.NoError(t, err, "get app host")
|
||||
@ -642,7 +642,7 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) {
|
||||
AppSlug: appName,
|
||||
Port: port,
|
||||
AgentName: proxyTestAgentName,
|
||||
WorkspaceName: workspaces[0].Name,
|
||||
WorkspaceName: res.Workspaces[0].Name,
|
||||
Username: me.Username,
|
||||
}.String()
|
||||
|
||||
|
@ -136,6 +136,19 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// run the query again to get the total count for frontend pagination
|
||||
filter.Offset = 0
|
||||
filter.Limit = 0
|
||||
all, err := api.Database.GetAuthorizedWorkspaces(ctx, filter, sqlFilter)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspaces.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
count := len(all)
|
||||
|
||||
data, err := api.workspaceData(ctx, workspaces)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
@ -154,58 +167,9 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, wss)
|
||||
}
|
||||
|
||||
func (api *API) workspaceCount(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
apiKey := httpmw.APIKey(r)
|
||||
|
||||
queryStr := r.URL.Query().Get("q")
|
||||
filter, errs := workspaceSearchQuery(queryStr, codersdk.Pagination{})
|
||||
if len(errs) > 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid workspace search query.",
|
||||
Validations: errs,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if filter.OwnerUsername == "me" {
|
||||
filter.OwnerID = apiKey.UserID
|
||||
filter.OwnerUsername = ""
|
||||
}
|
||||
|
||||
sqlFilter, err := api.HTTPAuth.AuthorizeSQLFilter(r, rbac.ActionRead, rbac.ResourceWorkspace.Type)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error preparing sql filter.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
countFilter := database.GetWorkspaceCountParams{
|
||||
Deleted: filter.Deleted,
|
||||
OwnerUsername: filter.OwnerUsername,
|
||||
OwnerID: filter.OwnerID,
|
||||
Name: filter.Name,
|
||||
Status: filter.Status,
|
||||
TemplateIds: filter.TemplateIds,
|
||||
TemplateName: filter.TemplateName,
|
||||
}
|
||||
|
||||
count, err := api.Database.GetAuthorizedWorkspaceCount(ctx, countFilter, sqlFilter)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspace count.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceCountResponse{
|
||||
Count: count,
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspacesResponse{
|
||||
Workspaces: wss,
|
||||
Count: count,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -147,7 +147,7 @@ func TestAdminViewAllWorkspaces(t *testing.T) {
|
||||
firstWorkspaces, err := other.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
||||
require.NoError(t, err, "(first) fetch workspaces")
|
||||
|
||||
require.ElementsMatch(t, otherWorkspaces, firstWorkspaces)
|
||||
require.ElementsMatch(t, otherWorkspaces.Workspaces, firstWorkspaces.Workspaces)
|
||||
}
|
||||
|
||||
func TestPostWorkspacesByOrganization(t *testing.T) {
|
||||
@ -646,27 +646,27 @@ func TestWorkspaceFilterManual(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
// full match
|
||||
ws, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
Name: workspace.Name,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ws, 1, workspace.Name)
|
||||
require.Equal(t, workspace.ID, ws[0].ID)
|
||||
require.Len(t, res.Workspaces, 1, workspace.Name)
|
||||
require.Equal(t, workspace.ID, res.Workspaces[0].ID)
|
||||
|
||||
// partial match
|
||||
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
Name: workspace.Name[1 : len(workspace.Name)-2],
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ws, 1)
|
||||
require.Equal(t, workspace.ID, ws[0].ID)
|
||||
require.Len(t, res.Workspaces, 1)
|
||||
require.Equal(t, workspace.ID, res.Workspaces[0].ID)
|
||||
|
||||
// no match
|
||||
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
Name: "$$$$",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ws, 0)
|
||||
require.Len(t, res.Workspaces, 0)
|
||||
})
|
||||
t.Run("Template", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@ -683,17 +683,17 @@ func TestWorkspaceFilterManual(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
// empty
|
||||
ws, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
||||
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ws, 2)
|
||||
require.Len(t, res.Workspaces, 2)
|
||||
|
||||
// single template
|
||||
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
Template: template.Name,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ws, 1)
|
||||
require.Equal(t, workspace.ID, ws[0].ID)
|
||||
require.Len(t, res.Workspaces, 1)
|
||||
require.Equal(t, workspace.ID, res.Workspaces[0].ID)
|
||||
})
|
||||
t.Run("Status", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@ -716,7 +716,7 @@ func TestWorkspaceFilterManual(t *testing.T) {
|
||||
// filter finds both running workspaces
|
||||
ws1, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ws1, 2)
|
||||
require.Len(t, ws1.Workspaces, 2)
|
||||
|
||||
// stop workspace1
|
||||
build1 := coderdtest.CreateWorkspaceBuild(t, client, workspace1, database.WorkspaceTransitionStop)
|
||||
@ -727,8 +727,8 @@ func TestWorkspaceFilterManual(t *testing.T) {
|
||||
Status: "running",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ws2, 1)
|
||||
require.Equal(t, workspace2.ID, ws2[0].ID)
|
||||
require.Len(t, ws2.Workspaces, 1)
|
||||
require.Equal(t, workspace2.ID, ws2.Workspaces[0].ID)
|
||||
|
||||
// stop workspace2
|
||||
build2 := coderdtest.CreateWorkspaceBuild(t, client, workspace2, database.WorkspaceTransitionStop)
|
||||
@ -739,7 +739,7 @@ func TestWorkspaceFilterManual(t *testing.T) {
|
||||
Status: "running",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ws3, 0)
|
||||
require.Len(t, ws3.Workspaces, 0)
|
||||
})
|
||||
t.Run("FilterQuery", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@ -756,12 +756,12 @@ func TestWorkspaceFilterManual(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
// single workspace
|
||||
ws, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
FilterQuery: fmt.Sprintf("template:%s %s/%s", template.Name, workspace.OwnerName, workspace.Name),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ws, 1)
|
||||
require.Equal(t, workspace.ID, ws[0].ID)
|
||||
require.Len(t, res.Workspaces, 1)
|
||||
require.Equal(t, workspace.ID, res.Workspaces[0].ID)
|
||||
})
|
||||
}
|
||||
|
||||
@ -781,14 +781,14 @@ func TestOffsetLimit(t *testing.T) {
|
||||
// empty finds all workspaces
|
||||
ws, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ws, 3)
|
||||
require.Len(t, ws.Workspaces, 3)
|
||||
|
||||
// offset 1 finds 2 workspaces
|
||||
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
Offset: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ws, 2)
|
||||
require.Len(t, ws.Workspaces, 2)
|
||||
|
||||
// offset 1 limit 1 finds 1 workspace
|
||||
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
@ -796,41 +796,14 @@ func TestOffsetLimit(t *testing.T) {
|
||||
Limit: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ws, 1)
|
||||
require.Len(t, ws.Workspaces, 1)
|
||||
|
||||
// offset 3 finds no workspaces
|
||||
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
Offset: 3,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ws, 0)
|
||||
}
|
||||
|
||||
func TestWorkspaceCount(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: 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)
|
||||
template2 := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template2.ID)
|
||||
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template2.ID)
|
||||
|
||||
response, err := client.WorkspaceCount(ctx, codersdk.WorkspaceCountRequest{})
|
||||
require.NoError(t, err, "fetch workspace count")
|
||||
// counts all
|
||||
require.Equal(t, int(response.Count), 3)
|
||||
|
||||
response2, err2 := client.WorkspaceCount(ctx, codersdk.WorkspaceCountRequest{
|
||||
SearchQuery: fmt.Sprintf("template:%s", template.Name),
|
||||
})
|
||||
require.NoError(t, err2, "fetch workspace count")
|
||||
// counts only those that pass filter
|
||||
require.Equal(t, int(response2.Count), 1)
|
||||
require.Len(t, ws.Workspaces, 0)
|
||||
}
|
||||
|
||||
func TestPostWorkspaceBuild(t *testing.T) {
|
||||
@ -974,11 +947,11 @@ func TestPostWorkspaceBuild(t *testing.T) {
|
||||
require.Equal(t, workspace.LatestBuild.BuildNumber+1, build.BuildNumber)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
|
||||
|
||||
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
Owner: user.UserID.String(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workspaces, 0)
|
||||
require.Len(t, res.Workspaces, 0)
|
||||
})
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user