mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
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:
Generated
+61
-54
@@ -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
|
||||
)
|
||||
|
||||
@@ -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, ¬Authorized) {
|
||||
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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Generated
+105
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Generated
+76
-1
@@ -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
@@ -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';
|
||||
+21
@@ -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;
|
||||
Generated
+41
-1
@@ -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
|
||||
|
||||
Generated
+9
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Generated
+353
@@ -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;
|
||||
Generated
+5
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user