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
+61 -54
View File
@@ -6,58 +6,65 @@ type CheckConstraint string
// CheckConstraint enums.
const (
CheckAIGatewayKeysHashedSecretCheck CheckConstraint = "ai_gateway_keys_hashed_secret_check" // ai_gateway_keys
CheckAIGatewayKeysNameCheck CheckConstraint = "ai_gateway_keys_name_check" // ai_gateway_keys
CheckAIGatewayKeysSecretPrefixCheck CheckConstraint = "ai_gateway_keys_secret_prefix_check" // ai_gateway_keys
CheckAIModelPricesCacheReadPriceCheck CheckConstraint = "ai_model_prices_cache_read_price_check" // ai_model_prices
CheckAIModelPricesCacheWritePriceCheck CheckConstraint = "ai_model_prices_cache_write_price_check" // ai_model_prices
CheckAIModelPricesInputPriceCheck CheckConstraint = "ai_model_prices_input_price_check" // ai_model_prices
CheckAIModelPricesOutputPriceCheck CheckConstraint = "ai_model_prices_output_price_check" // ai_model_prices
CheckAIProvidersNameCheck CheckConstraint = "ai_providers_name_check" // ai_providers
CheckAIUserDailySpendSpendMicrosCheck CheckConstraint = "ai_user_daily_spend_spend_micros_check" // ai_user_daily_spend
CheckAibridgeTokenUsagesCacheReadPriceMicrosCheck CheckConstraint = "aibridge_token_usages_cache_read_price_micros_check" // aibridge_token_usages
CheckAibridgeTokenUsagesCacheWritePriceMicrosCheck CheckConstraint = "aibridge_token_usages_cache_write_price_micros_check" // aibridge_token_usages
CheckAibridgeTokenUsagesCostMicrosCheck CheckConstraint = "aibridge_token_usages_cost_micros_check" // aibridge_token_usages
CheckAibridgeTokenUsagesInputPriceMicrosCheck CheckConstraint = "aibridge_token_usages_input_price_micros_check" // aibridge_token_usages
CheckAibridgeTokenUsagesOutputPriceMicrosCheck CheckConstraint = "aibridge_token_usages_output_price_micros_check" // aibridge_token_usages
CheckAPIKeysAllowListNotEmpty CheckConstraint = "api_keys_allow_list_not_empty" // api_keys
CheckBoundaryLogsSequenceNumberCheck CheckConstraint = "boundary_logs_sequence_number_check" // boundary_logs
CheckChatModelConfigsAIProviderRequiredWhenActive CheckConstraint = "chat_model_configs_ai_provider_required_when_active" // chat_model_configs
CheckChatModelConfigsCompressionThresholdCheck CheckConstraint = "chat_model_configs_compression_threshold_check" // chat_model_configs
CheckChatModelConfigsContextLimitCheck CheckConstraint = "chat_model_configs_context_limit_check" // chat_model_configs
CheckChatUsageLimitConfigDefaultLimitMicrosCheck CheckConstraint = "chat_usage_limit_config_default_limit_micros_check" // chat_usage_limit_config
CheckChatUsageLimitConfigPeriodCheck CheckConstraint = "chat_usage_limit_config_period_check" // chat_usage_limit_config
CheckChatUsageLimitConfigSingletonCheck CheckConstraint = "chat_usage_limit_config_singleton_check" // chat_usage_limit_config
CheckChatAclOnlyOnRootChats CheckConstraint = "chat_acl_only_on_root_chats" // chats
CheckChatGroupAclNotNullJsonb CheckConstraint = "chat_group_acl_not_null_jsonb" // chats
CheckChatUserAclNotNullJsonb CheckConstraint = "chat_user_acl_not_null_jsonb" // chats
CheckChatsPinOrderArchivedCheck CheckConstraint = "chats_pin_order_archived_check" // chats
CheckChatsPinOrderParentCheck CheckConstraint = "chats_pin_order_parent_check" // chats
CheckOneTimePasscodeSet CheckConstraint = "one_time_passcode_set" // users
CheckUsersChatSpendLimitMicrosCheck CheckConstraint = "users_chat_spend_limit_micros_check" // users
CheckUsersEmailNotEmpty CheckConstraint = "users_email_not_empty" // users
CheckUsersServiceAccountLoginType CheckConstraint = "users_service_account_login_type" // users
CheckUsersUsernameMinLength CheckConstraint = "users_username_min_length" // users
CheckOrganizationIDNotZero CheckConstraint = "organization_id_not_zero" // custom_roles
CheckGroupAIBudgetsSpendLimitMicrosCheck CheckConstraint = "group_ai_budgets_spend_limit_micros_check" // group_ai_budgets
CheckGroupsChatSpendLimitMicrosCheck CheckConstraint = "groups_chat_spend_limit_micros_check" // groups
CheckMcpServerConfigsAuthTypeCheck CheckConstraint = "mcp_server_configs_auth_type_check" // mcp_server_configs
CheckMcpServerConfigsAvailabilityCheck CheckConstraint = "mcp_server_configs_availability_check" // mcp_server_configs
CheckMcpServerConfigsTransportCheck CheckConstraint = "mcp_server_configs_transport_check" // mcp_server_configs
CheckMaxProvisionerLogsLength CheckConstraint = "max_provisioner_logs_length" // provisioner_jobs
CheckNatsPortValidTcp CheckConstraint = "nats_port_valid_tcp" // replicas
CheckMaxLogsLength CheckConstraint = "max_logs_length" // workspace_agents
CheckSubsystemsNotNone CheckConstraint = "subsystems_not_none" // workspace_agents
CheckWorkspaceBuildsDeadlineBelowMaxDeadline CheckConstraint = "workspace_builds_deadline_below_max_deadline" // workspace_builds
CheckGroupAclIsObject CheckConstraint = "group_acl_is_object" // workspaces
CheckUserAclIsObject CheckConstraint = "user_acl_is_object" // workspaces
CheckTelemetryLockEventTypeConstraint CheckConstraint = "telemetry_lock_event_type_constraint" // telemetry_locks
CheckValidationMonotonicOrder CheckConstraint = "validation_monotonic_order" // template_version_parameters
CheckUsageEventTypeCheck CheckConstraint = "usage_event_type_check" // usage_events
CheckUserAIBudgetOverridesSpendLimitMicrosCheck CheckConstraint = "user_ai_budget_overrides_spend_limit_micros_check" // user_ai_budget_overrides
CheckUserAIProviderKeysAPIKeyCheck CheckConstraint = "user_ai_provider_keys_api_key_check" // user_ai_provider_keys
CheckUserSkillsContentSize CheckConstraint = "user_skills_content_size" // user_skills
CheckUserSkillsDescriptionSize CheckConstraint = "user_skills_description_size" // user_skills
CheckUserSkillsNameFormat CheckConstraint = "user_skills_name_format" // user_skills
CheckUserSkillsNameSize CheckConstraint = "user_skills_name_size" // user_skills
CheckAIGatewayKeysHashedSecretCheck CheckConstraint = "ai_gateway_keys_hashed_secret_check" // ai_gateway_keys
CheckAIGatewayKeysNameCheck CheckConstraint = "ai_gateway_keys_name_check" // ai_gateway_keys
CheckAIGatewayKeysSecretPrefixCheck CheckConstraint = "ai_gateway_keys_secret_prefix_check" // ai_gateway_keys
CheckAIModelPricesCacheReadPriceCheck CheckConstraint = "ai_model_prices_cache_read_price_check" // ai_model_prices
CheckAIModelPricesCacheWritePriceCheck CheckConstraint = "ai_model_prices_cache_write_price_check" // ai_model_prices
CheckAIModelPricesInputPriceCheck CheckConstraint = "ai_model_prices_input_price_check" // ai_model_prices
CheckAIModelPricesOutputPriceCheck CheckConstraint = "ai_model_prices_output_price_check" // ai_model_prices
CheckAIProvidersNameCheck CheckConstraint = "ai_providers_name_check" // ai_providers
CheckAIUserDailySpendSpendMicrosCheck CheckConstraint = "ai_user_daily_spend_spend_micros_check" // ai_user_daily_spend
CheckAibridgeTokenUsagesCacheReadPriceMicrosCheck CheckConstraint = "aibridge_token_usages_cache_read_price_micros_check" // aibridge_token_usages
CheckAibridgeTokenUsagesCacheWritePriceMicrosCheck CheckConstraint = "aibridge_token_usages_cache_write_price_micros_check" // aibridge_token_usages
CheckAibridgeTokenUsagesCostMicrosCheck CheckConstraint = "aibridge_token_usages_cost_micros_check" // aibridge_token_usages
CheckAibridgeTokenUsagesInputPriceMicrosCheck CheckConstraint = "aibridge_token_usages_input_price_micros_check" // aibridge_token_usages
CheckAibridgeTokenUsagesOutputPriceMicrosCheck CheckConstraint = "aibridge_token_usages_output_price_micros_check" // aibridge_token_usages
CheckAPIKeysAllowListNotEmpty CheckConstraint = "api_keys_allow_list_not_empty" // api_keys
CheckBoundaryLogsSequenceNumberCheck CheckConstraint = "boundary_logs_sequence_number_check" // boundary_logs
CheckChatModelConfigsAIProviderRequiredWhenActive CheckConstraint = "chat_model_configs_ai_provider_required_when_active" // chat_model_configs
CheckChatModelConfigsCompressionThresholdCheck CheckConstraint = "chat_model_configs_compression_threshold_check" // chat_model_configs
CheckChatModelConfigsContextLimitCheck CheckConstraint = "chat_model_configs_context_limit_check" // chat_model_configs
CheckChatUsageLimitConfigDefaultLimitMicrosCheck CheckConstraint = "chat_usage_limit_config_default_limit_micros_check" // chat_usage_limit_config
CheckChatUsageLimitConfigPeriodCheck CheckConstraint = "chat_usage_limit_config_period_check" // chat_usage_limit_config
CheckChatUsageLimitConfigSingletonCheck CheckConstraint = "chat_usage_limit_config_singleton_check" // chat_usage_limit_config
CheckChatAclOnlyOnRootChats CheckConstraint = "chat_acl_only_on_root_chats" // chats
CheckChatGroupAclNotNullJsonb CheckConstraint = "chat_group_acl_not_null_jsonb" // chats
CheckChatUserAclNotNullJsonb CheckConstraint = "chat_user_acl_not_null_jsonb" // chats
CheckChatsPinOrderArchivedCheck CheckConstraint = "chats_pin_order_archived_check" // chats
CheckChatsPinOrderParentCheck CheckConstraint = "chats_pin_order_parent_check" // chats
CheckOneTimePasscodeSet CheckConstraint = "one_time_passcode_set" // users
CheckUsersChatSpendLimitMicrosCheck CheckConstraint = "users_chat_spend_limit_micros_check" // users
CheckUsersEmailNotEmpty CheckConstraint = "users_email_not_empty" // users
CheckUsersServiceAccountLoginType CheckConstraint = "users_service_account_login_type" // users
CheckUsersUsernameMinLength CheckConstraint = "users_username_min_length" // users
CheckOrganizationIDNotZero CheckConstraint = "organization_id_not_zero" // custom_roles
CheckGroupAIBudgetsSpendLimitMicrosCheck CheckConstraint = "group_ai_budgets_spend_limit_micros_check" // group_ai_budgets
CheckGroupsChatSpendLimitMicrosCheck CheckConstraint = "groups_chat_spend_limit_micros_check" // groups
CheckMcpServerConfigsAuthTypeCheck CheckConstraint = "mcp_server_configs_auth_type_check" // mcp_server_configs
CheckMcpServerConfigsAvailabilityCheck CheckConstraint = "mcp_server_configs_availability_check" // mcp_server_configs
CheckMcpServerConfigsTransportCheck CheckConstraint = "mcp_server_configs_transport_check" // mcp_server_configs
CheckMaxProvisionerLogsLength CheckConstraint = "max_provisioner_logs_length" // provisioner_jobs
CheckNatsPortValidTcp CheckConstraint = "nats_port_valid_tcp" // replicas
CheckMaxLogsLength CheckConstraint = "max_logs_length" // workspace_agents
CheckSubsystemsNotNone CheckConstraint = "subsystems_not_none" // workspace_agents
CheckWorkspaceBuildsDeadlineBelowMaxDeadline CheckConstraint = "workspace_builds_deadline_below_max_deadline" // workspace_builds
CheckGroupAclIsObject CheckConstraint = "group_acl_is_object" // workspaces
CheckUserAclIsObject CheckConstraint = "user_acl_is_object" // workspaces
CheckTelemetryLockEventTypeConstraint CheckConstraint = "telemetry_lock_event_type_constraint" // telemetry_locks
CheckValidationMonotonicOrder CheckConstraint = "validation_monotonic_order" // template_version_parameters
CheckUsageEventTypeCheck CheckConstraint = "usage_event_type_check" // usage_events
CheckUserAIBudgetOverridesSpendLimitMicrosCheck CheckConstraint = "user_ai_budget_overrides_spend_limit_micros_check" // user_ai_budget_overrides
CheckUserAIProviderKeysAPIKeyCheck CheckConstraint = "user_ai_provider_keys_api_key_check" // user_ai_provider_keys
CheckUserSkillsContentSize CheckConstraint = "user_skills_content_size" // user_skills
CheckUserSkillsDescriptionSize CheckConstraint = "user_skills_description_size" // user_skills
CheckUserSkillsNameFormat CheckConstraint = "user_skills_name_format" // user_skills
CheckUserSkillsNameSize CheckConstraint = "user_skills_name_size" // user_skills
CheckWorkspaceBuildOrchestrationsAttemptCountCheck CheckConstraint = "workspace_build_orchestrations_attempt_count_check" // workspace_build_orchestrations
CheckWorkspaceBuildOrchestrationsChildLogLevelCheck CheckConstraint = "workspace_build_orchestrations_child_log_level_check" // workspace_build_orchestrations
CheckWorkspaceBuildOrchestrationsChildParametersCheck CheckConstraint = "workspace_build_orchestrations_child_parameters_check" // workspace_build_orchestrations
CheckWorkspaceBuildOrchestrationsChildPresetVersionCheck CheckConstraint = "workspace_build_orchestrations_child_preset_version_check" // workspace_build_orchestrations
CheckWorkspaceBuildOrchestrationsCompletedChildCheck CheckConstraint = "workspace_build_orchestrations_completed_child_check" // workspace_build_orchestrations
CheckWorkspaceBuildOrchestrationsNextRetryAfterCheck CheckConstraint = "workspace_build_orchestrations_next_retry_after_check" // workspace_build_orchestrations
CheckWorkspaceBuildOrchestrationsStatusCheck CheckConstraint = "workspace_build_orchestrations_status_check" // workspace_build_orchestrations
)
+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{
+56
View File
@@ -793,6 +793,14 @@ func (m queryMetricsStore) DeleteOldWorkspaceAgentStats(ctx context.Context) err
return r0
}
func (m queryMetricsStore) DeleteOldWorkspaceBuildOrchestrations(ctx context.Context, arg database.DeleteOldWorkspaceBuildOrchestrationsParams) (int64, error) {
start := time.Now()
r0, r1 := m.s.DeleteOldWorkspaceBuildOrchestrations(ctx, arg)
m.queryLatencies.WithLabelValues("DeleteOldWorkspaceBuildOrchestrations").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteOldWorkspaceBuildOrchestrations").Inc()
return r0, r1
}
func (m queryMetricsStore) DeleteOrganizationMember(ctx context.Context, arg database.DeleteOrganizationMemberParams) error {
start := time.Now()
r0 := m.s.DeleteOrganizationMember(ctx, arg)
@@ -2337,6 +2345,14 @@ func (m queryMetricsStore) GetMCPServerUserTokensByUserID(ctx context.Context, u
return r0, r1
}
func (m queryMetricsStore) GetNextPendingWorkspaceBuildOrchestrationForUpdate(ctx context.Context) (database.WorkspaceBuildOrchestration, error) {
start := time.Now()
r0, r1 := m.s.GetNextPendingWorkspaceBuildOrchestrationForUpdate(ctx)
m.queryLatencies.WithLabelValues("GetNextPendingWorkspaceBuildOrchestrationForUpdate").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetNextPendingWorkspaceBuildOrchestrationForUpdate").Inc()
return r0, r1
}
func (m queryMetricsStore) GetNotificationMessagesByStatus(ctx context.Context, arg database.GetNotificationMessagesByStatusParams) ([]database.NotificationMessage, error) {
start := time.Now()
r0, r1 := m.s.GetNotificationMessagesByStatus(ctx, arg)
@@ -4529,6 +4545,14 @@ func (m queryMetricsStore) InsertWorkspaceBuild(ctx context.Context, arg databas
return r0
}
func (m queryMetricsStore) InsertWorkspaceBuildOrchestration(ctx context.Context, arg database.InsertWorkspaceBuildOrchestrationParams) (database.WorkspaceBuildOrchestration, error) {
start := time.Now()
r0, r1 := m.s.InsertWorkspaceBuildOrchestration(ctx, arg)
m.queryLatencies.WithLabelValues("InsertWorkspaceBuildOrchestration").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertWorkspaceBuildOrchestration").Inc()
return r0, r1
}
func (m queryMetricsStore) InsertWorkspaceBuildParameters(ctx context.Context, arg database.InsertWorkspaceBuildParametersParams) error {
start := time.Now()
r0 := m.s.InsertWorkspaceBuildParameters(ctx, arg)
@@ -5953,6 +5977,38 @@ func (m queryMetricsStore) UpdateWorkspaceBuildNotifiedAutostopDeadline(ctx cont
return r0
}
func (m queryMetricsStore) UpdateWorkspaceBuildOrchestrationCanceledByID(ctx context.Context, arg database.UpdateWorkspaceBuildOrchestrationCanceledByIDParams) (database.WorkspaceBuildOrchestration, error) {
start := time.Now()
r0, r1 := m.s.UpdateWorkspaceBuildOrchestrationCanceledByID(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateWorkspaceBuildOrchestrationCanceledByID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateWorkspaceBuildOrchestrationCanceledByID").Inc()
return r0, r1
}
func (m queryMetricsStore) UpdateWorkspaceBuildOrchestrationCompletedByID(ctx context.Context, arg database.UpdateWorkspaceBuildOrchestrationCompletedByIDParams) (database.WorkspaceBuildOrchestration, error) {
start := time.Now()
r0, r1 := m.s.UpdateWorkspaceBuildOrchestrationCompletedByID(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateWorkspaceBuildOrchestrationCompletedByID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateWorkspaceBuildOrchestrationCompletedByID").Inc()
return r0, r1
}
func (m queryMetricsStore) UpdateWorkspaceBuildOrchestrationFailedByID(ctx context.Context, arg database.UpdateWorkspaceBuildOrchestrationFailedByIDParams) (database.WorkspaceBuildOrchestration, error) {
start := time.Now()
r0, r1 := m.s.UpdateWorkspaceBuildOrchestrationFailedByID(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateWorkspaceBuildOrchestrationFailedByID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateWorkspaceBuildOrchestrationFailedByID").Inc()
return r0, r1
}
func (m queryMetricsStore) UpdateWorkspaceBuildOrchestrationRetryByID(ctx context.Context, arg database.UpdateWorkspaceBuildOrchestrationRetryByIDParams) (database.WorkspaceBuildOrchestration, error) {
start := time.Now()
r0, r1 := m.s.UpdateWorkspaceBuildOrchestrationRetryByID(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateWorkspaceBuildOrchestrationRetryByID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateWorkspaceBuildOrchestrationRetryByID").Inc()
return r0, r1
}
func (m queryMetricsStore) UpdateWorkspaceBuildProvisionerStateByID(ctx context.Context, arg database.UpdateWorkspaceBuildProvisionerStateByIDParams) error {
start := time.Now()
r0 := m.s.UpdateWorkspaceBuildProvisionerStateByID(ctx, arg)
+105
View File
@@ -1337,6 +1337,21 @@ func (mr *MockStoreMockRecorder) DeleteOldWorkspaceAgentStats(ctx any) *gomock.C
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldWorkspaceAgentStats", reflect.TypeOf((*MockStore)(nil).DeleteOldWorkspaceAgentStats), ctx)
}
// DeleteOldWorkspaceBuildOrchestrations mocks base method.
func (m *MockStore) DeleteOldWorkspaceBuildOrchestrations(ctx context.Context, arg database.DeleteOldWorkspaceBuildOrchestrationsParams) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteOldWorkspaceBuildOrchestrations", ctx, arg)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteOldWorkspaceBuildOrchestrations indicates an expected call of DeleteOldWorkspaceBuildOrchestrations.
func (mr *MockStoreMockRecorder) DeleteOldWorkspaceBuildOrchestrations(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldWorkspaceBuildOrchestrations", reflect.TypeOf((*MockStore)(nil).DeleteOldWorkspaceBuildOrchestrations), ctx, arg)
}
// DeleteOrganizationMember mocks base method.
func (m *MockStore) DeleteOrganizationMember(ctx context.Context, arg database.DeleteOrganizationMemberParams) error {
m.ctrl.T.Helper()
@@ -4333,6 +4348,21 @@ func (mr *MockStoreMockRecorder) GetMCPServerUserTokensByUserID(ctx, userID any)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMCPServerUserTokensByUserID", reflect.TypeOf((*MockStore)(nil).GetMCPServerUserTokensByUserID), ctx, userID)
}
// GetNextPendingWorkspaceBuildOrchestrationForUpdate mocks base method.
func (m *MockStore) GetNextPendingWorkspaceBuildOrchestrationForUpdate(ctx context.Context) (database.WorkspaceBuildOrchestration, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetNextPendingWorkspaceBuildOrchestrationForUpdate", ctx)
ret0, _ := ret[0].(database.WorkspaceBuildOrchestration)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetNextPendingWorkspaceBuildOrchestrationForUpdate indicates an expected call of GetNextPendingWorkspaceBuildOrchestrationForUpdate.
func (mr *MockStoreMockRecorder) GetNextPendingWorkspaceBuildOrchestrationForUpdate(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNextPendingWorkspaceBuildOrchestrationForUpdate", reflect.TypeOf((*MockStore)(nil).GetNextPendingWorkspaceBuildOrchestrationForUpdate), ctx)
}
// GetNotificationMessagesByStatus mocks base method.
func (m *MockStore) GetNotificationMessagesByStatus(ctx context.Context, arg database.GetNotificationMessagesByStatusParams) ([]database.NotificationMessage, error) {
m.ctrl.T.Helper()
@@ -8471,6 +8501,21 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceBuild(ctx, arg any) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceBuild", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceBuild), ctx, arg)
}
// InsertWorkspaceBuildOrchestration mocks base method.
func (m *MockStore) InsertWorkspaceBuildOrchestration(ctx context.Context, arg database.InsertWorkspaceBuildOrchestrationParams) (database.WorkspaceBuildOrchestration, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertWorkspaceBuildOrchestration", ctx, arg)
ret0, _ := ret[0].(database.WorkspaceBuildOrchestration)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertWorkspaceBuildOrchestration indicates an expected call of InsertWorkspaceBuildOrchestration.
func (mr *MockStoreMockRecorder) InsertWorkspaceBuildOrchestration(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceBuildOrchestration", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceBuildOrchestration), ctx, arg)
}
// InsertWorkspaceBuildParameters mocks base method.
func (m *MockStore) InsertWorkspaceBuildParameters(ctx context.Context, arg database.InsertWorkspaceBuildParametersParams) error {
m.ctrl.T.Helper()
@@ -11166,6 +11211,66 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceBuildNotifiedAutostopDeadline(ct
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceBuildNotifiedAutostopDeadline", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceBuildNotifiedAutostopDeadline), ctx, arg)
}
// UpdateWorkspaceBuildOrchestrationCanceledByID mocks base method.
func (m *MockStore) UpdateWorkspaceBuildOrchestrationCanceledByID(ctx context.Context, arg database.UpdateWorkspaceBuildOrchestrationCanceledByIDParams) (database.WorkspaceBuildOrchestration, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateWorkspaceBuildOrchestrationCanceledByID", ctx, arg)
ret0, _ := ret[0].(database.WorkspaceBuildOrchestration)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateWorkspaceBuildOrchestrationCanceledByID indicates an expected call of UpdateWorkspaceBuildOrchestrationCanceledByID.
func (mr *MockStoreMockRecorder) UpdateWorkspaceBuildOrchestrationCanceledByID(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceBuildOrchestrationCanceledByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceBuildOrchestrationCanceledByID), ctx, arg)
}
// UpdateWorkspaceBuildOrchestrationCompletedByID mocks base method.
func (m *MockStore) UpdateWorkspaceBuildOrchestrationCompletedByID(ctx context.Context, arg database.UpdateWorkspaceBuildOrchestrationCompletedByIDParams) (database.WorkspaceBuildOrchestration, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateWorkspaceBuildOrchestrationCompletedByID", ctx, arg)
ret0, _ := ret[0].(database.WorkspaceBuildOrchestration)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateWorkspaceBuildOrchestrationCompletedByID indicates an expected call of UpdateWorkspaceBuildOrchestrationCompletedByID.
func (mr *MockStoreMockRecorder) UpdateWorkspaceBuildOrchestrationCompletedByID(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceBuildOrchestrationCompletedByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceBuildOrchestrationCompletedByID), ctx, arg)
}
// UpdateWorkspaceBuildOrchestrationFailedByID mocks base method.
func (m *MockStore) UpdateWorkspaceBuildOrchestrationFailedByID(ctx context.Context, arg database.UpdateWorkspaceBuildOrchestrationFailedByIDParams) (database.WorkspaceBuildOrchestration, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateWorkspaceBuildOrchestrationFailedByID", ctx, arg)
ret0, _ := ret[0].(database.WorkspaceBuildOrchestration)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateWorkspaceBuildOrchestrationFailedByID indicates an expected call of UpdateWorkspaceBuildOrchestrationFailedByID.
func (mr *MockStoreMockRecorder) UpdateWorkspaceBuildOrchestrationFailedByID(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceBuildOrchestrationFailedByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceBuildOrchestrationFailedByID), ctx, arg)
}
// UpdateWorkspaceBuildOrchestrationRetryByID mocks base method.
func (m *MockStore) UpdateWorkspaceBuildOrchestrationRetryByID(ctx context.Context, arg database.UpdateWorkspaceBuildOrchestrationRetryByIDParams) (database.WorkspaceBuildOrchestration, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateWorkspaceBuildOrchestrationRetryByID", ctx, arg)
ret0, _ := ret[0].(database.WorkspaceBuildOrchestration)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateWorkspaceBuildOrchestrationRetryByID indicates an expected call of UpdateWorkspaceBuildOrchestrationRetryByID.
func (mr *MockStoreMockRecorder) UpdateWorkspaceBuildOrchestrationRetryByID(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceBuildOrchestrationRetryByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceBuildOrchestrationRetryByID), ctx, arg)
}
// UpdateWorkspaceBuildProvisionerStateByID mocks base method.
func (m *MockStore) UpdateWorkspaceBuildProvisionerStateByID(ctx context.Context, arg database.UpdateWorkspaceBuildProvisionerStateByIDParams) error {
m.ctrl.T.Helper()
+16
View File
@@ -39,6 +39,11 @@ const (
// long enough to cover the maximum interval of a heartbeat event (currently
// 1 hour) plus some buffer.
maxTelemetryHeartbeatAge = 24 * time.Hour
// Operational handoff state; terminal rows are kept for debugging, then
// purged.
workspaceBuildOrchestrationTerminalRetention = 24 * time.Hour
// Batch size for workspace build orchestration deletion.
workspaceBuildOrchestrationsBatchSize = 10000
// Chat and chat file batch sizes stay smaller than audit/connection
// log batches because chat_files rows carry bytea blobs.
chatsBatchSize = 1000
@@ -275,6 +280,15 @@ func (i *instance) purgeTick(ctx context.Context, db database.Store, start time.
}
}
deleteOldWorkspaceBuildOrchestrationsBefore := start.Add(-workspaceBuildOrchestrationTerminalRetention)
purgedWorkspaceBuildOrchestrations, err := tx.DeleteOldWorkspaceBuildOrchestrations(ctx, database.DeleteOldWorkspaceBuildOrchestrationsParams{
BeforeTime: deleteOldWorkspaceBuildOrchestrationsBefore,
LimitCount: workspaceBuildOrchestrationsBatchSize,
})
if err != nil {
return xerrors.Errorf("failed to delete old workspace build orchestrations: %w", err)
}
var purgedChats, purgedChatFiles, purgedChatDebugRuns int64
if purgeChats {
purgedChats, purgedChatFiles, err = i.purgeChatsInTx(ctx, tx, start, chatRetentionDays)
@@ -304,6 +318,7 @@ func (i *instance) purgeTick(ctx context.Context, db database.Store, start time.
slog.F("audit_logs", purgedAuditLogs),
slog.F("boundary_logs", purgedBoundaryLogs),
slog.F("boundary_sessions", purgedBoundarySessions),
slog.F("workspace_build_orchestrations", purgedWorkspaceBuildOrchestrations),
slog.F("chats", purgedChats),
slog.F("chat_files", purgedChatFiles),
slog.F("chat_debug_runs", purgedChatDebugRuns),
@@ -318,6 +333,7 @@ func (i *instance) purgeTick(ctx context.Context, db database.Store, start time.
i.recordsPurged.WithLabelValues("audit_logs").Add(float64(purgedAuditLogs))
i.recordsPurged.WithLabelValues("boundary_logs").Add(float64(purgedBoundaryLogs))
i.recordsPurged.WithLabelValues("boundary_sessions").Add(float64(purgedBoundarySessions))
i.recordsPurged.WithLabelValues("workspace_build_orchestrations").Add(float64(purgedWorkspaceBuildOrchestrations))
i.recordsPurged.WithLabelValues("chats").Add(float64(purgedChats))
i.recordsPurged.WithLabelValues("chat_debug_runs").Add(float64(purgedChatDebugRuns))
i.recordsPurged.WithLabelValues("chat_files").Add(float64(purgedChatFiles))
+147
View File
@@ -129,6 +129,11 @@ func TestMetrics(t *testing.T) {
})
require.GreaterOrEqual(t, auditLogs, 0)
workspaceBuildOrchestrations := promhelp.CounterValue(t, reg, "coderd_dbpurge_records_purged_total", prometheus.Labels{
"record_type": "workspace_build_orchestrations",
})
require.GreaterOrEqual(t, workspaceBuildOrchestrations, 0)
chats := promhelp.CounterValue(t, reg, "coderd_dbpurge_records_purged_total", prometheus.Labels{
"record_type": "chats",
})
@@ -247,6 +252,7 @@ func TestMetrics(t *testing.T) {
mDB.EXPECT().DeleteOldNotificationMessages(gomock.Any()).Return(nil).AnyTimes()
mDB.EXPECT().ExpirePrebuildsAPIKeys(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mDB.EXPECT().DeleteOldTelemetryLocks(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mDB.EXPECT().DeleteOldWorkspaceBuildOrchestrations(gomock.Any(), gomock.Any()).Return(int64(0), nil).AnyTimes()
mDB.EXPECT().DeleteOldAuditLogConnectionEvents(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mDB.EXPECT().DeleteOldChatDebugRuns(gomock.Any(), gomock.AssignableToTypeOf(database.DeleteOldChatDebugRunsParams{})).Return(int64(0), nil).MinTimes(1)
mDB.EXPECT().InTx(gomock.Any(), database.DefaultTXOptions().WithID("db_purge")).
@@ -297,6 +303,7 @@ func TestMetrics(t *testing.T) {
mDB.EXPECT().DeleteOldNotificationMessages(gomock.Any()).Return(nil).AnyTimes()
mDB.EXPECT().ExpirePrebuildsAPIKeys(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mDB.EXPECT().DeleteOldTelemetryLocks(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mDB.EXPECT().DeleteOldWorkspaceBuildOrchestrations(gomock.Any(), gomock.Any()).Return(int64(0), nil).AnyTimes()
mDB.EXPECT().DeleteOldAuditLogConnectionEvents(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mDB.EXPECT().DeleteOldChats(gomock.Any(), gomock.AssignableToTypeOf(database.DeleteOldChatsParams{})).Return(int64(0), nil).MinTimes(1)
mDB.EXPECT().DeleteOldChatFiles(gomock.Any(), gomock.AssignableToTypeOf(database.DeleteOldChatFilesParams{})).Return(int64(0), nil).MinTimes(1)
@@ -1117,6 +1124,146 @@ func TestDeleteOldTelemetryHeartbeats(t *testing.T) {
}, testutil.WaitShort, testutil.IntervalFast, "it should delete old telemetry heartbeats")
}
func TestDeleteOldWorkspaceBuildOrchestrations(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
db, _, rawDB := dbtestutil.NewDBWithSQLDB(t)
org := dbgen.Organization(t, db, database.Organization{})
user := dbgen.User(t, db, database.User{})
versionJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
Type: database.ProvisionerJobTypeTemplateVersionImport,
})
version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
OrganizationID: org.ID,
JobID: versionJob.ID,
CreatedBy: user.ID,
})
template := dbgen.Template(t, db, database.Template{
OrganizationID: org.ID,
ActiveVersionID: version.ID,
CreatedBy: user.ID,
})
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
OwnerID: user.ID,
OrganizationID: org.ID,
TemplateID: template.ID,
})
now := dbtime.Now()
cutoff := now.Add(-24 * time.Hour)
buildTime := cutoff.Add(-time.Hour)
oldCompletedTime := cutoff.Add(-3 * time.Minute)
oldFailedTime := cutoff.Add(-2 * time.Minute)
oldCanceledTime := cutoff.Add(-time.Minute)
oldPendingTime := cutoff.Add(-time.Minute)
recentTime := cutoff.Add(time.Minute)
createBuild := func(buildNumber int32, createdAt time.Time) database.WorkspaceBuild {
return mustCreateWorkspaceBuild(t, db, org, version, workspace.ID, createdAt, buildNumber)
}
insertOrchestration := func(parentBuild database.WorkspaceBuild, updatedAt time.Time) database.WorkspaceBuildOrchestration {
orchestration, err := db.InsertWorkspaceBuildOrchestration(ctx, database.InsertWorkspaceBuildOrchestrationParams{
ID: uuid.New(),
CreatedAt: updatedAt,
UpdatedAt: updatedAt,
ParentBuildID: parentBuild.ID,
ChildTransition: database.WorkspaceTransitionStart,
ChildRichParameterValues: json.RawMessage("[]"),
})
require.NoError(t, err)
return orchestration
}
// Given: old terminal orchestration rows (completed, failed,
// canceled), an old pending row, and a recent terminal row.
oldCompletedParent := createBuild(1, buildTime)
oldCompletedChild := createBuild(2, buildTime)
oldCompleted := insertOrchestration(oldCompletedParent, oldCompletedTime)
_, err := db.UpdateWorkspaceBuildOrchestrationCompletedByID(ctx, database.UpdateWorkspaceBuildOrchestrationCompletedByIDParams{
ID: oldCompleted.ID,
ChildBuildID: uuid.NullUUID{UUID: oldCompletedChild.ID, Valid: true},
UpdatedAt: oldCompletedTime,
})
require.NoError(t, err)
oldFailedParent := createBuild(3, buildTime)
oldFailed := insertOrchestration(oldFailedParent, oldFailedTime)
_, err = db.UpdateWorkspaceBuildOrchestrationFailedByID(ctx, database.UpdateWorkspaceBuildOrchestrationFailedByIDParams{
ID: oldFailed.ID,
Error: sql.NullString{String: "failed", Valid: true},
UpdatedAt: oldFailedTime,
})
require.NoError(t, err)
oldCanceledParent := createBuild(4, buildTime)
oldCanceled := insertOrchestration(oldCanceledParent, oldCanceledTime)
_, err = db.UpdateWorkspaceBuildOrchestrationCanceledByID(ctx, database.UpdateWorkspaceBuildOrchestrationCanceledByIDParams{
ID: oldCanceled.ID,
UpdatedAt: oldCanceledTime,
})
require.NoError(t, err)
oldPendingParent := createBuild(5, buildTime)
oldPending := insertOrchestration(oldPendingParent, oldPendingTime)
recentCompletedParent := createBuild(6, buildTime)
recentCompletedChild := createBuild(7, buildTime)
recentCompleted := insertOrchestration(recentCompletedParent, recentTime)
_, err = db.UpdateWorkspaceBuildOrchestrationCompletedByID(ctx, database.UpdateWorkspaceBuildOrchestrationCompletedByIDParams{
ID: recentCompleted.ID,
ChildBuildID: uuid.NullUUID{UUID: recentCompletedChild.ID, Valid: true},
UpdatedAt: recentTime,
})
require.NoError(t, err)
// When: old workspace build orchestrations are deleted with LimitCount 1
deleted, err := db.DeleteOldWorkspaceBuildOrchestrations(ctx, database.DeleteOldWorkspaceBuildOrchestrationsParams{
BeforeTime: cutoff,
LimitCount: 1,
})
require.NoError(t, err)
require.EqualValues(t, 1, deleted)
// Then: only the oldest terminal row is deleted.
assertOrchestrationDeleted(ctx, t, rawDB, oldCompletedParent.ID)
assertOrchestrationExists(ctx, t, rawDB, oldFailedParent.ID, oldFailed.ID)
assertOrchestrationExists(ctx, t, rawDB, oldCanceledParent.ID, oldCanceled.ID)
assertOrchestrationExists(ctx, t, rawDB, oldPendingParent.ID, oldPending.ID)
assertOrchestrationExists(ctx, t, rawDB, recentCompletedParent.ID, recentCompleted.ID)
// When: old workspace build orchestrations are deleted again.
deleted, err = db.DeleteOldWorkspaceBuildOrchestrations(ctx, database.DeleteOldWorkspaceBuildOrchestrationsParams{
BeforeTime: cutoff,
LimitCount: 10,
})
require.NoError(t, err)
require.EqualValues(t, 2, deleted)
// Then: the remaining old terminal rows are deleted.
assertOrchestrationDeleted(ctx, t, rawDB, oldFailedParent.ID)
assertOrchestrationDeleted(ctx, t, rawDB, oldCanceledParent.ID)
assertOrchestrationExists(ctx, t, rawDB, oldPendingParent.ID, oldPending.ID)
assertOrchestrationExists(ctx, t, rawDB, recentCompletedParent.ID, recentCompleted.ID)
}
func assertOrchestrationDeleted(ctx context.Context, t *testing.T, rawDB *sql.DB, parentBuildID uuid.UUID) {
t.Helper()
_, err := dbtestutil.GetWorkspaceBuildOrchestrationByParentBuildID(ctx, rawDB, parentBuildID)
require.ErrorIs(t, err, sql.ErrNoRows)
}
func assertOrchestrationExists(ctx context.Context, t *testing.T, rawDB *sql.DB, parentBuildID uuid.UUID, orchestrationID uuid.UUID) {
t.Helper()
orchestration, err := dbtestutil.GetWorkspaceBuildOrchestrationByParentBuildID(ctx, rawDB, parentBuildID)
require.NoError(t, err)
require.Equal(t, orchestrationID, orchestration.ID)
}
func TestDeleteOldConnectionLogs(t *testing.T) {
t.Parallel()
@@ -0,0 +1,32 @@
package dbtestutil
import (
"context"
"database/sql"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/coder/coder/v2/coderd/database"
)
// GetWorkspaceBuildOrchestrationByParentBuildID reads a workspace
// build orchestration row directly from the database for tests.
//
// It scans into the struct by column name so new columns are picked up
// automatically without updating this helper.
func GetWorkspaceBuildOrchestrationByParentBuildID(
ctx context.Context,
sqlDB *sql.DB,
parentBuildID uuid.UUID,
) (database.WorkspaceBuildOrchestration, error) {
db := sqlx.NewDb(sqlDB, "postgres")
var orchestration database.WorkspaceBuildOrchestration
err := db.GetContext(
ctx,
&orchestration,
`SELECT * FROM workspace_build_orchestrations WHERE parent_build_id = $1`,
parentBuildID,
)
return orchestration, err
}
+76 -1
View File
@@ -258,7 +258,12 @@ CREATE TYPE api_key_scope AS ENUM (
'ai_gateway_key:create',
'ai_gateway_key:delete',
'ai_gateway_key:read',
'ai_gateway_key:update'
'ai_gateway_key:update',
'workspace_build_orchestration:*',
'workspace_build_orchestration:create',
'workspace_build_orchestration:delete',
'workspace_build_orchestration:read',
'workspace_build_orchestration:update'
);
CREATE TYPE app_sharing_level AS ENUM (
@@ -3868,6 +3873,44 @@ CREATE TABLE workspace_app_statuses (
uri text
);
CREATE TABLE workspace_build_orchestrations (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
workspace_id uuid NOT NULL,
parent_build_id uuid NOT NULL,
child_build_id uuid,
child_transition workspace_transition NOT NULL,
child_template_version_id uuid,
child_template_version_preset_id uuid,
child_rich_parameter_values jsonb DEFAULT '[]'::jsonb NOT NULL,
child_log_level text DEFAULT ''::text NOT NULL,
child_reason build_reason,
attempt_count integer DEFAULT 0 NOT NULL,
next_retry_after timestamp with time zone,
status text DEFAULT 'pending'::text NOT NULL,
error text,
CONSTRAINT workspace_build_orchestrations_attempt_count_check CHECK ((attempt_count >= 0)),
CONSTRAINT workspace_build_orchestrations_child_log_level_check CHECK ((child_log_level = ANY (ARRAY[''::text, 'debug'::text]))),
CONSTRAINT workspace_build_orchestrations_child_parameters_check CHECK ((jsonb_typeof(child_rich_parameter_values) = 'array'::text)),
CONSTRAINT workspace_build_orchestrations_child_preset_version_check CHECK (((child_template_version_preset_id IS NULL) OR (child_template_version_id IS NOT NULL))),
CONSTRAINT workspace_build_orchestrations_completed_child_check CHECK (((status <> 'completed'::text) OR (child_build_id IS NOT NULL))),
CONSTRAINT workspace_build_orchestrations_next_retry_after_check CHECK (((status = 'pending'::text) OR (next_retry_after IS NULL))),
CONSTRAINT workspace_build_orchestrations_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'completed'::text, 'failed'::text, 'canceled'::text])))
);
COMMENT ON TABLE workspace_build_orchestrations IS 'Tracks durable follow-up workspace build operations, such as server-side restart, where one child build is created after a parent build completes successfully.';
COMMENT ON COLUMN workspace_build_orchestrations.workspace_id IS 'Copied from the parent build so the database can enforce that parent and child builds belong to the same workspace.';
COMMENT ON COLUMN workspace_build_orchestrations.parent_build_id IS 'Unique because we only support sequences with one child build per parent build.';
COMMENT ON COLUMN workspace_build_orchestrations.child_build_id IS 'Nullable because the child build is created only after the parent build completes successfully.';
COMMENT ON COLUMN workspace_build_orchestrations.attempt_count IS 'Counts retryable child build creation failures for this orchestration row.';
COMMENT ON COLUMN workspace_build_orchestrations.next_retry_after IS 'When set, the orchestrator skips this pending row until the timestamp has passed.';
CREATE TABLE workspace_build_parameters (
workspace_build_id uuid NOT NULL,
name text NOT NULL,
@@ -4382,6 +4425,9 @@ ALTER TABLE ONLY template_version_preset_parameters
ALTER TABLE ONLY template_version_preset_prebuild_schedules
ADD CONSTRAINT template_version_preset_prebuild_schedules_pkey PRIMARY KEY (id);
ALTER TABLE ONLY template_version_presets
ADD CONSTRAINT template_version_presets_id_template_version_id_key UNIQUE (id, template_version_id);
ALTER TABLE ONLY template_version_presets
ADD CONSTRAINT template_version_presets_pkey PRIMARY KEY (id);
@@ -4499,9 +4545,21 @@ ALTER TABLE ONLY workspace_apps
ALTER TABLE ONLY workspace_apps
ADD CONSTRAINT workspace_apps_pkey PRIMARY KEY (id);
ALTER TABLE ONLY workspace_build_orchestrations
ADD CONSTRAINT workspace_build_orchestrations_child_build_id_key UNIQUE (child_build_id);
ALTER TABLE ONLY workspace_build_orchestrations
ADD CONSTRAINT workspace_build_orchestrations_parent_build_id_key UNIQUE (parent_build_id);
ALTER TABLE ONLY workspace_build_orchestrations
ADD CONSTRAINT workspace_build_orchestrations_pkey PRIMARY KEY (id);
ALTER TABLE ONLY workspace_build_parameters
ADD CONSTRAINT workspace_build_parameters_workspace_build_id_name_key UNIQUE (workspace_build_id, name);
ALTER TABLE ONLY workspace_builds
ADD CONSTRAINT workspace_builds_id_workspace_id_key UNIQUE (id, workspace_id);
ALTER TABLE ONLY workspace_builds
ADD CONSTRAINT workspace_builds_job_id_key UNIQUE (job_id);
@@ -4747,6 +4805,8 @@ CREATE UNIQUE INDEX idx_users_username ON users USING btree (username) WHERE (de
CREATE INDEX idx_workspace_app_statuses_workspace_id_created_at ON workspace_app_statuses USING btree (workspace_id, created_at DESC);
CREATE INDEX idx_workspace_build_orchestrations_pending ON workspace_build_orchestrations USING btree (created_at) WHERE (status = 'pending'::text);
CREATE INDEX idx_workspace_builds_initiator_id ON workspace_builds USING btree (initiator_id);
CREATE UNIQUE INDEX notification_messages_dedupe_hash_idx ON notification_messages USING btree (dedupe_hash);
@@ -5376,6 +5436,21 @@ ALTER TABLE ONLY workspace_app_statuses
ALTER TABLE ONLY workspace_apps
ADD CONSTRAINT workspace_apps_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_build_orchestrations
ADD CONSTRAINT workspace_build_orchestrations_child_build_workspace_id_fkey FOREIGN KEY (child_build_id, workspace_id) REFERENCES workspace_builds(id, workspace_id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_build_orchestrations
ADD CONSTRAINT workspace_build_orchestrations_child_preset_id_fkey FOREIGN KEY (child_template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE SET NULL;
ALTER TABLE ONLY workspace_build_orchestrations
ADD CONSTRAINT workspace_build_orchestrations_child_preset_version_fkey FOREIGN KEY (child_template_version_preset_id, child_template_version_id) REFERENCES template_version_presets(id, template_version_id);
ALTER TABLE ONLY workspace_build_orchestrations
ADD CONSTRAINT workspace_build_orchestrations_child_template_version_id_fkey FOREIGN KEY (child_template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_build_orchestrations
ADD CONSTRAINT workspace_build_orchestrations_parent_build_workspace_id_fkey FOREIGN KEY (parent_build_id, workspace_id) REFERENCES workspace_builds(id, workspace_id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_build_parameters
ADD CONSTRAINT workspace_build_parameters_workspace_build_id_fkey FOREIGN KEY (workspace_build_id) REFERENCES workspace_builds(id) ON DELETE CASCADE;
+5
View File
@@ -145,6 +145,11 @@ const (
ForeignKeyWorkspaceAppStatusesAppID ForeignKeyConstraint = "workspace_app_statuses_app_id_fkey" // ALTER TABLE ONLY workspace_app_statuses ADD CONSTRAINT workspace_app_statuses_app_id_fkey FOREIGN KEY (app_id) REFERENCES workspace_apps(id);
ForeignKeyWorkspaceAppStatusesWorkspaceID ForeignKeyConstraint = "workspace_app_statuses_workspace_id_fkey" // ALTER TABLE ONLY workspace_app_statuses ADD CONSTRAINT workspace_app_statuses_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id);
ForeignKeyWorkspaceAppsAgentID ForeignKeyConstraint = "workspace_apps_agent_id_fkey" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
ForeignKeyWorkspaceBuildOrchestrationsChildBuildWorkspaceID ForeignKeyConstraint = "workspace_build_orchestrations_child_build_workspace_id_fkey" // ALTER TABLE ONLY workspace_build_orchestrations ADD CONSTRAINT workspace_build_orchestrations_child_build_workspace_id_fkey FOREIGN KEY (child_build_id, workspace_id) REFERENCES workspace_builds(id, workspace_id) ON DELETE CASCADE;
ForeignKeyWorkspaceBuildOrchestrationsChildPresetID ForeignKeyConstraint = "workspace_build_orchestrations_child_preset_id_fkey" // ALTER TABLE ONLY workspace_build_orchestrations ADD CONSTRAINT workspace_build_orchestrations_child_preset_id_fkey FOREIGN KEY (child_template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE SET NULL;
ForeignKeyWorkspaceBuildOrchestrationsChildPresetVersion ForeignKeyConstraint = "workspace_build_orchestrations_child_preset_version_fkey" // ALTER TABLE ONLY workspace_build_orchestrations ADD CONSTRAINT workspace_build_orchestrations_child_preset_version_fkey FOREIGN KEY (child_template_version_preset_id, child_template_version_id) REFERENCES template_version_presets(id, template_version_id);
ForeignKeyWorkspaceBuildOrchestrationsChildTemplateVersionID ForeignKeyConstraint = "workspace_build_orchestrations_child_template_version_id_fkey" // ALTER TABLE ONLY workspace_build_orchestrations ADD CONSTRAINT workspace_build_orchestrations_child_template_version_id_fkey FOREIGN KEY (child_template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE;
ForeignKeyWorkspaceBuildOrchestrationsParentBuildWorkspaceID ForeignKeyConstraint = "workspace_build_orchestrations_parent_build_workspace_id_fkey" // ALTER TABLE ONLY workspace_build_orchestrations ADD CONSTRAINT workspace_build_orchestrations_parent_build_workspace_id_fkey FOREIGN KEY (parent_build_id, workspace_id) REFERENCES workspace_builds(id, workspace_id) ON DELETE CASCADE;
ForeignKeyWorkspaceBuildParametersWorkspaceBuildID ForeignKeyConstraint = "workspace_build_parameters_workspace_build_id_fkey" // ALTER TABLE ONLY workspace_build_parameters ADD CONSTRAINT workspace_build_parameters_workspace_build_id_fkey FOREIGN KEY (workspace_build_id) REFERENCES workspace_builds(id) ON DELETE CASCADE;
ForeignKeyWorkspaceBuildsJobID ForeignKeyConstraint = "workspace_builds_job_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE;
ForeignKeyWorkspaceBuildsTemplateVersionID ForeignKeyConstraint = "workspace_builds_template_version_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE;
@@ -0,0 +1,10 @@
-- Enum additions to api_key_scope are intentionally not reversed
-- because Postgres cannot drop enum values safely.
DROP TABLE IF EXISTS workspace_build_orchestrations;
ALTER TABLE template_version_presets
DROP CONSTRAINT IF EXISTS template_version_presets_id_template_version_id_key;
ALTER TABLE workspace_builds
DROP CONSTRAINT IF EXISTS workspace_builds_id_workspace_id_key;
@@ -0,0 +1,103 @@
-- Postgres requires the referenced column set of a composite foreign
-- key to have its own unique constraint, even though id is already
-- unique.
ALTER TABLE workspace_builds
ADD CONSTRAINT workspace_builds_id_workspace_id_key
UNIQUE (id, workspace_id);
ALTER TABLE template_version_presets
ADD CONSTRAINT template_version_presets_id_template_version_id_key
UNIQUE (id, template_version_id);
CREATE TABLE workspace_build_orchestrations (
id UUID PRIMARY KEY NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
workspace_id UUID NOT NULL,
parent_build_id UUID UNIQUE NOT NULL,
child_build_id UUID UNIQUE,
child_transition workspace_transition NOT NULL,
child_template_version_id UUID REFERENCES template_versions(id) ON DELETE CASCADE,
child_template_version_preset_id UUID, -- a constraint is added below
child_rich_parameter_values JSONB DEFAULT '[]'::JSONB NOT NULL,
child_log_level TEXT DEFAULT '' NOT NULL,
child_reason build_reason,
attempt_count INTEGER DEFAULT 0 NOT NULL,
next_retry_after TIMESTAMPTZ,
status TEXT DEFAULT 'pending' NOT NULL,
error TEXT,
CONSTRAINT workspace_build_orchestrations_status_check CHECK (
status IN ('pending', 'completed', 'failed', 'canceled')
),
CONSTRAINT workspace_build_orchestrations_completed_child_check CHECK (
status <> 'completed' OR child_build_id IS NOT NULL
),
CONSTRAINT workspace_build_orchestrations_child_parameters_check CHECK (
jsonb_typeof(child_rich_parameter_values) = 'array'
),
CONSTRAINT workspace_build_orchestrations_attempt_count_check CHECK (
attempt_count >= 0
),
CONSTRAINT workspace_build_orchestrations_next_retry_after_check CHECK (
status = 'pending' OR next_retry_after IS NULL
),
CONSTRAINT workspace_build_orchestrations_child_preset_version_check CHECK (
child_template_version_preset_id IS NULL OR child_template_version_id IS NOT NULL
),
-- Mirrors CreateWorkspaceBuildRequest validation, where the optional
-- log level is either unset or debug.
CONSTRAINT workspace_build_orchestrations_child_log_level_check CHECK (
child_log_level IN ('', 'debug')
),
-- These constraints enforce that any stored child preset belongs to
-- the requested child template version, while preset deletion still
-- clears only the preset column.
CONSTRAINT workspace_build_orchestrations_child_preset_id_fkey
FOREIGN KEY (child_template_version_preset_id)
REFERENCES template_version_presets(id)
ON DELETE SET NULL,
CONSTRAINT workspace_build_orchestrations_child_preset_version_fkey
FOREIGN KEY (child_template_version_preset_id, child_template_version_id)
REFERENCES template_version_presets(id, template_version_id),
-- Composite foreign keys enforce that the parent and child builds
-- belong to the same workspace.
CONSTRAINT workspace_build_orchestrations_parent_build_workspace_id_fkey
FOREIGN KEY (parent_build_id, workspace_id)
REFERENCES workspace_builds(id, workspace_id)
ON DELETE CASCADE,
CONSTRAINT workspace_build_orchestrations_child_build_workspace_id_fkey
FOREIGN KEY (child_build_id, workspace_id)
REFERENCES workspace_builds(id, workspace_id)
ON DELETE CASCADE
);
-- The orchestrator scans eligible pending rows oldest first and skips
-- terminal rows and retry rows whose delay has not elapsed.
CREATE INDEX idx_workspace_build_orchestrations_pending
ON workspace_build_orchestrations (created_at)
WHERE status = 'pending';
COMMENT ON TABLE workspace_build_orchestrations IS
'Tracks durable follow-up workspace build operations, such as server-side restart, where one child build is created after a parent build completes successfully.';
COMMENT ON COLUMN workspace_build_orchestrations.parent_build_id IS
'Unique because we only support sequences with one child build per parent build.';
COMMENT ON COLUMN workspace_build_orchestrations.workspace_id IS
'Copied from the parent build so the database can enforce that parent and child builds belong to the same workspace.';
COMMENT ON COLUMN workspace_build_orchestrations.child_build_id IS
'Nullable because the child build is created only after the parent build completes successfully.';
COMMENT ON COLUMN workspace_build_orchestrations.attempt_count IS
'Counts retryable child build creation failures for this orchestration row.';
COMMENT ON COLUMN workspace_build_orchestrations.next_retry_after IS
'When set, the orchestrator skips this pending row until the timestamp has passed.';
-- Add workspace_build_orchestration scopes for RBAC.
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_build_orchestration:*';
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_build_orchestration:create';
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_build_orchestration:delete';
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_build_orchestration:read';
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_build_orchestration:update';
@@ -0,0 +1,21 @@
INSERT INTO workspace_build_orchestrations (
id,
created_at,
updated_at,
workspace_id,
parent_build_id,
child_transition
)
SELECT
'4e983a68-9b8a-4d4e-a4d6-5f2dd73551c2'::uuid,
NOW(),
NOW(),
workspace_id,
id,
'start'::workspace_transition
FROM
workspace_builds
ORDER BY
created_at, id
LIMIT 1
ON CONFLICT DO NOTHING;
+41 -1
View File
@@ -387,6 +387,11 @@ const (
ApiKeyScopeAIGatewayKeyDelete APIKeyScope = "ai_gateway_key:delete"
ApiKeyScopeAIGatewayKeyRead APIKeyScope = "ai_gateway_key:read"
ApiKeyScopeAIGatewayKeyUpdate APIKeyScope = "ai_gateway_key:update"
ApiKeyScopeWorkspaceBuildOrchestration APIKeyScope = "workspace_build_orchestration:*"
ApiKeyScopeWorkspaceBuildOrchestrationCreate APIKeyScope = "workspace_build_orchestration:create"
ApiKeyScopeWorkspaceBuildOrchestrationDelete APIKeyScope = "workspace_build_orchestration:delete"
ApiKeyScopeWorkspaceBuildOrchestrationRead APIKeyScope = "workspace_build_orchestration:read"
ApiKeyScopeWorkspaceBuildOrchestrationUpdate APIKeyScope = "workspace_build_orchestration:update"
)
func (e *APIKeyScope) Scan(src interface{}) error {
@@ -656,7 +661,12 @@ func (e APIKeyScope) Valid() bool {
ApiKeyScopeAIGatewayKeyCreate,
ApiKeyScopeAIGatewayKeyDelete,
ApiKeyScopeAIGatewayKeyRead,
ApiKeyScopeAIGatewayKeyUpdate:
ApiKeyScopeAIGatewayKeyUpdate,
ApiKeyScopeWorkspaceBuildOrchestration,
ApiKeyScopeWorkspaceBuildOrchestrationCreate,
ApiKeyScopeWorkspaceBuildOrchestrationDelete,
ApiKeyScopeWorkspaceBuildOrchestrationRead,
ApiKeyScopeWorkspaceBuildOrchestrationUpdate:
return true
}
return false
@@ -895,6 +905,11 @@ func AllAPIKeyScopeValues() []APIKeyScope {
ApiKeyScopeAIGatewayKeyDelete,
ApiKeyScopeAIGatewayKeyRead,
ApiKeyScopeAIGatewayKeyUpdate,
ApiKeyScopeWorkspaceBuildOrchestration,
ApiKeyScopeWorkspaceBuildOrchestrationCreate,
ApiKeyScopeWorkspaceBuildOrchestrationDelete,
ApiKeyScopeWorkspaceBuildOrchestrationRead,
ApiKeyScopeWorkspaceBuildOrchestrationUpdate,
}
}
@@ -6461,6 +6476,31 @@ type WorkspaceBuild struct {
InitiatorByName string `db:"initiator_by_name" json:"initiator_by_name"`
}
// Tracks durable follow-up workspace build operations, such as server-side restart, where one child build is created after a parent build completes successfully.
type WorkspaceBuildOrchestration struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
// Copied from the parent build so the database can enforce that parent and child builds belong to the same workspace.
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
// Unique because we only support sequences with one child build per parent build.
ParentBuildID uuid.UUID `db:"parent_build_id" json:"parent_build_id"`
// Nullable because the child build is created only after the parent build completes successfully.
ChildBuildID uuid.NullUUID `db:"child_build_id" json:"child_build_id"`
ChildTransition WorkspaceTransition `db:"child_transition" json:"child_transition"`
ChildTemplateVersionID uuid.NullUUID `db:"child_template_version_id" json:"child_template_version_id"`
ChildTemplateVersionPresetID uuid.NullUUID `db:"child_template_version_preset_id" json:"child_template_version_preset_id"`
ChildRichParameterValues json.RawMessage `db:"child_rich_parameter_values" json:"child_rich_parameter_values"`
ChildLogLevel string `db:"child_log_level" json:"child_log_level"`
ChildReason NullBuildReason `db:"child_reason" json:"child_reason"`
// Counts retryable child build creation failures for this orchestration row.
AttemptCount int32 `db:"attempt_count" json:"attempt_count"`
// When set, the orchestrator skips this pending row until the timestamp has passed.
NextRetryAfter sql.NullTime `db:"next_retry_after" json:"next_retry_after"`
Status string `db:"status" json:"status"`
Error sql.NullString `db:"error" json:"error"`
}
type WorkspaceBuildParameter struct {
WorkspaceBuildID uuid.UUID `db:"workspace_build_id" json:"workspace_build_id"`
// Parameter name
+9
View File
@@ -214,6 +214,7 @@ type sqlcQuerier interface {
// Logs can take up a lot of space, so it's important we clean up frequently.
DeleteOldWorkspaceAgentLogs(ctx context.Context, threshold time.Time) (int64, error)
DeleteOldWorkspaceAgentStats(ctx context.Context) error
DeleteOldWorkspaceBuildOrchestrations(ctx context.Context, arg DeleteOldWorkspaceBuildOrchestrationsParams) (int64, error)
DeleteOrganizationMember(ctx context.Context, arg DeleteOrganizationMemberParams) error
DeleteProvisionerKey(ctx context.Context, id uuid.UUID) error
DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error
@@ -610,6 +611,9 @@ type sqlcQuerier interface {
GetMCPServerConfigsByIDs(ctx context.Context, ids []uuid.UUID) ([]MCPServerConfig, error)
GetMCPServerUserToken(ctx context.Context, arg GetMCPServerUserTokenParams) (MCPServerUserToken, error)
GetMCPServerUserTokensByUserID(ctx context.Context, userID uuid.UUID) ([]MCPServerUserToken, error)
// Must be called from within a transaction. The row lock is released
// when the transaction ends.
GetNextPendingWorkspaceBuildOrchestrationForUpdate(ctx context.Context) (WorkspaceBuildOrchestration, error)
GetNotificationMessagesByStatus(ctx context.Context, arg GetNotificationMessagesByStatusParams) ([]NotificationMessage, error)
// Fetch the notification report generator log indicating recent activity.
GetNotificationReportGeneratorLogByTemplate(ctx context.Context, templateID uuid.UUID) (NotificationReportGeneratorLog, error)
@@ -1117,6 +1121,7 @@ type sqlcQuerier interface {
InsertWorkspaceAppStats(ctx context.Context, arg InsertWorkspaceAppStatsParams) error
InsertWorkspaceAppStatus(ctx context.Context, arg InsertWorkspaceAppStatusParams) (WorkspaceAppStatus, error)
InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) error
InsertWorkspaceBuildOrchestration(ctx context.Context, arg InsertWorkspaceBuildOrchestrationParams) (WorkspaceBuildOrchestration, error)
InsertWorkspaceBuildParameters(ctx context.Context, arg InsertWorkspaceBuildParametersParams) error
InsertWorkspaceModule(ctx context.Context, arg InsertWorkspaceModuleParams) (WorkspaceModule, error)
InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error)
@@ -1488,6 +1493,10 @@ type sqlcQuerier interface {
// reminder idempotent and HA-safe. It re-arms automatically when the deadline
// changes (e.g. an activity bump).
UpdateWorkspaceBuildNotifiedAutostopDeadline(ctx context.Context, arg UpdateWorkspaceBuildNotifiedAutostopDeadlineParams) error
UpdateWorkspaceBuildOrchestrationCanceledByID(ctx context.Context, arg UpdateWorkspaceBuildOrchestrationCanceledByIDParams) (WorkspaceBuildOrchestration, error)
UpdateWorkspaceBuildOrchestrationCompletedByID(ctx context.Context, arg UpdateWorkspaceBuildOrchestrationCompletedByIDParams) (WorkspaceBuildOrchestration, error)
UpdateWorkspaceBuildOrchestrationFailedByID(ctx context.Context, arg UpdateWorkspaceBuildOrchestrationFailedByIDParams) (WorkspaceBuildOrchestration, error)
UpdateWorkspaceBuildOrchestrationRetryByID(ctx context.Context, arg UpdateWorkspaceBuildOrchestrationRetryByIDParams) (WorkspaceBuildOrchestration, error)
UpdateWorkspaceBuildProvisionerStateByID(ctx context.Context, arg UpdateWorkspaceBuildProvisionerStateByIDParams) error
UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error
UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg UpdateWorkspaceDormantDeletingAtParams) (WorkspaceTable, error)
+580
View File
@@ -8800,6 +8800,586 @@ func TestUserSecretsAuthorization(t *testing.T) {
}
}
func TestUpdateWorkspaceBuildOrchestrationRetryByIDMaxAttempts(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
ctx := testutil.Context(t, testutil.WaitShort)
org := dbgen.Organization(t, db, database.Organization{})
user := dbgen.User(t, db, database.User{})
versionJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
Type: database.ProvisionerJobTypeTemplateVersionImport,
})
version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
OrganizationID: org.ID,
CreatedBy: user.ID,
JobID: versionJob.ID,
})
template := dbgen.Template(t, db, database.Template{
OrganizationID: org.ID,
ActiveVersionID: version.ID,
CreatedBy: user.ID,
})
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
OrganizationID: org.ID,
OwnerID: user.ID,
TemplateID: template.ID,
})
buildJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
Type: database.ProvisionerJobTypeWorkspaceBuild,
})
parentBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
WorkspaceID: workspace.ID,
TemplateVersionID: version.ID,
InitiatorID: user.ID,
JobID: buildJob.ID,
Transition: database.WorkspaceTransitionStop,
})
// Given: a pending orchestration row.
now := dbtime.Now()
orchestration, err := db.InsertWorkspaceBuildOrchestration(ctx, database.InsertWorkspaceBuildOrchestrationParams{
ID: uuid.New(),
CreatedAt: now,
UpdatedAt: now,
ParentBuildID: parentBuild.ID,
ChildTransition: database.WorkspaceTransitionStart,
ChildRichParameterValues: json.RawMessage("[]"),
})
require.NoError(t, err)
require.Equal(t, workspace.ID, orchestration.WorkspaceID)
const maxAttemptCount = 3
const retryError = "some retryable child build failure"
recordRetry := func(t *testing.T, wantAttempt int32, wantStatus string, wantNextRetry bool) {
t.Helper()
now := dbtime.Now()
nextRetryAfter := now.Add(time.Minute)
got, err := db.UpdateWorkspaceBuildOrchestrationRetryByID(ctx, database.UpdateWorkspaceBuildOrchestrationRetryByIDParams{
Error: sql.NullString{
String: retryError,
Valid: true,
},
NextRetryAfter: nextRetryAfter,
UpdatedAt: now,
ID: orchestration.ID,
MaxAttemptCount: maxAttemptCount,
})
require.NoError(t, err)
require.Equal(t, wantAttempt, got.AttemptCount)
require.Equal(t, wantStatus, got.Status)
require.True(t, got.Error.Valid)
require.Equal(t, retryError, got.Error.String)
require.Equal(t, wantNextRetry, got.NextRetryAfter.Valid)
if wantNextRetry {
require.False(t, got.NextRetryAfter.Time.Before(nextRetryAfter))
}
}
// When: retryable child build failures are recorded until one
// attempt remains.
recordRetry(t, 1, "pending", true)
recordRetry(t, 2, "pending", true)
// Then: the next attempt fails the row, clears its retry delay,
// and prevents further retry updates.
recordRetry(t, 3, "failed", false)
_, err = db.UpdateWorkspaceBuildOrchestrationRetryByID(ctx, database.UpdateWorkspaceBuildOrchestrationRetryByIDParams{
Error: sql.NullString{
String: retryError,
Valid: true,
},
NextRetryAfter: dbtime.Now().Add(time.Minute),
UpdatedAt: dbtime.Now(),
ID: orchestration.ID,
MaxAttemptCount: maxAttemptCount,
})
require.ErrorIs(t, err, sql.ErrNoRows)
}
func TestUpdateWorkspaceBuildOrchestrationRetryByIDPendingGateUnderContention(t *testing.T) {
t.Parallel()
db, _, _ := dbtestutil.NewDBWithSQLDB(t)
ctx := testutil.Context(t, testutil.WaitShort)
org := dbgen.Organization(t, db, database.Organization{})
user := dbgen.User(t, db, database.User{})
versionJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
Type: database.ProvisionerJobTypeTemplateVersionImport,
})
version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
OrganizationID: org.ID,
CreatedBy: user.ID,
JobID: versionJob.ID,
})
template := dbgen.Template(t, db, database.Template{
OrganizationID: org.ID,
ActiveVersionID: version.ID,
CreatedBy: user.ID,
})
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
OrganizationID: org.ID,
OwnerID: user.ID,
TemplateID: template.ID,
})
buildJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
Type: database.ProvisionerJobTypeWorkspaceBuild,
})
parentBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
WorkspaceID: workspace.ID,
TemplateVersionID: version.ID,
InitiatorID: user.ID,
JobID: buildJob.ID,
Transition: database.WorkspaceTransitionStop,
})
// Given: a pending orchestration row.
now := dbtime.Now()
orchestration, err := db.InsertWorkspaceBuildOrchestration(ctx, database.InsertWorkspaceBuildOrchestrationParams{
ID: uuid.New(),
CreatedAt: now,
UpdatedAt: now,
ParentBuildID: parentBuild.ID,
ChildTransition: database.WorkspaceTransitionStart,
ChildRichParameterValues: json.RawMessage("[]"),
})
require.NoError(t, err)
const retryError = "some retryable child build failure"
type retryResult struct {
orchestration database.WorkspaceBuildOrchestration
err error
}
firstUpdated := make(chan retryResult, 1)
releaseFirst := make(chan struct{})
firstErr := make(chan error, 1)
secondStarted := make(chan struct{}, 1)
secondErr := make(chan error, 1)
secondDone := make(chan struct{})
// When: one retry update terminalizes the row while another
// worker races to retry the same pending row.
go func() {
err := db.InTx(func(tx database.Store) error {
got, err := tx.UpdateWorkspaceBuildOrchestrationRetryByID(ctx, database.UpdateWorkspaceBuildOrchestrationRetryByIDParams{
Error: sql.NullString{
String: retryError,
Valid: true,
},
NextRetryAfter: dbtime.Now().Add(time.Minute),
UpdatedAt: dbtime.Now(),
ID: orchestration.ID,
// Use a single allowed retry so the winning update
// immediately terminalizes the row.
MaxAttemptCount: 1,
})
if err != nil {
firstUpdated <- retryResult{err: err}
return err
}
firstUpdated <- retryResult{orchestration: got}
<-releaseFirst
return nil
}, nil)
firstErr <- err
}()
firstResult := <-firstUpdated
require.NoError(t, firstResult.err)
go func() {
secondStarted <- struct{}{}
_, err := db.UpdateWorkspaceBuildOrchestrationRetryByID(ctx, database.UpdateWorkspaceBuildOrchestrationRetryByIDParams{
Error: sql.NullString{
String: retryError,
Valid: true,
},
NextRetryAfter: dbtime.Now().Add(time.Minute),
UpdatedAt: dbtime.Now(),
ID: orchestration.ID,
MaxAttemptCount: 1,
})
secondErr <- err
close(secondDone)
}()
<-secondStarted
// Then: while the first transaction is held open, the second
// update does not complete.
require.Never(t, func() bool {
select {
case <-secondDone:
return true
default:
return false
}
}, time.Second, testutil.IntervalFast)
close(releaseFirst)
require.NoError(t, <-firstErr)
// Then: after the first transaction commits the failed status, the
// second update rechecks the pending gate and affects no rows.
require.ErrorIs(t, <-secondErr, sql.ErrNoRows)
}
func TestGetNextPendingWorkspaceBuildOrchestrationForUpdateRetryDelay(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
ctx := testutil.Context(t, testutil.WaitShort)
org := dbgen.Organization(t, db, database.Organization{})
user := dbgen.User(t, db, database.User{})
versionJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
Type: database.ProvisionerJobTypeTemplateVersionImport,
})
version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
OrganizationID: org.ID,
CreatedBy: user.ID,
JobID: versionJob.ID,
})
template := dbgen.Template(t, db, database.Template{
OrganizationID: org.ID,
ActiveVersionID: version.ID,
CreatedBy: user.ID,
})
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
OrganizationID: org.ID,
OwnerID: user.ID,
TemplateID: template.ID,
})
var buildNumber int32
createOrchestration := func(t *testing.T, createdAt time.Time) database.WorkspaceBuildOrchestration {
t.Helper()
buildNumber++
buildJob := database.ProvisionerJob{
OrganizationID: org.ID,
Type: database.ProvisionerJobTypeWorkspaceBuild,
}
setJobStatus(t, database.ProvisionerJobStatusSucceeded, &buildJob)
buildJob = dbgen.ProvisionerJob(t, db, nil, buildJob)
parentBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
WorkspaceID: workspace.ID,
TemplateVersionID: version.ID,
InitiatorID: user.ID,
JobID: buildJob.ID,
BuildNumber: buildNumber,
Transition: database.WorkspaceTransitionStop,
})
orchestration, err := db.InsertWorkspaceBuildOrchestration(ctx, database.InsertWorkspaceBuildOrchestrationParams{
ID: uuid.New(),
CreatedAt: createdAt,
UpdatedAt: createdAt,
ParentBuildID: parentBuild.ID,
ChildTransition: database.WorkspaceTransitionStart,
ChildRichParameterValues: json.RawMessage("[]"),
})
require.NoError(t, err)
require.Equal(t, workspace.ID, orchestration.WorkspaceID)
return orchestration
}
claimNext := func(t *testing.T) (database.WorkspaceBuildOrchestration, error) {
t.Helper()
var orchestration database.WorkspaceBuildOrchestration
err := db.InTx(func(tx database.Store) error {
var err error
orchestration, err = tx.GetNextPendingWorkspaceBuildOrchestrationForUpdate(ctx)
return err
}, nil)
return orchestration, err
}
baseTime := dbtime.Now().Add(-time.Hour)
// Given: an old pending orchestration row with a future retry delay.
delayed := createOrchestration(t, baseTime)
_, err := db.UpdateWorkspaceBuildOrchestrationRetryByID(ctx, database.UpdateWorkspaceBuildOrchestrationRetryByIDParams{
Error: sql.NullString{
String: "retry later",
Valid: true,
},
NextRetryAfter: dbtime.Now().Add(time.Hour),
UpdatedAt: dbtime.Now(),
ID: delayed.ID,
MaxAttemptCount: 3,
})
require.NoError(t, err)
// When: the orchestrator claims the next eligible row.
_, err = claimNext(t)
// Then: no row is claimed before the retry time.
require.ErrorIs(t, err, sql.ErrNoRows)
// When: a later row is eligible immediately.
eligible := createOrchestration(t, baseTime.Add(time.Minute))
// Then: the old delayed row does not block the later eligible row.
got, err := claimNext(t)
require.NoError(t, err)
require.Equal(t, eligible.ID, got.ID)
// When: the delayed row's retry time has passed.
_, err = db.UpdateWorkspaceBuildOrchestrationRetryByID(ctx, database.UpdateWorkspaceBuildOrchestrationRetryByIDParams{
Error: sql.NullString{
String: "retry now",
Valid: true,
},
NextRetryAfter: dbtime.Now().Add(-time.Minute),
UpdatedAt: dbtime.Now(),
ID: delayed.ID,
MaxAttemptCount: 3,
})
require.NoError(t, err)
// Then: the delayed row is eligible again and is claimed first
// because it is older than the later row.
got, err = claimNext(t)
require.NoError(t, err)
require.Equal(t, delayed.ID, got.ID)
}
func TestUpdateWorkspaceBuildOrchestrationCompletedByIDWorkspaceMismatch(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
ctx := testutil.Context(t, testutil.WaitShort)
org := dbgen.Organization(t, db, database.Organization{})
user := dbgen.User(t, db, database.User{})
versionJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
Type: database.ProvisionerJobTypeTemplateVersionImport,
})
version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
OrganizationID: org.ID,
CreatedBy: user.ID,
JobID: versionJob.ID,
})
template := dbgen.Template(t, db, database.Template{
OrganizationID: org.ID,
ActiveVersionID: version.ID,
CreatedBy: user.ID,
})
parentWorkspace := dbgen.Workspace(t, db, database.WorkspaceTable{
OrganizationID: org.ID,
OwnerID: user.ID,
TemplateID: template.ID,
})
otherWorkspace := dbgen.Workspace(t, db, database.WorkspaceTable{
OrganizationID: org.ID,
OwnerID: user.ID,
TemplateID: template.ID,
})
parentJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
Type: database.ProvisionerJobTypeWorkspaceBuild,
})
parentBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
WorkspaceID: parentWorkspace.ID,
TemplateVersionID: version.ID,
InitiatorID: user.ID,
JobID: parentJob.ID,
Transition: database.WorkspaceTransitionStop,
})
// Given: a pending orchestration row.
orchestration, err := db.InsertWorkspaceBuildOrchestration(ctx, database.InsertWorkspaceBuildOrchestrationParams{
ID: uuid.New(),
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
ParentBuildID: parentBuild.ID,
ChildTransition: database.WorkspaceTransitionStart,
ChildRichParameterValues: json.RawMessage("[]"),
})
require.NoError(t, err)
require.Equal(t, parentWorkspace.ID, orchestration.WorkspaceID)
// Given: a child build whose workspace does not match the parent
// build's workspace.
childJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
Type: database.ProvisionerJobTypeWorkspaceBuild,
})
childBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
WorkspaceID: otherWorkspace.ID,
TemplateVersionID: version.ID,
InitiatorID: user.ID,
JobID: childJob.ID,
Transition: database.WorkspaceTransitionStart,
})
// When: the orchestration is completed with the child build.
_, err = db.UpdateWorkspaceBuildOrchestrationCompletedByID(ctx, database.UpdateWorkspaceBuildOrchestrationCompletedByIDParams{
ID: orchestration.ID,
ChildBuildID: uuid.NullUUID{UUID: childBuild.ID, Valid: true},
UpdatedAt: dbtime.Now(),
})
// Then: the composite foreign key rejects the mismatched child build.
require.Error(t, err)
require.True(t, database.IsForeignKeyViolation(err))
}
func TestInsertWorkspaceBuildOrchestrationPresetRequiresVersion(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
ctx := testutil.Context(t, testutil.WaitShort)
org := dbgen.Organization(t, db, database.Organization{})
user := dbgen.User(t, db, database.User{})
versionJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
Type: database.ProvisionerJobTypeTemplateVersionImport,
})
version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
OrganizationID: org.ID,
CreatedBy: user.ID,
JobID: versionJob.ID,
})
template := dbgen.Template(t, db, database.Template{
OrganizationID: org.ID,
ActiveVersionID: version.ID,
CreatedBy: user.ID,
})
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
OrganizationID: org.ID,
OwnerID: user.ID,
TemplateID: template.ID,
})
parentJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
Type: database.ProvisionerJobTypeWorkspaceBuild,
})
// Given: a parent build, a child preset, no child template
// version.
parentBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
WorkspaceID: workspace.ID,
TemplateVersionID: version.ID,
InitiatorID: user.ID,
JobID: parentJob.ID,
Transition: database.WorkspaceTransitionStop,
})
preset := dbgen.Preset(t, db, database.InsertPresetParams{
TemplateVersionID: version.ID,
})
// When: the orchestration row is inserted.
_, err := db.InsertWorkspaceBuildOrchestration(ctx, database.InsertWorkspaceBuildOrchestrationParams{
ID: uuid.New(),
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
ParentBuildID: parentBuild.ID,
ChildTransition: database.WorkspaceTransitionStart,
ChildTemplateVersionPresetID: uuid.NullUUID{
UUID: preset.ID,
Valid: true,
},
ChildRichParameterValues: json.RawMessage("[]"),
})
// Then: the check constraint rejects the missing child template
// version.
require.Error(t, err)
require.True(t, database.IsCheckViolation(err))
}
func TestInsertWorkspaceBuildOrchestrationPresetVersionMismatch(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
ctx := testutil.Context(t, testutil.WaitShort)
org := dbgen.Organization(t, db, database.Organization{})
user := dbgen.User(t, db, database.User{})
versionOneJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
Type: database.ProvisionerJobTypeTemplateVersionImport,
})
versionOne := dbgen.TemplateVersion(t, db, database.TemplateVersion{
OrganizationID: org.ID,
CreatedBy: user.ID,
JobID: versionOneJob.ID,
})
versionTwoJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
Type: database.ProvisionerJobTypeTemplateVersionImport,
})
versionTwo := dbgen.TemplateVersion(t, db, database.TemplateVersion{
OrganizationID: org.ID,
CreatedBy: user.ID,
JobID: versionTwoJob.ID,
})
template := dbgen.Template(t, db, database.Template{
OrganizationID: org.ID,
ActiveVersionID: versionOne.ID,
CreatedBy: user.ID,
})
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
OrganizationID: org.ID,
OwnerID: user.ID,
TemplateID: template.ID,
})
parentJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
Type: database.ProvisionerJobTypeWorkspaceBuild,
})
// Given: a parent build and a child preset.
parentBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
WorkspaceID: workspace.ID,
TemplateVersionID: versionOne.ID,
InitiatorID: user.ID,
JobID: parentJob.ID,
Transition: database.WorkspaceTransitionStop,
})
preset := dbgen.Preset(t, db, database.InsertPresetParams{
TemplateVersionID: versionOne.ID,
})
// When: the orchestration row is inserted with a different child
// template version.
_, err := db.InsertWorkspaceBuildOrchestration(ctx, database.InsertWorkspaceBuildOrchestrationParams{
ID: uuid.New(),
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
ParentBuildID: parentBuild.ID,
ChildTransition: database.WorkspaceTransitionStart,
ChildTemplateVersionID: uuid.NullUUID{
UUID: versionTwo.ID,
Valid: true,
},
ChildTemplateVersionPresetID: uuid.NullUUID{
UUID: preset.ID,
Valid: true,
},
ChildRichParameterValues: json.RawMessage("[]"),
})
// Then: the composite foreign key rejects the preset/version
// mismatch.
require.Error(t, err)
require.True(t, database.IsForeignKeyViolation(err))
}
func TestWorkspaceBuildDeadlineConstraint(t *testing.T) {
t.Parallel()
+353
View File
@@ -35141,6 +35141,359 @@ func (q *sqlQuerier) InsertWorkspaceAppStats(ctx context.Context, arg InsertWork
return err
}
const deleteOldWorkspaceBuildOrchestrations = `-- name: DeleteOldWorkspaceBuildOrchestrations :execrows
WITH deletable AS (
SELECT
id
FROM
workspace_build_orchestrations
WHERE
status IN ('completed', 'failed', 'canceled')
AND updated_at < $1::timestamptz
ORDER BY
updated_at ASC
LIMIT $2::int
)
DELETE FROM workspace_build_orchestrations
USING deletable
WHERE workspace_build_orchestrations.id = deletable.id
`
type DeleteOldWorkspaceBuildOrchestrationsParams struct {
BeforeTime time.Time `db:"before_time" json:"before_time"`
LimitCount int32 `db:"limit_count" json:"limit_count"`
}
func (q *sqlQuerier) DeleteOldWorkspaceBuildOrchestrations(ctx context.Context, arg DeleteOldWorkspaceBuildOrchestrationsParams) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteOldWorkspaceBuildOrchestrations, arg.BeforeTime, arg.LimitCount)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const getNextPendingWorkspaceBuildOrchestrationForUpdate = `-- name: GetNextPendingWorkspaceBuildOrchestrationForUpdate :one
SELECT
wbo.id, wbo.created_at, wbo.updated_at, wbo.workspace_id, wbo.parent_build_id, wbo.child_build_id, wbo.child_transition, wbo.child_template_version_id, wbo.child_template_version_preset_id, wbo.child_rich_parameter_values, wbo.child_log_level, wbo.child_reason, wbo.attempt_count, wbo.next_retry_after, wbo.status, wbo.error
FROM
workspace_build_orchestrations wbo
JOIN workspace_builds wb ON wbo.parent_build_id = wb.id
JOIN provisioner_jobs pj ON wb.job_id = pj.id
WHERE
wbo.status = 'pending'
AND (
wbo.next_retry_after IS NULL
OR wbo.next_retry_after <= NOW()
)
-- Include all terminal parent states so pending orchestration
-- rows are processed and resolved even when no child build should
-- be created.
AND pj.job_status IN ('succeeded', 'failed', 'canceled')
ORDER BY
wbo.created_at ASC
LIMIT 1
FOR UPDATE OF wbo SKIP LOCKED
`
// Must be called from within a transaction. The row lock is released
// when the transaction ends.
func (q *sqlQuerier) GetNextPendingWorkspaceBuildOrchestrationForUpdate(ctx context.Context) (WorkspaceBuildOrchestration, error) {
row := q.db.QueryRowContext(ctx, getNextPendingWorkspaceBuildOrchestrationForUpdate)
var i WorkspaceBuildOrchestration
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.ParentBuildID,
&i.ChildBuildID,
&i.ChildTransition,
&i.ChildTemplateVersionID,
&i.ChildTemplateVersionPresetID,
&i.ChildRichParameterValues,
&i.ChildLogLevel,
&i.ChildReason,
&i.AttemptCount,
&i.NextRetryAfter,
&i.Status,
&i.Error,
)
return i, err
}
const insertWorkspaceBuildOrchestration = `-- name: InsertWorkspaceBuildOrchestration :one
INSERT INTO workspace_build_orchestrations (
id,
created_at,
updated_at,
parent_build_id,
workspace_id,
child_transition,
child_template_version_id,
child_template_version_preset_id,
child_rich_parameter_values,
child_log_level,
child_reason,
status,
error
)
VALUES (
$1,
$2,
$3,
$4,
(SELECT workspace_id FROM workspace_builds WHERE id = $4),
$5,
$6,
$7,
$8,
$9,
$10,
'pending',
NULL
)
RETURNING id, created_at, updated_at, workspace_id, parent_build_id, child_build_id, child_transition, child_template_version_id, child_template_version_preset_id, child_rich_parameter_values, child_log_level, child_reason, attempt_count, next_retry_after, status, error
`
type InsertWorkspaceBuildOrchestrationParams struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ParentBuildID uuid.UUID `db:"parent_build_id" json:"parent_build_id"`
ChildTransition WorkspaceTransition `db:"child_transition" json:"child_transition"`
ChildTemplateVersionID uuid.NullUUID `db:"child_template_version_id" json:"child_template_version_id"`
ChildTemplateVersionPresetID uuid.NullUUID `db:"child_template_version_preset_id" json:"child_template_version_preset_id"`
ChildRichParameterValues json.RawMessage `db:"child_rich_parameter_values" json:"child_rich_parameter_values"`
ChildLogLevel string `db:"child_log_level" json:"child_log_level"`
ChildReason NullBuildReason `db:"child_reason" json:"child_reason"`
}
func (q *sqlQuerier) InsertWorkspaceBuildOrchestration(ctx context.Context, arg InsertWorkspaceBuildOrchestrationParams) (WorkspaceBuildOrchestration, error) {
row := q.db.QueryRowContext(ctx, insertWorkspaceBuildOrchestration,
arg.ID,
arg.CreatedAt,
arg.UpdatedAt,
arg.ParentBuildID,
arg.ChildTransition,
arg.ChildTemplateVersionID,
arg.ChildTemplateVersionPresetID,
arg.ChildRichParameterValues,
arg.ChildLogLevel,
arg.ChildReason,
)
var i WorkspaceBuildOrchestration
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.ParentBuildID,
&i.ChildBuildID,
&i.ChildTransition,
&i.ChildTemplateVersionID,
&i.ChildTemplateVersionPresetID,
&i.ChildRichParameterValues,
&i.ChildLogLevel,
&i.ChildReason,
&i.AttemptCount,
&i.NextRetryAfter,
&i.Status,
&i.Error,
)
return i, err
}
const updateWorkspaceBuildOrchestrationCanceledByID = `-- name: UpdateWorkspaceBuildOrchestrationCanceledByID :one
UPDATE
workspace_build_orchestrations
SET
status = 'canceled',
next_retry_after = NULL,
error = NULL,
updated_at = $1
WHERE
id = $2
AND status = 'pending'
RETURNING id, created_at, updated_at, workspace_id, parent_build_id, child_build_id, child_transition, child_template_version_id, child_template_version_preset_id, child_rich_parameter_values, child_log_level, child_reason, attempt_count, next_retry_after, status, error
`
type UpdateWorkspaceBuildOrchestrationCanceledByIDParams struct {
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateWorkspaceBuildOrchestrationCanceledByID(ctx context.Context, arg UpdateWorkspaceBuildOrchestrationCanceledByIDParams) (WorkspaceBuildOrchestration, error) {
row := q.db.QueryRowContext(ctx, updateWorkspaceBuildOrchestrationCanceledByID, arg.UpdatedAt, arg.ID)
var i WorkspaceBuildOrchestration
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.ParentBuildID,
&i.ChildBuildID,
&i.ChildTransition,
&i.ChildTemplateVersionID,
&i.ChildTemplateVersionPresetID,
&i.ChildRichParameterValues,
&i.ChildLogLevel,
&i.ChildReason,
&i.AttemptCount,
&i.NextRetryAfter,
&i.Status,
&i.Error,
)
return i, err
}
const updateWorkspaceBuildOrchestrationCompletedByID = `-- name: UpdateWorkspaceBuildOrchestrationCompletedByID :one
UPDATE
workspace_build_orchestrations
SET
child_build_id = $1,
status = 'completed',
next_retry_after = NULL,
error = NULL,
updated_at = $2
WHERE
id = $3
AND status = 'pending'
RETURNING id, created_at, updated_at, workspace_id, parent_build_id, child_build_id, child_transition, child_template_version_id, child_template_version_preset_id, child_rich_parameter_values, child_log_level, child_reason, attempt_count, next_retry_after, status, error
`
type UpdateWorkspaceBuildOrchestrationCompletedByIDParams struct {
ChildBuildID uuid.NullUUID `db:"child_build_id" json:"child_build_id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateWorkspaceBuildOrchestrationCompletedByID(ctx context.Context, arg UpdateWorkspaceBuildOrchestrationCompletedByIDParams) (WorkspaceBuildOrchestration, error) {
row := q.db.QueryRowContext(ctx, updateWorkspaceBuildOrchestrationCompletedByID, arg.ChildBuildID, arg.UpdatedAt, arg.ID)
var i WorkspaceBuildOrchestration
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.ParentBuildID,
&i.ChildBuildID,
&i.ChildTransition,
&i.ChildTemplateVersionID,
&i.ChildTemplateVersionPresetID,
&i.ChildRichParameterValues,
&i.ChildLogLevel,
&i.ChildReason,
&i.AttemptCount,
&i.NextRetryAfter,
&i.Status,
&i.Error,
)
return i, err
}
const updateWorkspaceBuildOrchestrationFailedByID = `-- name: UpdateWorkspaceBuildOrchestrationFailedByID :one
UPDATE
workspace_build_orchestrations
SET
status = 'failed',
next_retry_after = NULL,
error = $1,
updated_at = $2
WHERE
id = $3
AND status = 'pending'
RETURNING id, created_at, updated_at, workspace_id, parent_build_id, child_build_id, child_transition, child_template_version_id, child_template_version_preset_id, child_rich_parameter_values, child_log_level, child_reason, attempt_count, next_retry_after, status, error
`
type UpdateWorkspaceBuildOrchestrationFailedByIDParams struct {
Error sql.NullString `db:"error" json:"error"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateWorkspaceBuildOrchestrationFailedByID(ctx context.Context, arg UpdateWorkspaceBuildOrchestrationFailedByIDParams) (WorkspaceBuildOrchestration, error) {
row := q.db.QueryRowContext(ctx, updateWorkspaceBuildOrchestrationFailedByID, arg.Error, arg.UpdatedAt, arg.ID)
var i WorkspaceBuildOrchestration
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.ParentBuildID,
&i.ChildBuildID,
&i.ChildTransition,
&i.ChildTemplateVersionID,
&i.ChildTemplateVersionPresetID,
&i.ChildRichParameterValues,
&i.ChildLogLevel,
&i.ChildReason,
&i.AttemptCount,
&i.NextRetryAfter,
&i.Status,
&i.Error,
)
return i, err
}
const updateWorkspaceBuildOrchestrationRetryByID = `-- name: UpdateWorkspaceBuildOrchestrationRetryByID :one
UPDATE
workspace_build_orchestrations
SET
attempt_count = attempt_count + 1,
next_retry_after = CASE
WHEN attempt_count + 1 >= $1::int THEN NULL
ELSE $2::timestamptz
END,
status = CASE
WHEN attempt_count + 1 >= $1::int THEN 'failed'
ELSE status
END,
error = $3,
updated_at = $4
WHERE
id = $5
AND status = 'pending'
RETURNING id, created_at, updated_at, workspace_id, parent_build_id, child_build_id, child_transition, child_template_version_id, child_template_version_preset_id, child_rich_parameter_values, child_log_level, child_reason, attempt_count, next_retry_after, status, error
`
type UpdateWorkspaceBuildOrchestrationRetryByIDParams struct {
MaxAttemptCount int32 `db:"max_attempt_count" json:"max_attempt_count"`
NextRetryAfter time.Time `db:"next_retry_after" json:"next_retry_after"`
Error sql.NullString `db:"error" json:"error"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateWorkspaceBuildOrchestrationRetryByID(ctx context.Context, arg UpdateWorkspaceBuildOrchestrationRetryByIDParams) (WorkspaceBuildOrchestration, error) {
row := q.db.QueryRowContext(ctx, updateWorkspaceBuildOrchestrationRetryByID,
arg.MaxAttemptCount,
arg.NextRetryAfter,
arg.Error,
arg.UpdatedAt,
arg.ID,
)
var i WorkspaceBuildOrchestration
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.ParentBuildID,
&i.ChildBuildID,
&i.ChildTransition,
&i.ChildTemplateVersionID,
&i.ChildTemplateVersionPresetID,
&i.ChildRichParameterValues,
&i.ChildLogLevel,
&i.ChildReason,
&i.AttemptCount,
&i.NextRetryAfter,
&i.Status,
&i.Error,
)
return i, err
}
const getUserWorkspaceBuildParameters = `-- name: GetUserWorkspaceBuildParameters :many
SELECT name, value
FROM (
@@ -0,0 +1,133 @@
-- name: InsertWorkspaceBuildOrchestration :one
INSERT INTO workspace_build_orchestrations (
id,
created_at,
updated_at,
parent_build_id,
workspace_id,
child_transition,
child_template_version_id,
child_template_version_preset_id,
child_rich_parameter_values,
child_log_level,
child_reason,
status,
error
)
VALUES (
@id,
@created_at,
@updated_at,
@parent_build_id,
(SELECT workspace_id FROM workspace_builds WHERE id = @parent_build_id),
@child_transition,
@child_template_version_id,
@child_template_version_preset_id,
@child_rich_parameter_values,
@child_log_level,
@child_reason,
'pending',
NULL
)
RETURNING *;
-- name: GetNextPendingWorkspaceBuildOrchestrationForUpdate :one
-- Must be called from within a transaction. The row lock is released
-- when the transaction ends.
SELECT
wbo.*
FROM
workspace_build_orchestrations wbo
JOIN workspace_builds wb ON wbo.parent_build_id = wb.id
JOIN provisioner_jobs pj ON wb.job_id = pj.id
WHERE
wbo.status = 'pending'
AND (
wbo.next_retry_after IS NULL
OR wbo.next_retry_after <= NOW()
)
-- Include all terminal parent states so pending orchestration
-- rows are processed and resolved even when no child build should
-- be created.
AND pj.job_status IN ('succeeded', 'failed', 'canceled')
ORDER BY
wbo.created_at ASC
LIMIT 1
FOR UPDATE OF wbo SKIP LOCKED;
-- name: UpdateWorkspaceBuildOrchestrationCompletedByID :one
UPDATE
workspace_build_orchestrations
SET
child_build_id = @child_build_id,
status = 'completed',
next_retry_after = NULL,
error = NULL,
updated_at = @updated_at
WHERE
id = @id
AND status = 'pending'
RETURNING *;
-- name: UpdateWorkspaceBuildOrchestrationFailedByID :one
UPDATE
workspace_build_orchestrations
SET
status = 'failed',
next_retry_after = NULL,
error = @error,
updated_at = @updated_at
WHERE
id = @id
AND status = 'pending'
RETURNING *;
-- name: UpdateWorkspaceBuildOrchestrationRetryByID :one
UPDATE
workspace_build_orchestrations
SET
attempt_count = attempt_count + 1,
next_retry_after = CASE
WHEN attempt_count + 1 >= @max_attempt_count::int THEN NULL
ELSE @next_retry_after::timestamptz
END,
status = CASE
WHEN attempt_count + 1 >= @max_attempt_count::int THEN 'failed'
ELSE status
END,
error = @error,
updated_at = @updated_at
WHERE
id = @id
AND status = 'pending'
RETURNING *;
-- name: UpdateWorkspaceBuildOrchestrationCanceledByID :one
UPDATE
workspace_build_orchestrations
SET
status = 'canceled',
next_retry_after = NULL,
error = NULL,
updated_at = @updated_at
WHERE
id = @id
AND status = 'pending'
RETURNING *;
-- name: DeleteOldWorkspaceBuildOrchestrations :execrows
WITH deletable AS (
SELECT
id
FROM
workspace_build_orchestrations
WHERE
status IN ('completed', 'failed', 'canceled')
AND updated_at < @before_time::timestamptz
ORDER BY
updated_at ASC
LIMIT @limit_count::int
)
DELETE FROM workspace_build_orchestrations
USING deletable
WHERE workspace_build_orchestrations.id = deletable.id;
+5
View File
@@ -92,6 +92,7 @@ const (
UniqueTemplateVersionParametersTemplateVersionIDNameKey UniqueConstraint = "template_version_parameters_template_version_id_name_key" // ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_name_key UNIQUE (template_version_id, name);
UniqueTemplateVersionPresetParametersPkey UniqueConstraint = "template_version_preset_parameters_pkey" // ALTER TABLE ONLY template_version_preset_parameters ADD CONSTRAINT template_version_preset_parameters_pkey PRIMARY KEY (id);
UniqueTemplateVersionPresetPrebuildSchedulesPkey UniqueConstraint = "template_version_preset_prebuild_schedules_pkey" // ALTER TABLE ONLY template_version_preset_prebuild_schedules ADD CONSTRAINT template_version_preset_prebuild_schedules_pkey PRIMARY KEY (id);
UniqueTemplateVersionPresetsIDTemplateVersionIDKey UniqueConstraint = "template_version_presets_id_template_version_id_key" // ALTER TABLE ONLY template_version_presets ADD CONSTRAINT template_version_presets_id_template_version_id_key UNIQUE (id, template_version_id);
UniqueTemplateVersionPresetsPkey UniqueConstraint = "template_version_presets_pkey" // ALTER TABLE ONLY template_version_presets ADD CONSTRAINT template_version_presets_pkey PRIMARY KEY (id);
UniqueTemplateVersionTerraformValuesTemplateVersionIDKey UniqueConstraint = "template_version_terraform_values_template_version_id_key" // ALTER TABLE ONLY template_version_terraform_values ADD CONSTRAINT template_version_terraform_values_template_version_id_key UNIQUE (template_version_id);
UniqueTemplateVersionVariablesTemplateVersionIDNameKey UniqueConstraint = "template_version_variables_template_version_id_name_key" // ALTER TABLE ONLY template_version_variables ADD CONSTRAINT template_version_variables_template_version_id_name_key UNIQUE (template_version_id, name);
@@ -131,7 +132,11 @@ const (
UniqueWorkspaceAppStatusesPkey UniqueConstraint = "workspace_app_statuses_pkey" // ALTER TABLE ONLY workspace_app_statuses ADD CONSTRAINT workspace_app_statuses_pkey PRIMARY KEY (id);
UniqueWorkspaceAppsAgentIDSlugIndex UniqueConstraint = "workspace_apps_agent_id_slug_idx" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_slug_idx UNIQUE (agent_id, slug);
UniqueWorkspaceAppsPkey UniqueConstraint = "workspace_apps_pkey" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_pkey PRIMARY KEY (id);
UniqueWorkspaceBuildOrchestrationsChildBuildIDKey UniqueConstraint = "workspace_build_orchestrations_child_build_id_key" // ALTER TABLE ONLY workspace_build_orchestrations ADD CONSTRAINT workspace_build_orchestrations_child_build_id_key UNIQUE (child_build_id);
UniqueWorkspaceBuildOrchestrationsParentBuildIDKey UniqueConstraint = "workspace_build_orchestrations_parent_build_id_key" // ALTER TABLE ONLY workspace_build_orchestrations ADD CONSTRAINT workspace_build_orchestrations_parent_build_id_key UNIQUE (parent_build_id);
UniqueWorkspaceBuildOrchestrationsPkey UniqueConstraint = "workspace_build_orchestrations_pkey" // ALTER TABLE ONLY workspace_build_orchestrations ADD CONSTRAINT workspace_build_orchestrations_pkey PRIMARY KEY (id);
UniqueWorkspaceBuildParametersWorkspaceBuildIDNameKey UniqueConstraint = "workspace_build_parameters_workspace_build_id_name_key" // ALTER TABLE ONLY workspace_build_parameters ADD CONSTRAINT workspace_build_parameters_workspace_build_id_name_key UNIQUE (workspace_build_id, name);
UniqueWorkspaceBuildsIDWorkspaceIDKey UniqueConstraint = "workspace_builds_id_workspace_id_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_id_workspace_id_key UNIQUE (id, workspace_id);
UniqueWorkspaceBuildsJobIDKey UniqueConstraint = "workspace_builds_job_id_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_key UNIQUE (job_id);
UniqueWorkspaceBuildsPkey UniqueConstraint = "workspace_builds_pkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_pkey PRIMARY KEY (id);
UniqueWorkspaceBuildsWorkspaceIDBuildNumberKey UniqueConstraint = "workspace_builds_workspace_id_build_number_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_workspace_id_build_number_key UNIQUE (workspace_id, build_number);