mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: add db query to retrieve workspaces & their agents (#14792)
Second PR for #14716. Adds a query that, given a user ID, returns all the workspaces they own, that can also be `ActionRead` by the requesting user. ``` type GetWorkspacesAndAgentsByOwnerIDRow struct { WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` WorkspaceName string `db:"workspace_name" json:"workspace_name"` JobStatus ProvisionerJobStatus `db:"job_status" json:"job_status"` Transition WorkspaceTransition `db:"transition" json:"transition"` Agents []AgentIDNamePair `db:"agents" json:"agents"` } ``` `JobStatus` and `Transition` are set using the latest build/job of the workspace. Deleted workspaces are not included.
This commit is contained in:
+210
-42
@@ -24,7 +24,9 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/database/migrations"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
@@ -612,6 +614,130 @@ func TestGetWorkspaceAgentUsageStatsAndLabels(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAuthorizedWorkspacesAndAgentsByOwnerID(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
sqlDB := testSQLDB(t)
|
||||
err := migrations.Up(sqlDB)
|
||||
require.NoError(t, err)
|
||||
db := database.New(sqlDB)
|
||||
authorizer := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
|
||||
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
owner := dbgen.User(t, db, database.User{
|
||||
RBACRoles: []string{rbac.RoleOwner().String()},
|
||||
})
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
tpl := dbgen.Template(t, db, database.Template{
|
||||
OrganizationID: org.ID,
|
||||
CreatedBy: owner.ID,
|
||||
})
|
||||
|
||||
pendingID := uuid.New()
|
||||
createTemplateVersion(t, db, tpl, tvArgs{
|
||||
Status: database.ProvisionerJobStatusPending,
|
||||
CreateWorkspace: true,
|
||||
WorkspaceID: pendingID,
|
||||
CreateAgent: true,
|
||||
})
|
||||
failedID := uuid.New()
|
||||
createTemplateVersion(t, db, tpl, tvArgs{
|
||||
Status: database.ProvisionerJobStatusFailed,
|
||||
CreateWorkspace: true,
|
||||
CreateAgent: true,
|
||||
WorkspaceID: failedID,
|
||||
})
|
||||
succeededID := uuid.New()
|
||||
createTemplateVersion(t, db, tpl, tvArgs{
|
||||
Status: database.ProvisionerJobStatusSucceeded,
|
||||
WorkspaceTransition: database.WorkspaceTransitionStart,
|
||||
CreateWorkspace: true,
|
||||
WorkspaceID: succeededID,
|
||||
CreateAgent: true,
|
||||
ExtraAgents: 1,
|
||||
ExtraBuilds: 2,
|
||||
})
|
||||
deletedID := uuid.New()
|
||||
createTemplateVersion(t, db, tpl, tvArgs{
|
||||
Status: database.ProvisionerJobStatusSucceeded,
|
||||
WorkspaceTransition: database.WorkspaceTransitionDelete,
|
||||
CreateWorkspace: true,
|
||||
WorkspaceID: deletedID,
|
||||
CreateAgent: false,
|
||||
})
|
||||
|
||||
ownerCheckFn := func(ownerRows []database.GetWorkspacesAndAgentsByOwnerIDRow) {
|
||||
require.Len(t, ownerRows, 4)
|
||||
for _, row := range ownerRows {
|
||||
switch row.ID {
|
||||
case pendingID:
|
||||
require.Len(t, row.Agents, 1)
|
||||
require.Equal(t, database.ProvisionerJobStatusPending, row.JobStatus)
|
||||
case failedID:
|
||||
require.Len(t, row.Agents, 1)
|
||||
require.Equal(t, database.ProvisionerJobStatusFailed, row.JobStatus)
|
||||
case succeededID:
|
||||
require.Len(t, row.Agents, 2)
|
||||
require.Equal(t, database.ProvisionerJobStatusSucceeded, row.JobStatus)
|
||||
require.Equal(t, database.WorkspaceTransitionStart, row.Transition)
|
||||
case deletedID:
|
||||
require.Len(t, row.Agents, 0)
|
||||
require.Equal(t, database.ProvisionerJobStatusSucceeded, row.JobStatus)
|
||||
require.Equal(t, database.WorkspaceTransitionDelete, row.Transition)
|
||||
default:
|
||||
t.Fatalf("unexpected workspace ID: %s", row.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Run("sqlQuerier", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
userSubject, _, err := httpmw.UserRBACSubject(ctx, db, user.ID, rbac.ExpandableScope(rbac.ScopeAll))
|
||||
require.NoError(t, err)
|
||||
preparedUser, err := authorizer.Prepare(ctx, userSubject, policy.ActionRead, rbac.ResourceWorkspace.Type)
|
||||
require.NoError(t, err)
|
||||
userCtx := dbauthz.As(ctx, userSubject)
|
||||
userRows, err := db.GetAuthorizedWorkspacesAndAgentsByOwnerID(userCtx, owner.ID, preparedUser)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, userRows, 0)
|
||||
|
||||
ownerSubject, _, err := httpmw.UserRBACSubject(ctx, db, owner.ID, rbac.ExpandableScope(rbac.ScopeAll))
|
||||
require.NoError(t, err)
|
||||
preparedOwner, err := authorizer.Prepare(ctx, ownerSubject, policy.ActionRead, rbac.ResourceWorkspace.Type)
|
||||
require.NoError(t, err)
|
||||
ownerCtx := dbauthz.As(ctx, ownerSubject)
|
||||
ownerRows, err := db.GetAuthorizedWorkspacesAndAgentsByOwnerID(ownerCtx, owner.ID, preparedOwner)
|
||||
require.NoError(t, err)
|
||||
ownerCheckFn(ownerRows)
|
||||
})
|
||||
|
||||
t.Run("dbauthz", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
authzdb := dbauthz.New(db, authorizer, slogtest.Make(t, &slogtest.Options{}), coderdtest.AccessControlStorePointer())
|
||||
|
||||
userSubject, _, err := httpmw.UserRBACSubject(ctx, authzdb, user.ID, rbac.ExpandableScope(rbac.ScopeAll))
|
||||
require.NoError(t, err)
|
||||
userCtx := dbauthz.As(ctx, userSubject)
|
||||
|
||||
ownerSubject, _, err := httpmw.UserRBACSubject(ctx, authzdb, owner.ID, rbac.ExpandableScope(rbac.ScopeAll))
|
||||
require.NoError(t, err)
|
||||
ownerCtx := dbauthz.As(ctx, ownerSubject)
|
||||
|
||||
userRows, err := authzdb.GetWorkspacesAndAgentsByOwnerID(userCtx, owner.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, userRows, 0)
|
||||
|
||||
ownerRows, err := authzdb.GetWorkspacesAndAgentsByOwnerID(ownerCtx, owner.ID)
|
||||
require.NoError(t, err)
|
||||
ownerCheckFn(ownerRows)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInsertWorkspaceAgentLogs(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
@@ -1537,7 +1663,11 @@ type tvArgs struct {
|
||||
Status database.ProvisionerJobStatus
|
||||
// CreateWorkspace is true if we should create a workspace for the template version
|
||||
CreateWorkspace bool
|
||||
WorkspaceID uuid.UUID
|
||||
CreateAgent bool
|
||||
WorkspaceTransition database.WorkspaceTransition
|
||||
ExtraAgents int
|
||||
ExtraBuilds int
|
||||
}
|
||||
|
||||
// createTemplateVersion is a helper function to create a version with its dependencies.
|
||||
@@ -1554,6 +1684,84 @@ func createTemplateVersion(t testing.TB, db database.Store, tpl database.Templat
|
||||
CreatedBy: tpl.CreatedBy,
|
||||
})
|
||||
|
||||
latestJob := database.ProvisionerJob{
|
||||
ID: version.JobID,
|
||||
Error: sql.NullString{},
|
||||
OrganizationID: tpl.OrganizationID,
|
||||
InitiatorID: tpl.CreatedBy,
|
||||
Type: database.ProvisionerJobTypeTemplateVersionImport,
|
||||
}
|
||||
setJobStatus(t, args.Status, &latestJob)
|
||||
dbgen.ProvisionerJob(t, db, nil, latestJob)
|
||||
if args.CreateWorkspace {
|
||||
wrk := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||||
ID: args.WorkspaceID,
|
||||
CreatedAt: time.Time{},
|
||||
UpdatedAt: time.Time{},
|
||||
OwnerID: tpl.CreatedBy,
|
||||
OrganizationID: tpl.OrganizationID,
|
||||
TemplateID: tpl.ID,
|
||||
})
|
||||
trans := database.WorkspaceTransitionStart
|
||||
if args.WorkspaceTransition != "" {
|
||||
trans = args.WorkspaceTransition
|
||||
}
|
||||
latestJob = database.ProvisionerJob{
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
InitiatorID: tpl.CreatedBy,
|
||||
OrganizationID: tpl.OrganizationID,
|
||||
}
|
||||
setJobStatus(t, args.Status, &latestJob)
|
||||
latestJob = dbgen.ProvisionerJob(t, db, nil, latestJob)
|
||||
latestResource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
||||
JobID: latestJob.ID,
|
||||
})
|
||||
dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
||||
WorkspaceID: wrk.ID,
|
||||
TemplateVersionID: version.ID,
|
||||
BuildNumber: 1,
|
||||
Transition: trans,
|
||||
InitiatorID: tpl.CreatedBy,
|
||||
JobID: latestJob.ID,
|
||||
})
|
||||
for i := 0; i < args.ExtraBuilds; i++ {
|
||||
latestJob = database.ProvisionerJob{
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
InitiatorID: tpl.CreatedBy,
|
||||
OrganizationID: tpl.OrganizationID,
|
||||
}
|
||||
setJobStatus(t, args.Status, &latestJob)
|
||||
latestJob = dbgen.ProvisionerJob(t, db, nil, latestJob)
|
||||
latestResource = dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
||||
JobID: latestJob.ID,
|
||||
})
|
||||
dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
||||
WorkspaceID: wrk.ID,
|
||||
TemplateVersionID: version.ID,
|
||||
BuildNumber: int32(i) + 2,
|
||||
Transition: trans,
|
||||
InitiatorID: tpl.CreatedBy,
|
||||
JobID: latestJob.ID,
|
||||
})
|
||||
}
|
||||
|
||||
if args.CreateAgent {
|
||||
dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ResourceID: latestResource.ID,
|
||||
})
|
||||
}
|
||||
for i := 0; i < args.ExtraAgents; i++ {
|
||||
dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ResourceID: latestResource.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
func setJobStatus(t testing.TB, status database.ProvisionerJobStatus, j *database.ProvisionerJob) {
|
||||
t.Helper()
|
||||
|
||||
earlier := sql.NullTime{
|
||||
Time: dbtime.Now().Add(time.Second * -30),
|
||||
Valid: true,
|
||||
@@ -1562,17 +1770,7 @@ func createTemplateVersion(t testing.TB, db database.Store, tpl database.Templat
|
||||
Time: dbtime.Now(),
|
||||
Valid: true,
|
||||
}
|
||||
j := database.ProvisionerJob{
|
||||
ID: version.JobID,
|
||||
CreatedAt: earlier.Time,
|
||||
UpdatedAt: earlier.Time,
|
||||
Error: sql.NullString{},
|
||||
OrganizationID: tpl.OrganizationID,
|
||||
InitiatorID: tpl.CreatedBy,
|
||||
Type: database.ProvisionerJobTypeTemplateVersionImport,
|
||||
}
|
||||
|
||||
switch args.Status {
|
||||
switch status {
|
||||
case database.ProvisionerJobStatusRunning:
|
||||
j.StartedAt = earlier
|
||||
case database.ProvisionerJobStatusPending:
|
||||
@@ -1591,38 +1789,8 @@ func createTemplateVersion(t testing.TB, db database.Store, tpl database.Templat
|
||||
j.StartedAt = earlier
|
||||
j.CompletedAt = now
|
||||
default:
|
||||
t.Fatalf("invalid status: %s", args.Status)
|
||||
t.Fatalf("invalid status: %s", status)
|
||||
}
|
||||
|
||||
dbgen.ProvisionerJob(t, db, nil, j)
|
||||
if args.CreateWorkspace {
|
||||
wrk := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||||
CreatedAt: time.Time{},
|
||||
UpdatedAt: time.Time{},
|
||||
OwnerID: tpl.CreatedBy,
|
||||
OrganizationID: tpl.OrganizationID,
|
||||
TemplateID: tpl.ID,
|
||||
})
|
||||
trans := database.WorkspaceTransitionStart
|
||||
if args.WorkspaceTransition != "" {
|
||||
trans = args.WorkspaceTransition
|
||||
}
|
||||
buildJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
CompletedAt: now,
|
||||
InitiatorID: tpl.CreatedBy,
|
||||
OrganizationID: tpl.OrganizationID,
|
||||
})
|
||||
dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
||||
WorkspaceID: wrk.ID,
|
||||
TemplateVersionID: version.ID,
|
||||
BuildNumber: 1,
|
||||
Transition: trans,
|
||||
InitiatorID: tpl.CreatedBy,
|
||||
JobID: buildJob.ID,
|
||||
})
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
func TestArchiveVersions(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user