fix: exclude provisioner_state from workspace_build_with_user view (#22159)

The provisioner state for a workspace build was being loaded for every
long-lived agent rpc connection. Since this state can be anywhere from
kilobytes to megabytes this can gradually cause the `coderd` memory
footprint to grow over time. It's also a lot of unnecessary allocations
for every query that fetches a workspace build since only a few callers
ever actually reference the provisioner state.

This PR removes it from the returned workspace build and adds a query to
fetch the provisioner state explicitly.
This commit is contained in:
Jon Ayers
2026-02-23 22:46:17 -06:00
committed by GitHub
parent bf076fb7ee
commit 0a7a3da178
27 changed files with 379 additions and 90 deletions
+4 -4
View File
@@ -33,7 +33,7 @@ func TestStatePull(t *testing.T) {
OrganizationID: owner.OrganizationID,
OwnerID: taUser.ID,
}).
Seed(database.WorkspaceBuild{ProvisionerState: wantState}).
Seed(database.WorkspaceBuild{}).ProvisionerState(wantState).
Do()
statefilePath := filepath.Join(t.TempDir(), "state")
inv, root := clitest.New(t, "state", "pull", r.Workspace.Name, statefilePath)
@@ -54,7 +54,7 @@ func TestStatePull(t *testing.T) {
OrganizationID: owner.OrganizationID,
OwnerID: taUser.ID,
}).
Seed(database.WorkspaceBuild{ProvisionerState: wantState}).
Seed(database.WorkspaceBuild{}).ProvisionerState(wantState).
Do()
inv, root := clitest.New(t, "state", "pull", r.Workspace.Name)
var gotState bytes.Buffer
@@ -74,7 +74,7 @@ func TestStatePull(t *testing.T) {
OrganizationID: owner.OrganizationID,
OwnerID: taUser.ID,
}).
Seed(database.WorkspaceBuild{ProvisionerState: wantState}).
Seed(database.WorkspaceBuild{}).ProvisionerState(wantState).
Do()
inv, root := clitest.New(t, "state", "pull", taUser.Username+"/"+r.Workspace.Name,
"--build", fmt.Sprintf("%d", r.Build.BuildNumber))
@@ -170,7 +170,7 @@ func TestStatePush(t *testing.T) {
OrganizationID: owner.OrganizationID,
OwnerID: taUser.ID,
}).
Seed(database.WorkspaceBuild{ProvisionerState: initialState}).
Seed(database.WorkspaceBuild{}).ProvisionerState(initialState).
Do()
wantState := []byte("updated state")
stateFile, err := os.CreateTemp(t.TempDir(), "")
+39 -1
View File
@@ -668,6 +668,31 @@ var (
}),
Scope: rbac.ScopeAll,
}.WithCachedASTValue()
subjectWorkspaceBuilder = rbac.Subject{
Type: rbac.SubjectTypeWorkspaceBuilder,
FriendlyName: "Workspace Builder",
ID: uuid.Nil.String(),
Roles: rbac.Roles([]rbac.Role{
{
Identifier: rbac.RoleIdentifier{Name: "workspace-builder"},
DisplayName: "Workspace Builder",
Site: rbac.Permissions(map[string][]policy.Action{
// Reading provisioner daemons to check eligibility.
rbac.ResourceProvisionerDaemon.Type: {policy.ActionRead},
// Updating provisioner jobs (e.g. marking prebuild
// jobs complete).
rbac.ResourceProvisionerJobs.Type: {policy.ActionUpdate},
// Reading provisioner state requires template update
// permission.
rbac.ResourceTemplate.Type: {policy.ActionUpdate},
}),
User: []rbac.Permission{},
ByOrgID: map[string]rbac.OrgPermissions{},
},
}),
Scope: rbac.ScopeAll,
}.WithCachedASTValue()
)
// AsProvisionerd returns a context with an actor that has permissions required
@@ -774,6 +799,14 @@ func AsBoundaryUsageTracker(ctx context.Context) context.Context {
return As(ctx, subjectBoundaryUsageTracker)
}
// AsWorkspaceBuilder returns a context with an actor that has permissions
// required for the workspace builder to prepare workspace builds. This
// includes reading provisioner daemons, updating provisioner jobs, and
// reading provisioner state (which requires template update permission).
func AsWorkspaceBuilder(ctx context.Context) context.Context {
return As(ctx, subjectWorkspaceBuilder)
}
var AsRemoveActor = rbac.Subject{
ID: "remove-actor",
}
@@ -2257,7 +2290,7 @@ func (q *querier) GetAuditLogsOffset(ctx context.Context, arg database.GetAuditL
}
func (q *querier) GetAuthenticatedWorkspaceAgentAndBuildByAuthToken(ctx context.Context, authToken uuid.UUID) (database.GetAuthenticatedWorkspaceAgentAndBuildByAuthTokenRow, error) {
// This is a system function
// This is a system function.
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
return database.GetAuthenticatedWorkspaceAgentAndBuildByAuthTokenRow{}, err
}
@@ -3914,6 +3947,11 @@ func (q *querier) GetWorkspaceBuildParametersByBuildIDs(ctx context.Context, wor
return q.db.GetAuthorizedWorkspaceBuildParametersByBuildIDs(ctx, workspaceBuildIDs, prep)
}
func (q *querier) GetWorkspaceBuildProvisionerStateByID(ctx context.Context, buildID uuid.UUID) (database.GetWorkspaceBuildProvisionerStateByIDRow, error) {
// Fetching the provisioner state requires Update permission on the template.
return fetchWithAction(q.log, q.auth, policy.ActionUpdate, q.db.GetWorkspaceBuildProvisionerStateByID)(ctx, buildID)
}
func (q *querier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
return nil, err
+9
View File
@@ -1969,6 +1969,15 @@ func (s *MethodTestSuite) TestWorkspace() {
dbm.EXPECT().GetWorkspaceByID(gomock.Any(), ws.ID).Return(ws, nil).AnyTimes()
check.Args(build.ID).Asserts(ws, policy.ActionRead).Returns(build)
}))
s.Run("GetWorkspaceBuildProvisionerStateByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
row := database.GetWorkspaceBuildProvisionerStateByIDRow{
ProvisionerState: []byte("state"),
TemplateID: uuid.New(),
TemplateOrganizationID: uuid.New(),
}
dbm.EXPECT().GetWorkspaceBuildProvisionerStateByID(gomock.Any(), gomock.Any()).Return(row, nil).AnyTimes()
check.Args(uuid.New()).Asserts(row, policy.ActionUpdate).Returns(row)
}))
s.Run("GetWorkspaceBuildByJobID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
ws := testutil.Fake(s.T(), faker, database.Workspace{})
build := testutil.Fake(s.T(), faker, database.WorkspaceBuild{WorkspaceID: ws.ID})
+19
View File
@@ -67,6 +67,8 @@ type WorkspaceBuildBuilder struct {
jobError string // Error message for failed jobs
jobErrorCode string // Error code for failed jobs
provisionerState []byte
}
// BuilderOption is a functional option for customizing job timestamps
@@ -138,6 +140,15 @@ func (b WorkspaceBuildBuilder) Seed(seed database.WorkspaceBuild) WorkspaceBuild
return b
}
// ProvisionerState sets the provisioner state for the workspace build.
// This is stored separately from the seed because ProvisionerState is
// not part of the WorkspaceBuild view struct.
func (b WorkspaceBuildBuilder) ProvisionerState(state []byte) WorkspaceBuildBuilder {
//nolint: revive // returns modified struct
b.provisionerState = state
return b
}
func (b WorkspaceBuildBuilder) Resource(resource ...*sdkproto.Resource) WorkspaceBuildBuilder {
//nolint: revive // returns modified struct
b.resources = append(b.resources, resource...)
@@ -464,6 +475,14 @@ func (b WorkspaceBuildBuilder) doInTX() WorkspaceResponse {
}
resp.Build = dbgen.WorkspaceBuild(b.t, b.db, b.seed)
if len(b.provisionerState) > 0 {
err = b.db.UpdateWorkspaceBuildProvisionerStateByID(ownerCtx, database.UpdateWorkspaceBuildProvisionerStateByIDParams{
ID: resp.Build.ID,
UpdatedAt: dbtime.Now(),
ProvisionerState: b.provisionerState,
})
require.NoError(b.t, err, "update provisioner state")
}
b.logger.Debug(context.Background(), "created workspace build",
slog.F("build_id", resp.Build.ID),
slog.F("workspace_id", resp.Workspace.ID),
+1 -1
View File
@@ -504,7 +504,7 @@ func WorkspaceBuild(t testing.TB, db database.Store, orig database.WorkspaceBuil
Transition: takeFirst(orig.Transition, database.WorkspaceTransitionStart),
InitiatorID: takeFirst(orig.InitiatorID, uuid.New()),
JobID: jobID,
ProvisionerState: takeFirstSlice(orig.ProvisionerState, []byte{}),
ProvisionerState: []byte{},
Deadline: takeFirst(orig.Deadline, dbtime.Now().Add(time.Hour)),
MaxDeadline: takeFirst(orig.MaxDeadline, time.Time{}),
Reason: takeFirst(orig.Reason, database.BuildReasonInitiator),
@@ -2430,6 +2430,14 @@ func (m queryMetricsStore) GetWorkspaceBuildParametersByBuildIDs(ctx context.Con
return r0, r1
}
func (m queryMetricsStore) GetWorkspaceBuildProvisionerStateByID(ctx context.Context, workspaceBuildID uuid.UUID) (database.GetWorkspaceBuildProvisionerStateByIDRow, error) {
start := time.Now()
r0, r1 := m.s.GetWorkspaceBuildProvisionerStateByID(ctx, workspaceBuildID)
m.queryLatencies.WithLabelValues("GetWorkspaceBuildProvisionerStateByID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetWorkspaceBuildProvisionerStateByID").Inc()
return r0, r1
}
func (m queryMetricsStore) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) {
start := time.Now()
r0, r1 := m.s.GetWorkspaceBuildStatsByTemplates(ctx, since)
+15
View File
@@ -4544,6 +4544,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaceBuildParametersByBuildIDs(ctx, work
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceBuildParametersByBuildIDs", reflect.TypeOf((*MockStore)(nil).GetWorkspaceBuildParametersByBuildIDs), ctx, workspaceBuildIds)
}
// GetWorkspaceBuildProvisionerStateByID mocks base method.
func (m *MockStore) GetWorkspaceBuildProvisionerStateByID(ctx context.Context, workspaceBuildID uuid.UUID) (database.GetWorkspaceBuildProvisionerStateByIDRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetWorkspaceBuildProvisionerStateByID", ctx, workspaceBuildID)
ret0, _ := ret[0].(database.GetWorkspaceBuildProvisionerStateByIDRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetWorkspaceBuildProvisionerStateByID indicates an expected call of GetWorkspaceBuildProvisionerStateByID.
func (mr *MockStoreMockRecorder) GetWorkspaceBuildProvisionerStateByID(ctx, workspaceBuildID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceBuildProvisionerStateByID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceBuildProvisionerStateByID), ctx, workspaceBuildID)
}
// GetWorkspaceBuildStatsByTemplates mocks base method.
func (m *MockStore) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) {
m.ctrl.T.Helper()
-1
View File
@@ -2708,7 +2708,6 @@ CREATE VIEW workspace_build_with_user AS
workspace_builds.build_number,
workspace_builds.transition,
workspace_builds.initiator_id,
workspace_builds.provisioner_state,
workspace_builds.job_id,
workspace_builds.deadline,
workspace_builds.reason,
+23 -4
View File
@@ -51,15 +51,34 @@ func TestViewSubsetTemplateVersion(t *testing.T) {
}
}
// TestViewSubsetWorkspaceBuild ensures WorkspaceBuildTable is a subset of WorkspaceBuild
// TestViewSubsetWorkspaceBuild ensures WorkspaceBuildTable is a subset of
// WorkspaceBuild, with the exception of ProvisionerState which is
// intentionally excluded from the workspace_build_with_user view to avoid
// loading the large Terraform state blob on hot paths.
func TestViewSubsetWorkspaceBuild(t *testing.T) {
t.Parallel()
table := reflect.TypeOf(database.WorkspaceBuildTable{})
joined := reflect.TypeOf(database.WorkspaceBuild{})
tableFields := allFields(table)
joinedFields := allFields(joined)
if !assert.Subset(t, fieldNames(joinedFields), fieldNames(tableFields), "table is not subset") {
tableFields := fieldNames(allFields(table))
joinedFields := fieldNames(allFields(joined))
// ProvisionerState is intentionally excluded from the
// workspace_build_with_user view to avoid loading multi-MB Terraform
// state blobs on hot paths. Callers that need it use
// GetWorkspaceBuildProvisionerStateByID instead.
excludedFields := map[string]bool{
"ProvisionerState": true,
}
var filtered []string
for _, name := range tableFields {
if !excludedFields[name] {
filtered = append(filtered, name)
}
}
if !assert.Subset(t, joinedFields, filtered, "table is not subset") {
t.Log("Some fields were added to the WorkspaceBuild Table without updating the 'workspace_build_with_user' view.")
t.Log("See migration 000141_join_users_build_version.up.sql to create the view.")
}
@@ -0,0 +1,31 @@
-- Restore provisioner_state to workspace_build_with_user view.
DROP VIEW workspace_build_with_user;
CREATE VIEW workspace_build_with_user AS
SELECT
workspace_builds.id,
workspace_builds.created_at,
workspace_builds.updated_at,
workspace_builds.workspace_id,
workspace_builds.template_version_id,
workspace_builds.build_number,
workspace_builds.transition,
workspace_builds.initiator_id,
workspace_builds.provisioner_state,
workspace_builds.job_id,
workspace_builds.deadline,
workspace_builds.reason,
workspace_builds.daily_cost,
workspace_builds.max_deadline,
workspace_builds.template_version_preset_id,
workspace_builds.has_ai_task,
workspace_builds.has_external_agent,
COALESCE(visible_users.avatar_url, ''::text) AS initiator_by_avatar_url,
COALESCE(visible_users.username, ''::text) AS initiator_by_username,
COALESCE(visible_users.name, ''::text) AS initiator_by_name
FROM
workspace_builds
LEFT JOIN
visible_users ON workspace_builds.initiator_id = visible_users.id;
COMMENT ON VIEW workspace_build_with_user IS 'Joins in the username + avatar url of the initiated by user.';
@@ -0,0 +1,33 @@
-- Drop and recreate workspace_build_with_user to exclude provisioner_state.
-- This avoids loading the large Terraform state blob (1-5 MB per workspace)
-- on every query that uses this view. The callers that need provisioner_state
-- now fetch it separately via GetWorkspaceBuildProvisionerStateByID.
DROP VIEW workspace_build_with_user;
CREATE VIEW workspace_build_with_user AS
SELECT
workspace_builds.id,
workspace_builds.created_at,
workspace_builds.updated_at,
workspace_builds.workspace_id,
workspace_builds.template_version_id,
workspace_builds.build_number,
workspace_builds.transition,
workspace_builds.initiator_id,
workspace_builds.job_id,
workspace_builds.deadline,
workspace_builds.reason,
workspace_builds.daily_cost,
workspace_builds.max_deadline,
workspace_builds.template_version_preset_id,
workspace_builds.has_ai_task,
workspace_builds.has_external_agent,
COALESCE(visible_users.avatar_url, ''::text) AS initiator_by_avatar_url,
COALESCE(visible_users.username, ''::text) AS initiator_by_username,
COALESCE(visible_users.name, ''::text) AS initiator_by_name
FROM
workspace_builds
LEFT JOIN
visible_users ON workspace_builds.initiator_id = visible_users.id;
COMMENT ON VIEW workspace_build_with_user IS 'Joins in the username + avatar url of the initiated by user.';
+8
View File
@@ -316,6 +316,14 @@ func (t GetFileTemplatesRow) RBACObject() rbac.Object {
WithGroupACL(t.GroupACL)
}
// RBACObject for a workspace build's provisioner state requires Update access of the template.
func (t GetWorkspaceBuildProvisionerStateByIDRow) RBACObject() rbac.Object {
return rbac.ResourceTemplate.WithID(t.TemplateID).
InOrg(t.TemplateOrganizationID).
WithACLUserList(t.UserACL).
WithGroupACL(t.GroupACL)
}
func (t Template) DeepCopy() Template {
cpy := t
cpy.UserACL = maps.Clone(t.UserACL)
-1
View File
@@ -4987,7 +4987,6 @@ type WorkspaceBuild struct {
BuildNumber int32 `db:"build_number" json:"build_number"`
Transition WorkspaceTransition `db:"transition" json:"transition"`
InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"`
ProvisionerState []byte `db:"provisioner_state" json:"provisioner_state"`
JobID uuid.UUID `db:"job_id" json:"job_id"`
Deadline time.Time `db:"deadline" json:"deadline"`
Reason BuildReason `db:"reason" json:"reason"`
+5
View File
@@ -506,6 +506,11 @@ type sqlcQuerier interface {
GetWorkspaceBuildMetricsByResourceID(ctx context.Context, id uuid.UUID) (GetWorkspaceBuildMetricsByResourceIDRow, error)
GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]WorkspaceBuildParameter, error)
GetWorkspaceBuildParametersByBuildIDs(ctx context.Context, workspaceBuildIds []uuid.UUID) ([]WorkspaceBuildParameter, error)
// Fetches the provisioner state of a workspace build, joined through to the
// template so that dbauthz can enforce policy.ActionUpdate on the template.
// Provisioner state contains sensitive Terraform state and should only be
// accessible to template administrators.
GetWorkspaceBuildProvisionerStateByID(ctx context.Context, workspaceBuildID uuid.UUID) (GetWorkspaceBuildProvisionerStateByIDRow, error)
GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]GetWorkspaceBuildStatsByTemplatesRow, error)
GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error)
GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error)
+51 -18
View File
@@ -17955,7 +17955,7 @@ const getAuthenticatedWorkspaceAgentAndBuildByAuthToken = `-- name: GetAuthentic
SELECT
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl,
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted,
workspace_build_with_user.id, workspace_build_with_user.created_at, workspace_build_with_user.updated_at, workspace_build_with_user.workspace_id, workspace_build_with_user.template_version_id, workspace_build_with_user.build_number, workspace_build_with_user.transition, workspace_build_with_user.initiator_id, workspace_build_with_user.provisioner_state, workspace_build_with_user.job_id, workspace_build_with_user.deadline, workspace_build_with_user.reason, workspace_build_with_user.daily_cost, workspace_build_with_user.max_deadline, workspace_build_with_user.template_version_preset_id, workspace_build_with_user.has_ai_task, workspace_build_with_user.has_external_agent, workspace_build_with_user.initiator_by_avatar_url, workspace_build_with_user.initiator_by_username, workspace_build_with_user.initiator_by_name,
workspace_build_with_user.id, workspace_build_with_user.created_at, workspace_build_with_user.updated_at, workspace_build_with_user.workspace_id, workspace_build_with_user.template_version_id, workspace_build_with_user.build_number, workspace_build_with_user.transition, workspace_build_with_user.initiator_id, workspace_build_with_user.job_id, workspace_build_with_user.deadline, workspace_build_with_user.reason, workspace_build_with_user.daily_cost, workspace_build_with_user.max_deadline, workspace_build_with_user.template_version_preset_id, workspace_build_with_user.has_ai_task, workspace_build_with_user.has_external_agent, workspace_build_with_user.initiator_by_avatar_url, workspace_build_with_user.initiator_by_username, workspace_build_with_user.initiator_by_name,
tasks.id AS task_id
FROM
workspace_agents
@@ -18093,7 +18093,6 @@ func (q *sqlQuerier) GetAuthenticatedWorkspaceAgentAndBuildByAuthToken(ctx conte
&i.WorkspaceBuild.BuildNumber,
&i.WorkspaceBuild.Transition,
&i.WorkspaceBuild.InitiatorID,
&i.WorkspaceBuild.ProvisionerState,
&i.WorkspaceBuild.JobID,
&i.WorkspaceBuild.Deadline,
&i.WorkspaceBuild.Reason,
@@ -21007,7 +21006,7 @@ func (q *sqlQuerier) InsertWorkspaceBuildParameters(ctx context.Context, arg Ins
}
const getActiveWorkspaceBuildsByTemplateID = `-- name: GetActiveWorkspaceBuildsByTemplateID :many
SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.template_version_preset_id, wb.has_ai_task, wb.has_external_agent, wb.initiator_by_avatar_url, wb.initiator_by_username, wb.initiator_by_name
SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.template_version_preset_id, wb.has_ai_task, wb.has_external_agent, wb.initiator_by_avatar_url, wb.initiator_by_username, wb.initiator_by_name
FROM (
SELECT
workspace_id, MAX(build_number) as max_build_number
@@ -21055,7 +21054,6 @@ func (q *sqlQuerier) GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, t
&i.BuildNumber,
&i.Transition,
&i.InitiatorID,
&i.ProvisionerState,
&i.JobID,
&i.Deadline,
&i.Reason,
@@ -21163,7 +21161,7 @@ func (q *sqlQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, a
const getLatestWorkspaceBuildByWorkspaceID = `-- name: GetLatestWorkspaceBuildByWorkspaceID :one
SELECT
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
workspace_build_with_user AS workspace_builds
WHERE
@@ -21186,7 +21184,6 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, w
&i.BuildNumber,
&i.Transition,
&i.InitiatorID,
&i.ProvisionerState,
&i.JobID,
&i.Deadline,
&i.Reason,
@@ -21205,7 +21202,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, w
const getLatestWorkspaceBuildsByWorkspaceIDs = `-- name: GetLatestWorkspaceBuildsByWorkspaceIDs :many
SELECT
DISTINCT ON (workspace_id)
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
workspace_build_with_user AS workspace_builds
WHERE
@@ -21232,7 +21229,6 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context,
&i.BuildNumber,
&i.Transition,
&i.InitiatorID,
&i.ProvisionerState,
&i.JobID,
&i.Deadline,
&i.Reason,
@@ -21260,7 +21256,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context,
const getWorkspaceBuildByID = `-- name: GetWorkspaceBuildByID :one
SELECT
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
workspace_build_with_user AS workspace_builds
WHERE
@@ -21281,7 +21277,6 @@ func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (W
&i.BuildNumber,
&i.Transition,
&i.InitiatorID,
&i.ProvisionerState,
&i.JobID,
&i.Deadline,
&i.Reason,
@@ -21299,7 +21294,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (W
const getWorkspaceBuildByJobID = `-- name: GetWorkspaceBuildByJobID :one
SELECT
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
workspace_build_with_user AS workspace_builds
WHERE
@@ -21320,7 +21315,6 @@ func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UU
&i.BuildNumber,
&i.Transition,
&i.InitiatorID,
&i.ProvisionerState,
&i.JobID,
&i.Deadline,
&i.Reason,
@@ -21338,7 +21332,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UU
const getWorkspaceBuildByWorkspaceIDAndBuildNumber = `-- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one
SELECT
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
workspace_build_with_user AS workspace_builds
WHERE
@@ -21363,7 +21357,6 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Co
&i.BuildNumber,
&i.Transition,
&i.InitiatorID,
&i.ProvisionerState,
&i.JobID,
&i.Deadline,
&i.Reason,
@@ -21435,6 +21428,48 @@ func (q *sqlQuerier) GetWorkspaceBuildMetricsByResourceID(ctx context.Context, i
return i, err
}
const getWorkspaceBuildProvisionerStateByID = `-- name: GetWorkspaceBuildProvisionerStateByID :one
SELECT
workspace_builds.provisioner_state,
templates.id AS template_id,
templates.organization_id AS template_organization_id,
templates.user_acl,
templates.group_acl
FROM
workspace_builds
INNER JOIN
workspaces ON workspaces.id = workspace_builds.workspace_id
INNER JOIN
templates ON templates.id = workspaces.template_id
WHERE
workspace_builds.id = $1
`
type GetWorkspaceBuildProvisionerStateByIDRow struct {
ProvisionerState []byte `db:"provisioner_state" json:"provisioner_state"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
TemplateOrganizationID uuid.UUID `db:"template_organization_id" json:"template_organization_id"`
UserACL TemplateACL `db:"user_acl" json:"user_acl"`
GroupACL TemplateACL `db:"group_acl" json:"group_acl"`
}
// Fetches the provisioner state of a workspace build, joined through to the
// template so that dbauthz can enforce policy.ActionUpdate on the template.
// Provisioner state contains sensitive Terraform state and should only be
// accessible to template administrators.
func (q *sqlQuerier) GetWorkspaceBuildProvisionerStateByID(ctx context.Context, workspaceBuildID uuid.UUID) (GetWorkspaceBuildProvisionerStateByIDRow, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceBuildProvisionerStateByID, workspaceBuildID)
var i GetWorkspaceBuildProvisionerStateByIDRow
err := row.Scan(
&i.ProvisionerState,
&i.TemplateID,
&i.TemplateOrganizationID,
&i.UserACL,
&i.GroupACL,
)
return i, err
}
const getWorkspaceBuildStatsByTemplates = `-- name: GetWorkspaceBuildStatsByTemplates :many
SELECT
w.template_id,
@@ -21504,7 +21539,7 @@ func (q *sqlQuerier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, sinc
const getWorkspaceBuildsByWorkspaceID = `-- name: GetWorkspaceBuildsByWorkspaceID :many
SELECT
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
workspace_build_with_user AS workspace_builds
WHERE
@@ -21568,7 +21603,6 @@ func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg Ge
&i.BuildNumber,
&i.Transition,
&i.InitiatorID,
&i.ProvisionerState,
&i.JobID,
&i.Deadline,
&i.Reason,
@@ -21595,7 +21629,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg Ge
}
const getWorkspaceBuildsCreatedAfter = `-- name: GetWorkspaceBuildsCreatedAfter :many
SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user WHERE created_at > $1
SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user WHERE created_at > $1
`
func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) {
@@ -21616,7 +21650,6 @@ func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, created
&i.BuildNumber,
&i.Transition,
&i.InitiatorID,
&i.ProvisionerState,
&i.JobID,
&i.Deadline,
&i.Reason,
@@ -271,3 +271,23 @@ JOIN workspace_resources wr ON wr.job_id = wb.job_id
JOIN workspace_agents wa ON wa.resource_id = wr.id
WHERE wb.job_id = (SELECT job_id FROM workspace_resources WHERE workspace_resources.id = $1)
GROUP BY wb.created_at, wb.transition, t.name, o.name, w.owner_id;
-- name: GetWorkspaceBuildProvisionerStateByID :one
-- Fetches the provisioner state of a workspace build, joined through to the
-- template so that dbauthz can enforce policy.ActionUpdate on the template.
-- Provisioner state contains sensitive Terraform state and should only be
-- accessible to template administrators.
SELECT
workspace_builds.provisioner_state,
templates.id AS template_id,
templates.organization_id AS template_organization_id,
templates.user_acl,
templates.group_acl
FROM
workspace_builds
INNER JOIN
workspaces ON workspaces.id = workspace_builds.workspace_id
INNER JOIN
templates ON templates.id = workspaces.template_id
WHERE
workspace_builds.id = @workspace_build_id;
+11 -3
View File
@@ -348,8 +348,12 @@ func reapJob(ctx context.Context, log slog.Logger, db database.Store, pub pubsub
// Only copy the provisioner state if there's no state in
// the current build.
if len(build.ProvisionerState) == 0 {
// Get the previous build if it exists.
currentStateRow, err := db.GetWorkspaceBuildProvisionerStateByID(ctx, build.ID)
if err != nil {
return xerrors.Errorf("get workspace build provisioner state: %w", err)
}
if len(currentStateRow.ProvisionerState) == 0 {
// Get the previous build's state if it exists.
prevBuild, err := db.GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx, database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams{
WorkspaceID: build.WorkspaceID,
BuildNumber: build.BuildNumber - 1,
@@ -358,10 +362,14 @@ func reapJob(ctx context.Context, log slog.Logger, db database.Store, pub pubsub
return xerrors.Errorf("get previous workspace build: %w", err)
}
if err == nil {
prevStateRow, err := db.GetWorkspaceBuildProvisionerStateByID(ctx, prevBuild.ID)
if err != nil {
return xerrors.Errorf("get previous workspace build provisioner state: %w", err)
}
err = db.UpdateWorkspaceBuildProvisionerStateByID(ctx, database.UpdateWorkspaceBuildProvisionerStateByIDParams{
ID: build.ID,
UpdatedAt: dbtime.Now(),
ProvisionerState: prevBuild.ProvisionerState,
ProvisionerState: prevStateRow.ProvisionerState,
})
if err != nil {
return xerrors.Errorf("update workspace build by id: %w", err)
+29 -22
View File
@@ -126,9 +126,9 @@ func TestDetectorHungWorkspaceBuild(t *testing.T) {
previousBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OrganizationID: org.ID,
OwnerID: user.ID,
}).Pubsub(pubsub).Seed(database.WorkspaceBuild{
ProvisionerState: expectedWorkspaceBuildState,
}).Succeeded(dbfake.WithJobCompletedAt(twentyMinAgo)).
}).Pubsub(pubsub).Seed(database.WorkspaceBuild{}).
ProvisionerState(expectedWorkspaceBuildState).
Succeeded(dbfake.WithJobCompletedAt(twentyMinAgo)).
Do()
// Current build (hung - running job with UpdatedAt > 5 min ago).
@@ -163,7 +163,9 @@ func TestDetectorHungWorkspaceBuild(t *testing.T) {
// Check that the provisioner state was copied.
build, err := db.GetWorkspaceBuildByID(ctx, currentBuild.Build.ID)
require.NoError(t, err)
require.Equal(t, expectedWorkspaceBuildState, build.ProvisionerState)
provisionerStateRow, err := db.GetWorkspaceBuildProvisionerStateByID(ctx, build.ID)
require.NoError(t, err)
require.Equal(t, expectedWorkspaceBuildState, provisionerStateRow.ProvisionerState)
detector.Close()
detector.Wait()
@@ -194,9 +196,9 @@ func TestDetectorHungWorkspaceBuildNoOverrideState(t *testing.T) {
previousBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OrganizationID: org.ID,
OwnerID: user.ID,
}).Pubsub(pubsub).Seed(database.WorkspaceBuild{
ProvisionerState: []byte(`{"dean":"NOT cool","colin":"also NOT cool"}`),
}).Succeeded(dbfake.WithJobCompletedAt(twentyMinAgo)).
}).Pubsub(pubsub).Seed(database.WorkspaceBuild{}).
ProvisionerState([]byte(`{"dean":"NOT cool","colin":"also NOT cool"}`)).
Succeeded(dbfake.WithJobCompletedAt(twentyMinAgo)).
Do()
// Current build (hung - running job with UpdatedAt > 5 min ago).
@@ -204,9 +206,8 @@ func TestDetectorHungWorkspaceBuildNoOverrideState(t *testing.T) {
currentBuild := dbfake.WorkspaceBuild(t, db, previousBuild.Workspace).
Pubsub(pubsub).
Seed(database.WorkspaceBuild{
BuildNumber: 2,
ProvisionerState: expectedWorkspaceBuildState,
}).
BuildNumber: 2,
}).ProvisionerState(expectedWorkspaceBuildState).
Starting(dbfake.WithJobStartedAt(tenMinAgo), dbfake.WithJobUpdatedAt(sixMinAgo)).
Do()
@@ -235,7 +236,9 @@ func TestDetectorHungWorkspaceBuildNoOverrideState(t *testing.T) {
// Check that the provisioner state was NOT copied.
build, err := db.GetWorkspaceBuildByID(ctx, currentBuild.Build.ID)
require.NoError(t, err)
require.Equal(t, expectedWorkspaceBuildState, build.ProvisionerState)
provisionerStateRow, err := db.GetWorkspaceBuildProvisionerStateByID(ctx, build.ID)
require.NoError(t, err)
require.Equal(t, expectedWorkspaceBuildState, provisionerStateRow.ProvisionerState)
detector.Close()
detector.Wait()
@@ -266,9 +269,9 @@ func TestDetectorHungWorkspaceBuildNoOverrideStateIfNoExistingBuild(t *testing.T
currentBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OrganizationID: org.ID,
OwnerID: user.ID,
}).Pubsub(pubsub).Seed(database.WorkspaceBuild{
ProvisionerState: expectedWorkspaceBuildState,
}).Starting(dbfake.WithJobStartedAt(tenMinAgo), dbfake.WithJobUpdatedAt(sixMinAgo)).
}).Pubsub(pubsub).Seed(database.WorkspaceBuild{}).
ProvisionerState(expectedWorkspaceBuildState).
Starting(dbfake.WithJobStartedAt(tenMinAgo), dbfake.WithJobUpdatedAt(sixMinAgo)).
Do()
t.Log("current job ID: ", currentBuild.Build.JobID)
@@ -295,7 +298,9 @@ func TestDetectorHungWorkspaceBuildNoOverrideStateIfNoExistingBuild(t *testing.T
// Check that the provisioner state was NOT updated.
build, err := db.GetWorkspaceBuildByID(ctx, currentBuild.Build.ID)
require.NoError(t, err)
require.Equal(t, expectedWorkspaceBuildState, build.ProvisionerState)
provisionerStateRow, err := db.GetWorkspaceBuildProvisionerStateByID(ctx, build.ID)
require.NoError(t, err)
require.Equal(t, expectedWorkspaceBuildState, provisionerStateRow.ProvisionerState)
detector.Close()
detector.Wait()
@@ -325,9 +330,9 @@ func TestDetectorPendingWorkspaceBuildNoOverrideStateIfNoExistingBuild(t *testin
currentBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OrganizationID: org.ID,
OwnerID: user.ID,
}).Pubsub(pubsub).Seed(database.WorkspaceBuild{
ProvisionerState: expectedWorkspaceBuildState,
}).Pending(dbfake.WithJobCreatedAt(thirtyFiveMinAgo), dbfake.WithJobUpdatedAt(thirtyFiveMinAgo)).
}).Pubsub(pubsub).Seed(database.WorkspaceBuild{}).
ProvisionerState(expectedWorkspaceBuildState).
Pending(dbfake.WithJobCreatedAt(thirtyFiveMinAgo), dbfake.WithJobUpdatedAt(thirtyFiveMinAgo)).
Do()
t.Log("current job ID: ", currentBuild.Build.JobID)
@@ -356,7 +361,9 @@ func TestDetectorPendingWorkspaceBuildNoOverrideStateIfNoExistingBuild(t *testin
// Check that the provisioner state was NOT updated.
build, err := db.GetWorkspaceBuildByID(ctx, currentBuild.Build.ID)
require.NoError(t, err)
require.Equal(t, expectedWorkspaceBuildState, build.ProvisionerState)
provisionerStateRow, err := db.GetWorkspaceBuildProvisionerStateByID(ctx, build.ID)
require.NoError(t, err)
require.Equal(t, expectedWorkspaceBuildState, provisionerStateRow.ProvisionerState)
detector.Close()
detector.Wait()
@@ -398,9 +405,9 @@ func TestDetectorWorkspaceBuildForDormantWorkspace(t *testing.T) {
Time: now.Add(-time.Hour),
Valid: true,
},
}).Pubsub(pubsub).Seed(database.WorkspaceBuild{
ProvisionerState: expectedWorkspaceBuildState,
}).Starting(dbfake.WithJobStartedAt(tenMinAgo), dbfake.WithJobUpdatedAt(sixMinAgo)).
}).Pubsub(pubsub).Seed(database.WorkspaceBuild{}).
ProvisionerState(expectedWorkspaceBuildState).
Starting(dbfake.WithJobStartedAt(tenMinAgo), dbfake.WithJobUpdatedAt(sixMinAgo)).
Do()
t.Log("current job ID: ", currentBuild.Build.JobID)
@@ -725,11 +725,16 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo
}
}
provisionerStateRow, err := s.Database.GetWorkspaceBuildProvisionerStateByID(ctx, workspaceBuild.ID)
if err != nil {
return nil, failJob(fmt.Sprintf("get workspace build provisioner state: %s", err))
}
protoJob.Type = &proto.AcquiredJob_WorkspaceBuild_{
WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{
WorkspaceBuildId: workspaceBuild.ID.String(),
WorkspaceName: workspace.Name,
State: workspaceBuild.ProvisionerState,
State: provisionerStateRow.ProvisionerState,
RichParameterValues: convertRichParameterValues(workspaceBuildParameters),
PreviousParameterValues: convertRichParameterValues(lastWorkspaceBuildParameters),
VariableValues: asVariableValues(templateVariables),
@@ -1321,7 +1321,9 @@ func TestFailJob(t *testing.T) {
<-publishedLogs
build, err := db.GetWorkspaceBuildByID(ctx, buildID)
require.NoError(t, err)
require.Equal(t, "some state", string(build.ProvisionerState))
provisionerStateRow, err := db.GetWorkspaceBuildProvisionerStateByID(ctx, build.ID)
require.NoError(t, err)
require.Equal(t, "some state", string(provisionerStateRow.ProvisionerState))
require.Len(t, auditor.AuditLogs(), 1)
// Assert that the workspace_id field get populated
+1
View File
@@ -81,6 +81,7 @@ const (
SubjectAibridged SubjectType = "aibridged"
SubjectTypeDBPurge SubjectType = "dbpurge"
SubjectTypeBoundaryUsageTracker SubjectType = "boundary_usage_tracker"
SubjectTypeWorkspaceBuilder SubjectType = "workspace_builder"
)
const (
+7 -15
View File
@@ -856,32 +856,24 @@ func (api *API) workspaceBuildLogs(rw http.ResponseWriter, r *http.Request) {
func (api *API) workspaceBuildState(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspaceBuild := httpmw.WorkspaceBuildParam(r)
workspace, err := api.Database.GetWorkspaceByID(ctx, workspaceBuild.WorkspaceID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "No workspace exists for this job.",
})
// The dbauthz layer enforces policy.ActionUpdate on the template.
row, err := api.Database.GetWorkspaceBuildProvisionerStateByID(ctx, workspaceBuild.ID)
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return
}
template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to get template",
Message: "Internal error fetching provisioner state.",
Detail: err.Error(),
})
return
}
// You must have update permissions on the template to get the state.
// This matches a push!
if !api.Authorize(r, policy.ActionUpdate, template.RBACObject()) {
httpapi.ResourceNotFound(rw)
return
}
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write(workspaceBuild.ProvisionerState)
_, _ = rw.Write(row.ProvisionerState)
}
// @Summary Update workspace build state
+9 -4
View File
@@ -452,7 +452,7 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
// to read all provisioner daemons. We need to retrieve the eligible
// provisioner daemons for this job to show in the UI if there is no
// matching provisioner daemon.
provisionerDaemons, err := b.store.GetEligibleProvisionerDaemonsByProvisionerJobIDs(dbauthz.AsSystemReadProvisionerDaemons(b.ctx), []uuid.UUID{provisionerJob.ID})
provisionerDaemons, err := b.store.GetEligibleProvisionerDaemonsByProvisionerJobIDs(dbauthz.AsWorkspaceBuilder(b.ctx), []uuid.UUID{provisionerJob.ID})
if err != nil {
// NOTE: we do **not** want to fail a workspace build if we fail to
// retrieve provisioner daemons. This is just to show in the UI if there
@@ -570,8 +570,8 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
}
}
if b.state.orphan && !hasActiveEligibleProvisioner {
// nolint: gocritic // At this moment, we are pretending to be provisionerd.
if err := store.UpdateProvisionerJobWithCompleteWithStartedAtByID(dbauthz.AsProvisionerd(b.ctx), database.UpdateProvisionerJobWithCompleteWithStartedAtByIDParams{
// nolint: gocritic // User won't necessarily have the permission to do this so we act as a system user.
if err := store.UpdateProvisionerJobWithCompleteWithStartedAtByID(dbauthz.AsWorkspaceBuilder(b.ctx), database.UpdateProvisionerJobWithCompleteWithStartedAtByIDParams{
CompletedAt: sql.NullTime{Valid: true, Time: now},
Error: sql.NullString{Valid: false},
ErrorCode: sql.NullString{Valid: false},
@@ -815,7 +815,12 @@ func (b *Builder) getState() ([]byte, error) {
if err != nil {
return nil, xerrors.Errorf("get last build to get state: %w", err)
}
return bld.ProvisionerState, nil
// nolint: gocritic // Workspace builder needs to read provisioner state for the new build.
state, err := b.store.GetWorkspaceBuildProvisionerStateByID(dbauthz.AsWorkspaceBuilder(b.ctx), bld.ID)
if err != nil {
return nil, xerrors.Errorf("get workspace build provisioner state: %w", err)
}
return state.ProvisionerState, nil
}
func (b *Builder) getParameters() (names, values []string, err error) {
+20 -1
View File
@@ -65,6 +65,7 @@ func TestBuilder_NoOptions(t *testing.T) {
withTemplate,
withInactiveVersion(nil),
withLastBuildFound,
withLastBuildState,
withTemplateVersionVariables(inactiveVersionID, nil),
withRichParameters(nil),
withParameterSchemas(inactiveJobID, nil),
@@ -124,6 +125,7 @@ func TestBuilder_Initiator(t *testing.T) {
withTemplate,
withInactiveVersion(nil),
withLastBuildFound,
withLastBuildState,
withTemplateVersionVariables(inactiveVersionID, nil),
withRichParameters(nil),
withParameterSchemas(inactiveJobID, nil),
@@ -174,6 +176,7 @@ func TestBuilder_Baggage(t *testing.T) {
withTemplate,
withInactiveVersion(nil),
withLastBuildFound,
withLastBuildState,
withTemplateVersionVariables(inactiveVersionID, nil),
withRichParameters(nil),
withParameterSchemas(inactiveJobID, nil),
@@ -216,6 +219,7 @@ func TestBuilder_Reason(t *testing.T) {
withTemplate,
withInactiveVersion(nil),
withLastBuildFound,
withLastBuildState,
withTemplateVersionVariables(inactiveVersionID, nil),
withRichParameters(nil),
withParameterSchemas(inactiveJobID, nil),
@@ -365,6 +369,7 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
withTemplate,
withInactiveVersion(richParameters),
withLastBuildFound,
withLastBuildState,
withTemplateVersionVariables(inactiveVersionID, templateVersionVariables),
withRichParameters(nil),
withParameterSchemas(inactiveJobID, nil),
@@ -464,6 +469,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
withTemplate,
withInactiveVersion(richParameters),
withLastBuildFound,
withLastBuildState,
withTemplateVersionVariables(inactiveVersionID, nil),
withRichParameters(initialBuildParameters),
withParameterSchemas(inactiveJobID, nil),
@@ -515,6 +521,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
withTemplate,
withInactiveVersion(richParameters),
withLastBuildFound,
withLastBuildState,
withTemplateVersionVariables(inactiveVersionID, nil),
withRichParameters(initialBuildParameters),
withParameterSchemas(inactiveJobID, nil),
@@ -661,6 +668,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
withTemplate,
withActiveVersion(version2params),
withLastBuildFound,
withLastBuildState,
withTemplateVersionVariables(activeVersionID, nil),
withRichParameters(initialBuildParameters),
withParameterSchemas(activeJobID, nil),
@@ -727,6 +735,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
withTemplate,
withActiveVersion(version2params),
withLastBuildFound,
withLastBuildState,
withTemplateVersionVariables(activeVersionID, nil),
withRichParameters(initialBuildParameters),
withParameterSchemas(activeJobID, nil),
@@ -791,6 +800,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
withTemplate,
withActiveVersion(version2params),
withLastBuildFound,
withLastBuildState,
withTemplateVersionVariables(activeVersionID, nil),
withRichParameters(initialBuildParameters),
withParameterSchemas(activeJobID, nil),
@@ -1062,6 +1072,7 @@ func TestWorkspaceBuildUsageChecker(t *testing.T) {
withTemplate,
withInactiveVersion(nil),
withLastBuildFound,
withLastBuildState,
withTemplateVersionVariables(inactiveVersionID, nil),
withRichParameters(nil),
withParameterSchemas(inactiveJobID, nil),
@@ -1175,6 +1186,7 @@ func TestWorkspaceBuildWithTask(t *testing.T) {
withTemplate,
withInactiveVersion(nil),
withLastBuildFound,
withLastBuildState,
withTemplateVersionVariables(inactiveVersionID, nil),
withRichParameters(nil),
withParameterSchemas(inactiveJobID, nil),
@@ -1378,7 +1390,6 @@ func withLastBuildFound(mTx *dbmock.MockStore) {
Transition: database.WorkspaceTransitionStart,
InitiatorID: userID,
JobID: lastBuildJobID,
ProvisionerState: []byte("last build state"),
Reason: database.BuildReasonInitiator,
}, nil)
@@ -1398,6 +1409,14 @@ func withLastBuildFound(mTx *dbmock.MockStore) {
}, nil)
}
func withLastBuildState(mTx *dbmock.MockStore) {
mTx.EXPECT().GetWorkspaceBuildProvisionerStateByID(gomock.Any(), lastBuildID).
Times(1).
Return(database.GetWorkspaceBuildProvisionerStateByIDRow{
ProvisionerState: []byte("last build state"),
}, nil)
}
func withLastBuildNotFound(mTx *dbmock.MockStore) {
mTx.EXPECT().GetLatestWorkspaceBuildByWorkspaceID(gomock.Any(), workspaceID).
Times(1).
+1 -1
View File
@@ -36,7 +36,7 @@ We track the following resources:
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>active_version_id</td><td>true</td></tr><tr><td>activity_bump</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>cors_behavior</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>disable_module_cache</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_port_sharing_level</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_display_name</td><td>false</td></tr><tr><td>organization_icon</td><td>false</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>organization_name</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>use_classic_parameter_flow</td><td>true</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>has_external_agent</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>source_example_id</td><td>false</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>github_com_user_id</td><td>false</td></tr><tr><td>hashed_one_time_passcode</td><td>false</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>is_system</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>one_time_passcode_expires_at</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>has_external_agent</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_name</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>template_version_preset_id</td><td>false</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>has_external_agent</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_name</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>template_version_preset_id</td><td>false</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
| WorkspaceTable<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>favorite</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>next_start_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
-1
View File
@@ -190,7 +190,6 @@ var auditableResourcesTypes = map[any]map[string]Action{
"build_number": ActionIgnore,
"transition": ActionIgnore,
"initiator_id": ActionIgnore,
"provisioner_state": ActionIgnore,
"job_id": ActionIgnore,
"deadline": ActionIgnore,
"reason": ActionIgnore,
+26 -11
View File
@@ -253,13 +253,16 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
TemplateID: template.ID,
}).Seed(database.WorkspaceBuild{
TemplateVersionID: templateVersion.ID,
ProvisionerState: []byte(must(cryptorand.String(64))),
}).Succeeded(dbfake.WithJobCompletedAt(buildTime)).Do()
}).ProvisionerState([]byte(must(cryptorand.String(64)))).Succeeded(dbfake.WithJobCompletedAt(buildTime)).Do()
// Assert test invariant: workspace build state must not be empty
require.NotEmpty(t, buildResp.Build.ProvisionerState, "provisioner state must not be empty")
var buildProvisionerState []byte
buildProvisionerStateRow, err := db.GetWorkspaceBuildProvisionerStateByID(ctx, buildResp.Build.ID)
require.NoError(t, err)
buildProvisionerState = buildProvisionerStateRow.ProvisionerState
require.NotEmpty(t, buildProvisionerState, "provisioner state must not be empty")
err := db.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{
err = db.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{
ID: buildResp.Build.ID,
UpdatedAt: buildTime,
Deadline: c.deadline,
@@ -310,7 +313,9 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
require.WithinDuration(t, c.newMaxDeadline, newBuild.MaxDeadline, time.Second, "max_deadline")
// Check that the new build has the same state as before.
require.Equal(t, wsBuild.ProvisionerState, newBuild.ProvisionerState, "provisioner state mismatch")
newBuildProvisionerStateRow, err := db.GetWorkspaceBuildProvisionerStateByID(ctx, newBuild.ID)
require.NoError(t, err)
require.Equal(t, buildProvisionerState, newBuildProvisionerStateRow.ProvisionerState, "provisioner state mismatch")
})
}
}
@@ -388,7 +393,8 @@ func TestTemplateUpdateBuildDeadlinesSkip(t *testing.T) {
shouldBeUpdated bool
// Set below:
wsBuild database.WorkspaceBuild
wsBuild database.WorkspaceBuild
wsBuildProvisionerState []byte
}{
{
name: "DifferentTemplate",
@@ -483,19 +489,25 @@ func TestTemplateUpdateBuildDeadlinesSkip(t *testing.T) {
},
OrganizationID: templateJob.OrganizationID,
})
wsBuildProvisionerState := []byte(must(cryptorand.String(64)))
wsBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
WorkspaceID: wsID,
BuildNumber: b.buildNumber,
JobID: job.ID,
InitiatorID: user.ID,
TemplateVersionID: templateVersion.ID,
ProvisionerState: []byte(must(cryptorand.String(64))),
})
err = db.UpdateWorkspaceBuildProvisionerStateByID(ctx, database.UpdateWorkspaceBuildProvisionerStateByIDParams{
ID: wsBuild.ID,
UpdatedAt: wsBuild.UpdatedAt,
ProvisionerState: wsBuildProvisionerState,
})
require.NoError(t, err)
// Assert test invariant: workspace build state must not be empty
require.NotEmpty(t, wsBuild.ProvisionerState, "provisioner state must not be empty")
require.NotEmpty(t, wsBuildProvisionerState, "provisioner state must not be empty")
err := db.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{
err = db.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{
ID: wsBuild.ID,
UpdatedAt: buildTime,
Deadline: originalMaxDeadline,
@@ -507,8 +519,9 @@ func TestTemplateUpdateBuildDeadlinesSkip(t *testing.T) {
require.NoError(t, err)
// Assert test invariant: workspace build state must not be empty
require.NotEmpty(t, wsBuild.ProvisionerState, "provisioner state must not be empty")
require.NotEmpty(t, wsBuildProvisionerState, "provisioner state must not be empty")
builds[i].wsBuildProvisionerState = wsBuildProvisionerState
builds[i].wsBuild = wsBuild
if !b.buildStarted {
@@ -595,7 +608,9 @@ func TestTemplateUpdateBuildDeadlinesSkip(t *testing.T) {
assert.WithinDuration(t, originalMaxDeadline, newBuild.MaxDeadline, time.Second, msg)
}
assert.Equal(t, builds[i].wsBuild.ProvisionerState, newBuild.ProvisionerState, "provisioner state mismatch")
newBuildProvisionerStateRow, err := db.GetWorkspaceBuildProvisionerStateByID(ctx, newBuild.ID)
require.NoError(t, err)
assert.Equal(t, builds[i].wsBuildProvisionerState, newBuildProvisionerStateRow.ProvisionerState, "provisioner state mismatch")
}
}