feat: add workspace restart functionality to API (#25757)

This models restart as durable orchestration of existing stop and
start workspace builds instead of adding a new restart transition.
Keeping restart as two existing transitions preserves the current
build/provisioner model.

The child start build is created only after the parent stop build
succeeds, rather than being inserted immediately in a pending
state. That keeps `workspace_builds` aligned with actual
provisioner-ready work and avoids introducing a second
pending-build lifecycle that the provisioner and build acquisition
paths would need to understand.

Refs: https://linear.app/codercom/issue/PLAT-143
This commit is contained in:
George K
2026-07-07 09:18:30 -07:00
committed by GitHub
parent ba094c5706
commit 6af0f4d698
45 changed files with 4412 additions and 144 deletions
+133 -29
View File
@@ -482,31 +482,32 @@ var (
Identifier: rbac.RoleIdentifier{Name: "system"},
DisplayName: "Coder",
Site: rbac.Permissions(map[string][]policy.Action{
rbac.ResourceWildcard.Type: {policy.ActionRead},
rbac.ResourceApiKey.Type: rbac.ResourceApiKey.AvailableActions(),
rbac.ResourceGroup.Type: {policy.ActionCreate, policy.ActionUpdate},
rbac.ResourceAssignRole.Type: rbac.ResourceAssignRole.AvailableActions(),
rbac.ResourceAssignOrgRole.Type: rbac.ResourceAssignOrgRole.AvailableActions(),
rbac.ResourceSystem.Type: {policy.WildcardSymbol},
rbac.ResourceOrganization.Type: {policy.ActionCreate, policy.ActionRead},
rbac.ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionDelete, policy.ActionRead},
rbac.ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
rbac.ResourceUser.Type: rbac.ResourceUser.AvailableActions(),
rbac.ResourceWorkspaceDormant.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStop},
rbac.ResourceWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop, policy.ActionSSH, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent},
rbac.ResourceWorkspaceProxy.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceDeploymentConfig.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceNotificationMessage.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceNotificationPreference.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceNotificationTemplate.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceCryptoKey.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceFile.Type: {policy.ActionCreate, policy.ActionRead},
rbac.ResourceProvisionerJobs.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionCreate},
rbac.ResourceOauth2App.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceOauth2AppSecret.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceChat.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceAIProvider.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceAIGatewayKey.Type: {policy.ActionRead, policy.ActionUpdate},
rbac.ResourceWildcard.Type: {policy.ActionRead},
rbac.ResourceApiKey.Type: rbac.ResourceApiKey.AvailableActions(),
rbac.ResourceGroup.Type: {policy.ActionCreate, policy.ActionUpdate},
rbac.ResourceAssignRole.Type: rbac.ResourceAssignRole.AvailableActions(),
rbac.ResourceAssignOrgRole.Type: rbac.ResourceAssignOrgRole.AvailableActions(),
rbac.ResourceSystem.Type: {policy.WildcardSymbol},
rbac.ResourceOrganization.Type: {policy.ActionCreate, policy.ActionRead},
rbac.ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionDelete, policy.ActionRead},
rbac.ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
rbac.ResourceUser.Type: rbac.ResourceUser.AvailableActions(),
rbac.ResourceWorkspaceDormant.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStop},
rbac.ResourceWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop, policy.ActionSSH, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent},
rbac.ResourceWorkspaceProxy.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceWorkspaceBuildOrchestration.Type: {policy.ActionUpdate, policy.ActionRead},
rbac.ResourceDeploymentConfig.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceNotificationMessage.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceNotificationPreference.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceNotificationTemplate.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceCryptoKey.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceFile.Type: {policy.ActionCreate, policy.ActionRead},
rbac.ResourceProvisionerJobs.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionCreate},
rbac.ResourceOauth2App.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceOauth2AppSecret.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceChat.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceAIProvider.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceAIGatewayKey.Type: {policy.ActionRead, policy.ActionUpdate},
}),
User: []rbac.Permission{},
ByOrgID: map[string]rbac.OrgPermissions{},
@@ -691,10 +692,11 @@ var (
Identifier: rbac.RoleIdentifier{Name: "dbpurge"},
DisplayName: "DB Purge Daemon",
Site: rbac.Permissions(map[string][]policy.Action{
rbac.ResourceSystem.Type: {policy.ActionDelete},
rbac.ResourceNotificationMessage.Type: {policy.ActionDelete},
rbac.ResourceApiKey.Type: {policy.ActionDelete},
rbac.ResourceAibridgeInterception.Type: {policy.ActionDelete},
rbac.ResourceSystem.Type: {policy.ActionDelete},
rbac.ResourceNotificationMessage.Type: {policy.ActionDelete},
rbac.ResourceApiKey.Type: {policy.ActionDelete},
rbac.ResourceAibridgeInterception.Type: {policy.ActionDelete},
rbac.ResourceWorkspaceBuildOrchestration.Type: {policy.ActionDelete},
// Chat auto-archive sets archived=true on inactive chats.
rbac.ResourceChat.Type: {policy.ActionRead, policy.ActionUpdate},
// Purge old boundary logs past the retention period.
@@ -2390,6 +2392,13 @@ func (q *querier) DeleteOldWorkspaceAgentStats(ctx context.Context) error {
return q.db.DeleteOldWorkspaceAgentStats(ctx)
}
func (q *querier) DeleteOldWorkspaceBuildOrchestrations(ctx context.Context, arg database.DeleteOldWorkspaceBuildOrchestrationsParams) (int64, error) {
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceWorkspaceBuildOrchestration.AnyOrganization()); err != nil {
return 0, err
}
return q.db.DeleteOldWorkspaceBuildOrchestrations(ctx, arg)
}
func (q *querier) DeleteOrganizationMember(ctx context.Context, arg database.DeleteOrganizationMemberParams) error {
return deleteQ[database.OrganizationMember](q.log, q.auth, func(ctx context.Context, arg database.DeleteOrganizationMemberParams) (database.OrganizationMember, error) {
member, err := database.ExpectOne(q.OrganizationMembers(ctx, database.OrganizationMembersParams{
@@ -3996,6 +4005,13 @@ func (q *querier) GetMCPServerUserTokensByUserID(ctx context.Context, userID uui
return q.db.GetMCPServerUserTokensByUserID(ctx, userID)
}
func (q *querier) GetNextPendingWorkspaceBuildOrchestrationForUpdate(ctx context.Context) (database.WorkspaceBuildOrchestration, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspaceBuildOrchestration.AnyOrganization()); err != nil {
return database.WorkspaceBuildOrchestration{}, err
}
return q.db.GetNextPendingWorkspaceBuildOrchestrationForUpdate(ctx)
}
func (q *querier) GetNotificationMessagesByStatus(ctx context.Context, arg database.GetNotificationMessagesByStatusParams) ([]database.NotificationMessage, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceNotificationMessage); err != nil {
return nil, err
@@ -6466,6 +6482,66 @@ func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertW
return q.db.InsertWorkspaceBuild(ctx, arg)
}
func (q *querier) InsertWorkspaceBuildOrchestration(ctx context.Context, arg database.InsertWorkspaceBuildOrchestrationParams) (database.WorkspaceBuildOrchestration, error) {
// Read through the raw q.db to fetch the authz context; authorization
// happens via q.authorizeContext below, as in InsertWorkspaceBuild.
parentBuild, err := q.db.GetWorkspaceBuildByID(ctx, arg.ParentBuildID)
if err != nil {
return database.WorkspaceBuildOrchestration{}, xerrors.Errorf("get parent workspace build by id: %w", err)
}
workspace, err := q.db.GetWorkspaceByID(ctx, parentBuild.WorkspaceID)
if err != nil {
return database.WorkspaceBuildOrchestration{}, xerrors.Errorf("get workspace by id: %w", err)
}
if workspace.IsPrebuild() {
return database.WorkspaceBuildOrchestration{}, xerrors.New("cannot orchestrate prebuild workspace builds")
}
// The current API flow inserts this row immediately after
// creating the parent build, so the parent transition has already
// been authorized. Still, make sure future callers cannot attach
// the child intent to a parent build the actor could not initiate.
parentAction, err := workspaceTransitionAction(parentBuild.Transition)
if err != nil {
return database.WorkspaceBuildOrchestration{}, err
}
if err := q.authorizeContext(ctx, parentAction, workspace); err != nil {
return database.WorkspaceBuildOrchestration{}, err
}
// The orchestrator uses system authority to create the child
// build after the parent succeeds, so the initiating actor must
// be authorized now.
childAction, err := workspaceTransitionAction(arg.ChildTransition)
if err != nil {
return database.WorkspaceBuildOrchestration{}, err
}
if err := q.authorizeContext(ctx, childAction, workspace); err != nil {
return database.WorkspaceBuildOrchestration{}, err
}
if arg.ChildTransition == database.WorkspaceTransitionStart && arg.ChildTemplateVersionID.Valid {
// Only template admins may queue child builds with a durable
// template version pin, since the active version can change
// before the worker creates the child build.
template, err := q.db.GetTemplateByID(ctx, workspace.TemplateID)
if err != nil {
return database.WorkspaceBuildOrchestration{}, xerrors.Errorf("get template by id: %w", err)
}
err = q.authorizeContext(ctx, policy.ActionUpdate, template)
var notAuthorized NotAuthorizedError
if xerrors.As(err, &notAuthorized) {
return database.WorkspaceBuildOrchestration{}, err
} else if err != nil {
return database.WorkspaceBuildOrchestration{}, xerrors.Errorf("cannot pin template version for child build: %w", err)
}
}
return q.db.InsertWorkspaceBuildOrchestration(ctx, arg)
}
func (q *querier) InsertWorkspaceBuildParameters(ctx context.Context, arg database.InsertWorkspaceBuildParametersParams) error {
// TODO: Optimize this. We always have the workspace and build already fetched.
build, err := q.db.GetWorkspaceBuildByID(ctx, arg.WorkspaceBuildID)
@@ -8457,6 +8533,34 @@ func (q *querier) UpdateWorkspaceBuildNotifiedAutostopDeadline(ctx context.Conte
return q.db.UpdateWorkspaceBuildNotifiedAutostopDeadline(ctx, arg)
}
func (q *querier) UpdateWorkspaceBuildOrchestrationCanceledByID(ctx context.Context, arg database.UpdateWorkspaceBuildOrchestrationCanceledByIDParams) (database.WorkspaceBuildOrchestration, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspaceBuildOrchestration.AnyOrganization()); err != nil {
return database.WorkspaceBuildOrchestration{}, err
}
return q.db.UpdateWorkspaceBuildOrchestrationCanceledByID(ctx, arg)
}
func (q *querier) UpdateWorkspaceBuildOrchestrationCompletedByID(ctx context.Context, arg database.UpdateWorkspaceBuildOrchestrationCompletedByIDParams) (database.WorkspaceBuildOrchestration, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspaceBuildOrchestration.AnyOrganization()); err != nil {
return database.WorkspaceBuildOrchestration{}, err
}
return q.db.UpdateWorkspaceBuildOrchestrationCompletedByID(ctx, arg)
}
func (q *querier) UpdateWorkspaceBuildOrchestrationFailedByID(ctx context.Context, arg database.UpdateWorkspaceBuildOrchestrationFailedByIDParams) (database.WorkspaceBuildOrchestration, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspaceBuildOrchestration.AnyOrganization()); err != nil {
return database.WorkspaceBuildOrchestration{}, err
}
return q.db.UpdateWorkspaceBuildOrchestrationFailedByID(ctx, arg)
}
func (q *querier) UpdateWorkspaceBuildOrchestrationRetryByID(ctx context.Context, arg database.UpdateWorkspaceBuildOrchestrationRetryByIDParams) (database.WorkspaceBuildOrchestration, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspaceBuildOrchestration.AnyOrganization()); err != nil {
return database.WorkspaceBuildOrchestration{}, err
}
return q.db.UpdateWorkspaceBuildOrchestrationRetryByID(ctx, arg)
}
func (q *querier) UpdateWorkspaceBuildProvisionerStateByID(ctx context.Context, arg database.UpdateWorkspaceBuildProvisionerStateByIDParams) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
return err
+156
View File
@@ -4024,6 +4024,162 @@ func (s *MethodTestSuite) TestWorkspace() {
dbm.EXPECT().InsertWorkspaceBuild(gomock.Any(), arg).Return(nil).AnyTimes()
check.Args(arg).Asserts(w, policy.ActionDelete)
}))
s.Run("Start/PinnedVersion/InsertWorkspaceBuildOrchestration", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
activeVersionID := uuid.New()
childVersionID := uuid.New()
t := testutil.Fake(s.T(), faker, database.Template{ActiveVersionID: activeVersionID})
w := testutil.Fake(s.T(), faker, database.Workspace{TemplateID: t.ID})
parentBuild := testutil.Fake(s.T(), faker, database.WorkspaceBuild{
WorkspaceID: w.ID,
TemplateVersionID: activeVersionID,
Transition: database.WorkspaceTransitionStop,
})
arg := database.InsertWorkspaceBuildOrchestrationParams{
ParentBuildID: parentBuild.ID,
ChildTransition: database.WorkspaceTransitionStart,
ChildTemplateVersionID: uuid.NullUUID{
UUID: childVersionID,
Valid: true,
},
}
orchestration := testutil.Fake(s.T(), faker, database.WorkspaceBuildOrchestration{})
dbm.EXPECT().GetWorkspaceBuildByID(gomock.Any(), parentBuild.ID).Return(parentBuild, nil).AnyTimes()
dbm.EXPECT().GetWorkspaceByID(gomock.Any(), w.ID).Return(w, nil).AnyTimes()
dbm.EXPECT().GetTemplateByID(gomock.Any(), t.ID).Return(t, nil).AnyTimes()
// Ensure template admins may queue child builds with a durable
// template version pin.
dbm.EXPECT().InsertWorkspaceBuildOrchestration(gomock.Any(), arg).Return(orchestration, nil).AnyTimes()
check.Args(arg).
Asserts(
w, policy.ActionWorkspaceStop,
w, policy.ActionWorkspaceStart,
t, policy.ActionUpdate,
).
Returns(orchestration)
}))
s.Run("Start/PinnedVersionWithoutTemplateUpdate/InsertWorkspaceBuildOrchestration", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
activeVersionID := uuid.New()
t := testutil.Fake(s.T(), faker, database.Template{ActiveVersionID: activeVersionID})
w := testutil.Fake(s.T(), faker, database.Workspace{TemplateID: t.ID})
parentBuild := testutil.Fake(s.T(), faker, database.WorkspaceBuild{
WorkspaceID: w.ID,
TemplateVersionID: activeVersionID,
Transition: database.WorkspaceTransitionStop,
})
arg := database.InsertWorkspaceBuildOrchestrationParams{
ParentBuildID: parentBuild.ID,
ChildTransition: database.WorkspaceTransitionStart,
ChildTemplateVersionID: uuid.NullUUID{
UUID: activeVersionID,
Valid: true,
},
}
dbm.EXPECT().GetWorkspaceBuildByID(gomock.Any(), parentBuild.ID).Return(parentBuild, nil).AnyTimes()
dbm.EXPECT().GetWorkspaceByID(gomock.Any(), w.ID).Return(w, nil).AnyTimes()
dbm.EXPECT().GetTemplateByID(gomock.Any(), t.ID).Return(t, nil).AnyTimes()
// Ensure non-template admins cannot queue a durable template
// version pin for the child build.
check.Args(arg).
Asserts(
w, policy.ActionWorkspaceStop,
w, policy.ActionWorkspaceStart,
t, policy.ActionUpdate,
).
Errors(errMatchAny).
WithSuccessAuthorizer(func(_ context.Context, _ rbac.Subject, action policy.Action, obj rbac.Object) error {
if action == policy.ActionUpdate && obj.Type == rbac.ResourceTemplate.Type {
return xerrors.New("not authorized to update template")
}
return nil
})
}))
s.Run("Start/UnpinnedVersion/InsertWorkspaceBuildOrchestration", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
t := testutil.Fake(s.T(), faker, database.Template{})
w := testutil.Fake(s.T(), faker, database.Workspace{TemplateID: t.ID})
parentBuild := testutil.Fake(s.T(), faker, database.WorkspaceBuild{
WorkspaceID: w.ID,
Transition: database.WorkspaceTransitionStop,
})
arg := database.InsertWorkspaceBuildOrchestrationParams{
ParentBuildID: parentBuild.ID,
ChildTransition: database.WorkspaceTransitionStart,
}
orchestration := testutil.Fake(s.T(), faker, database.WorkspaceBuildOrchestration{})
dbm.EXPECT().GetWorkspaceBuildByID(gomock.Any(), parentBuild.ID).Return(parentBuild, nil).AnyTimes()
dbm.EXPECT().GetWorkspaceByID(gomock.Any(), w.ID).Return(w, nil).AnyTimes()
// Ensure an unpinned child build does not require template update permission.
dbm.EXPECT().InsertWorkspaceBuildOrchestration(gomock.Any(), arg).Return(orchestration, nil).AnyTimes()
check.Args(arg).
Asserts(
w, policy.ActionWorkspaceStop,
w, policy.ActionWorkspaceStart,
).
Returns(orchestration)
}))
s.Run("GetNextPendingWorkspaceBuildOrchestrationForUpdate", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
orchestration := testutil.Fake(s.T(), faker, database.WorkspaceBuildOrchestration{})
dbm.EXPECT().GetNextPendingWorkspaceBuildOrchestrationForUpdate(gomock.Any()).Return(orchestration, nil).AnyTimes()
check.Args().
Asserts(rbac.ResourceWorkspaceBuildOrchestration.AnyOrganization(), policy.ActionRead).
Returns(orchestration)
}))
s.Run("UpdateWorkspaceBuildOrchestrationCanceledByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
orchestration := testutil.Fake(s.T(), faker, database.WorkspaceBuildOrchestration{})
arg := database.UpdateWorkspaceBuildOrchestrationCanceledByIDParams{
UpdatedAt: dbtime.Now(),
ID: orchestration.ID,
}
dbm.EXPECT().UpdateWorkspaceBuildOrchestrationCanceledByID(gomock.Any(), arg).Return(orchestration, nil).AnyTimes()
check.Args(arg).
Asserts(rbac.ResourceWorkspaceBuildOrchestration.AnyOrganization(), policy.ActionUpdate).
Returns(orchestration)
}))
s.Run("UpdateWorkspaceBuildOrchestrationCompletedByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
orchestration := testutil.Fake(s.T(), faker, database.WorkspaceBuildOrchestration{})
arg := database.UpdateWorkspaceBuildOrchestrationCompletedByIDParams{
ChildBuildID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
UpdatedAt: dbtime.Now(),
ID: orchestration.ID,
}
dbm.EXPECT().UpdateWorkspaceBuildOrchestrationCompletedByID(gomock.Any(), arg).Return(orchestration, nil).AnyTimes()
check.Args(arg).
Asserts(rbac.ResourceWorkspaceBuildOrchestration.AnyOrganization(), policy.ActionUpdate).
Returns(orchestration)
}))
s.Run("UpdateWorkspaceBuildOrchestrationFailedByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
orchestration := testutil.Fake(s.T(), faker, database.WorkspaceBuildOrchestration{})
arg := database.UpdateWorkspaceBuildOrchestrationFailedByIDParams{
Error: sql.NullString{String: "failed", Valid: true},
UpdatedAt: dbtime.Now(),
ID: orchestration.ID,
}
dbm.EXPECT().UpdateWorkspaceBuildOrchestrationFailedByID(gomock.Any(), arg).Return(orchestration, nil).AnyTimes()
check.Args(arg).
Asserts(rbac.ResourceWorkspaceBuildOrchestration.AnyOrganization(), policy.ActionUpdate).
Returns(orchestration)
}))
s.Run("UpdateWorkspaceBuildOrchestrationRetryByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
orchestration := testutil.Fake(s.T(), faker, database.WorkspaceBuildOrchestration{})
arg := database.UpdateWorkspaceBuildOrchestrationRetryByIDParams{
MaxAttemptCount: 3,
NextRetryAfter: dbtime.Now(),
Error: sql.NullString{String: "retry", Valid: true},
UpdatedAt: dbtime.Now(),
ID: orchestration.ID,
}
dbm.EXPECT().UpdateWorkspaceBuildOrchestrationRetryByID(gomock.Any(), arg).Return(orchestration, nil).AnyTimes()
check.Args(arg).
Asserts(rbac.ResourceWorkspaceBuildOrchestration.AnyOrganization(), policy.ActionUpdate).
Returns(orchestration)
}))
s.Run("DeleteOldWorkspaceBuildOrchestrations", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
arg := database.DeleteOldWorkspaceBuildOrchestrationsParams{
BeforeTime: dbtime.Now(),
LimitCount: 100,
}
dbm.EXPECT().DeleteOldWorkspaceBuildOrchestrations(gomock.Any(), arg).Return(int64(0), nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceWorkspaceBuildOrchestration.AnyOrganization(), policy.ActionDelete)
}))
s.Run("Start/InsertWorkspaceBuildParameters", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
w := testutil.Fake(s.T(), faker, database.Workspace{})
b := testutil.Fake(s.T(), faker, database.WorkspaceBuild{