mirror of
https://github.com/coder/coder.git
synced 2026-10-31 11:02:50 +08:00
Follow-up to #28302 ([GRU-82](https://linear.app/codercom/issue/GRU-82), [RFC](https://app.notion.com/p/coderhq/Lite-Workspace-API-Requests-3aad579be5928060b5e7cc65c539717f)). That PR added related-data selection but noted that selecting `latest_build.job` still ran the expensive `GetProvisionerJobsByIDsWithQueuePosition` query, which computes queue position and size with window functions over pending jobs and provisioner daemons and is consistently the top resource consumer in scale tests. This PR adds a cheaper `GetProvisionerJobsByIDs` query that fetches jobs by ID without the queue-position computation, and selects between the two based on whether `latest_build.job.queue_position` is requested. When the queue position is not selected, `QueuePosition` and `QueueSize` are left zero and the rest of the response is unchanged. Both paths go through a single `provisionerJobsByIDs` API method that takes the `jobRelated` selection, switches on it, and reshapes the cheaper query's rows into `GetProvisionerJobsByIDsWithQueuePositionRow` so downstream conversion is uniform. The new query has the same dbauthz properties as the queue-position variant (system-level pass-through pending the proper provisioner-job RBAC check tracked in #16160), with a matching authorization test. No behavior change for existing callers: they select `queue_position` (via `allLatestBuildRelated`), so they continue to use the queue-position query. --- *Generated by Coder Agents on behalf of @spikecurtis.*
306 lines
11 KiB
Go
306 lines
11 KiB
Go
package coderd
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/mock/gomock"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbmock"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
// TestAllWorkspaceRelated verifies that allWorkspaceRelated selects every node
|
|
// in the related-data hierarchy.
|
|
func TestAllWorkspaceRelated(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
all := allWorkspaceRelated()
|
|
require.True(t, all.Template)
|
|
require.NotNil(t, all.LatestBuild)
|
|
require.NotNil(t, all.LatestBuild.Job)
|
|
require.True(t, all.LatestBuild.Job.QueuePosition)
|
|
require.True(t, all.LatestBuild.TemplateVersion)
|
|
require.NotNil(t, all.LatestBuild.Resources)
|
|
require.True(t, all.LatestBuild.Resources.Metadata)
|
|
require.NotNil(t, all.LatestBuild.Resources.Agents)
|
|
require.NotNil(t, all.LatestBuild.Resources.Agents.Apps)
|
|
require.True(t, all.LatestBuild.Resources.Agents.Apps.Statuses)
|
|
require.True(t, all.LatestBuild.Resources.Agents.Scripts)
|
|
require.True(t, all.LatestBuild.Resources.Agents.LogSources)
|
|
require.True(t, all.LatestBuild.appStatuses())
|
|
}
|
|
|
|
// TestLatestBuildRelatedAppStatuses verifies the nil-safe appStatuses accessor
|
|
// only reports true when the full apps.statuses path is present.
|
|
func TestLatestBuildRelatedAppStatuses(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
require.False(t, (*latestBuildRelated)(nil).appStatuses())
|
|
require.False(t, (&latestBuildRelated{}).appStatuses())
|
|
require.False(t, (&latestBuildRelated{Resources: &resourcesRelated{}}).appStatuses())
|
|
require.False(t, (&latestBuildRelated{Resources: &resourcesRelated{Agents: &agentsRelated{}}}).appStatuses())
|
|
require.False(t, (&latestBuildRelated{Resources: &resourcesRelated{Agents: &agentsRelated{Apps: &appsRelated{}}}}).appStatuses())
|
|
require.True(t, (&latestBuildRelated{Resources: &resourcesRelated{Agents: &agentsRelated{Apps: &appsRelated{Statuses: true}}}}).appStatuses())
|
|
}
|
|
|
|
// TestWorkspaceBuildsDataQueryGating asserts that workspaceBuildsData only
|
|
// issues the database queries implied by the selection. gomock is strict, so
|
|
// any query that is not set up here fails the test if invoked.
|
|
func TestWorkspaceBuildsDataQueryGating(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
build := database.WorkspaceBuild{
|
|
ID: uuid.New(),
|
|
JobID: uuid.New(),
|
|
WorkspaceID: uuid.New(),
|
|
TemplateVersionID: uuid.New(),
|
|
}
|
|
resource := database.WorkspaceResource{ID: uuid.New(), JobID: build.JobID}
|
|
agent := database.WorkspaceAgent{ID: uuid.New(), ResourceID: resource.ID}
|
|
app := database.WorkspaceApp{ID: uuid.New(), AgentID: agent.ID}
|
|
|
|
expectJob := func(db *dbmock.MockStore) {
|
|
db.EXPECT().GetProvisionerJobsByIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.ProvisionerJob{}, nil)
|
|
db.EXPECT().GetEligibleProvisionerDaemonsByProvisionerJobIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}, nil)
|
|
}
|
|
expectJobWithQueuePosition := func(db *dbmock.MockStore) {
|
|
db.EXPECT().GetProvisionerJobsByIDsWithQueuePosition(gomock.Any(), gomock.Any()).
|
|
Return([]database.GetProvisionerJobsByIDsWithQueuePositionRow{}, nil)
|
|
db.EXPECT().GetEligibleProvisionerDaemonsByProvisionerJobIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}, nil)
|
|
}
|
|
expectTemplateVersion := func(db *dbmock.MockStore) {
|
|
db.EXPECT().GetTemplateVersionsByIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.TemplateVersion{}, nil)
|
|
}
|
|
expectResources := func(db *dbmock.MockStore) {
|
|
db.EXPECT().GetWorkspaceResourcesByJobIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.WorkspaceResource{resource}, nil)
|
|
}
|
|
expectAgents := func(db *dbmock.MockStore) {
|
|
db.EXPECT().GetWorkspaceAgentsByResourceIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.WorkspaceAgent{agent}, nil)
|
|
}
|
|
expectApps := func(db *dbmock.MockStore) {
|
|
db.EXPECT().GetWorkspaceAppsByAgentIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.WorkspaceApp{app}, nil)
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
cfg latestBuildRelated
|
|
setup func(*dbmock.MockStore)
|
|
}{
|
|
{
|
|
name: "BuildOnly",
|
|
cfg: latestBuildRelated{},
|
|
setup: func(*dbmock.MockStore) {},
|
|
},
|
|
{
|
|
name: "Job",
|
|
cfg: latestBuildRelated{Job: &jobRelated{}},
|
|
setup: expectJob,
|
|
},
|
|
{
|
|
name: "JobWithQueuePosition",
|
|
cfg: latestBuildRelated{Job: &jobRelated{QueuePosition: true}},
|
|
setup: expectJobWithQueuePosition,
|
|
},
|
|
{
|
|
name: "TemplateVersion",
|
|
cfg: latestBuildRelated{TemplateVersion: true},
|
|
setup: expectTemplateVersion,
|
|
},
|
|
{
|
|
name: "Resources",
|
|
cfg: latestBuildRelated{Resources: &resourcesRelated{}},
|
|
setup: expectResources,
|
|
},
|
|
{
|
|
name: "Metadata",
|
|
cfg: latestBuildRelated{Resources: &resourcesRelated{Metadata: true}},
|
|
setup: func(db *dbmock.MockStore) {
|
|
expectResources(db)
|
|
db.EXPECT().GetWorkspaceResourceMetadataByResourceIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.WorkspaceResourceMetadatum{}, nil)
|
|
},
|
|
},
|
|
{
|
|
name: "Agents",
|
|
cfg: latestBuildRelated{Resources: &resourcesRelated{Agents: &agentsRelated{}}},
|
|
setup: func(db *dbmock.MockStore) {
|
|
expectResources(db)
|
|
expectAgents(db)
|
|
},
|
|
},
|
|
{
|
|
name: "Apps",
|
|
cfg: latestBuildRelated{Resources: &resourcesRelated{Agents: &agentsRelated{Apps: &appsRelated{}}}},
|
|
setup: func(db *dbmock.MockStore) {
|
|
expectResources(db)
|
|
expectAgents(db)
|
|
expectApps(db)
|
|
},
|
|
},
|
|
{
|
|
name: "AppStatuses",
|
|
cfg: latestBuildRelated{Resources: &resourcesRelated{Agents: &agentsRelated{Apps: &appsRelated{Statuses: true}}}},
|
|
setup: func(db *dbmock.MockStore) {
|
|
expectResources(db)
|
|
expectAgents(db)
|
|
expectApps(db)
|
|
db.EXPECT().GetWorkspaceAppStatusesByAppIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.WorkspaceAppStatus{}, nil)
|
|
},
|
|
},
|
|
{
|
|
name: "Scripts",
|
|
cfg: latestBuildRelated{Resources: &resourcesRelated{Agents: &agentsRelated{Scripts: true}}},
|
|
setup: func(db *dbmock.MockStore) {
|
|
expectResources(db)
|
|
expectAgents(db)
|
|
db.EXPECT().GetWorkspaceAgentScriptsByAgentIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.GetWorkspaceAgentScriptsByAgentIDsRow{}, nil)
|
|
},
|
|
},
|
|
{
|
|
name: "LogSources",
|
|
cfg: latestBuildRelated{Resources: &resourcesRelated{Agents: &agentsRelated{LogSources: true}}},
|
|
setup: func(db *dbmock.MockStore) {
|
|
expectResources(db)
|
|
expectAgents(db)
|
|
db.EXPECT().GetWorkspaceAgentLogSourcesByAgentIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.WorkspaceAgentLogSource{}, nil)
|
|
},
|
|
},
|
|
{
|
|
name: "All",
|
|
cfg: allLatestBuildRelated(),
|
|
setup: func(db *dbmock.MockStore) {
|
|
expectJobWithQueuePosition(db)
|
|
expectTemplateVersion(db)
|
|
expectResources(db)
|
|
expectAgents(db)
|
|
expectApps(db)
|
|
db.EXPECT().GetWorkspaceResourceMetadataByResourceIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.WorkspaceResourceMetadatum{}, nil)
|
|
db.EXPECT().GetWorkspaceAppStatusesByAppIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.WorkspaceAppStatus{}, nil)
|
|
db.EXPECT().GetWorkspaceAgentScriptsByAgentIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.GetWorkspaceAgentScriptsByAgentIDsRow{}, nil)
|
|
db.EXPECT().GetWorkspaceAgentLogSourcesByAgentIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.WorkspaceAgentLogSource{}, nil)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
ctrl := gomock.NewController(t)
|
|
db := dbmock.NewMockStore(ctrl)
|
|
tc.setup(db)
|
|
|
|
api := &API{Options: &Options{Database: db}}
|
|
_, err := api.workspaceBuildsData(ctx, []database.WorkspaceBuild{build}, tc.cfg)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWorkspaceBuildsDataResourcesShortCircuit verifies that when resources are
|
|
// requested but none exist, no agent-level queries are issued even though the
|
|
// deeper nodes are selected.
|
|
func TestWorkspaceBuildsDataResourcesShortCircuit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
ctrl := gomock.NewController(t)
|
|
db := dbmock.NewMockStore(ctrl)
|
|
|
|
build := database.WorkspaceBuild{ID: uuid.New(), JobID: uuid.New(), TemplateVersionID: uuid.New()}
|
|
// No resources returned: the agent, app, and status queries must be skipped.
|
|
db.EXPECT().GetWorkspaceResourcesByJobIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.WorkspaceResource{}, nil)
|
|
|
|
cfg := latestBuildRelated{Resources: &resourcesRelated{Agents: &agentsRelated{Apps: &appsRelated{Statuses: true}}}}
|
|
api := &API{Options: &Options{Database: db}}
|
|
_, err := api.workspaceBuildsData(ctx, []database.WorkspaceBuild{build}, cfg)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestWorkspaceDataQueryGating asserts that workspaceData gates its
|
|
// workspace-level queries (templates, latest builds, and latest app statuses)
|
|
// on the selection.
|
|
func TestWorkspaceDataQueryGating(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
workspace := database.Workspace{ID: uuid.New(), TemplateID: uuid.New()}
|
|
|
|
cases := []struct {
|
|
name string
|
|
cfg workspaceRelated
|
|
setup func(*dbmock.MockStore)
|
|
}{
|
|
{
|
|
name: "None",
|
|
cfg: workspaceRelated{},
|
|
setup: func(*dbmock.MockStore) {},
|
|
},
|
|
{
|
|
name: "Template",
|
|
cfg: workspaceRelated{Template: true},
|
|
setup: func(db *dbmock.MockStore) {
|
|
db.EXPECT().GetTemplatesWithFilter(gomock.Any(), gomock.Any()).
|
|
Return([]database.Template{}, nil)
|
|
},
|
|
},
|
|
{
|
|
name: "LatestBuild",
|
|
cfg: workspaceRelated{LatestBuild: &latestBuildRelated{}},
|
|
setup: func(db *dbmock.MockStore) {
|
|
// No builds returned, so no build-subtree queries run.
|
|
db.EXPECT().GetLatestWorkspaceBuildsByWorkspaceIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.WorkspaceBuild{}, nil)
|
|
},
|
|
},
|
|
{
|
|
name: "AppStatuses",
|
|
cfg: workspaceRelated{LatestBuild: &latestBuildRelated{
|
|
Resources: &resourcesRelated{Agents: &agentsRelated{Apps: &appsRelated{Statuses: true}}},
|
|
}},
|
|
setup: func(db *dbmock.MockStore) {
|
|
// The workspace-level latest app status query is gated on the
|
|
// apps.statuses node. With no builds, only the workspace-level
|
|
// status, build, and (empty) resource queries run.
|
|
db.EXPECT().GetLatestWorkspaceAppStatusesByWorkspaceIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.WorkspaceAppStatus{}, nil)
|
|
db.EXPECT().GetLatestWorkspaceBuildsByWorkspaceIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.WorkspaceBuild{}, nil)
|
|
db.EXPECT().GetWorkspaceResourcesByJobIDs(gomock.Any(), gomock.Any()).
|
|
Return([]database.WorkspaceResource{}, nil)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
ctrl := gomock.NewController(t)
|
|
db := dbmock.NewMockStore(ctrl)
|
|
tc.setup(db)
|
|
|
|
api := &API{Options: &Options{Database: db}}
|
|
_, err := api.workspaceData(ctx, []database.Workspace{workspace}, tc.cfg)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
}
|