From 17867b6d9b0167d50fc8fbd550b6eeebdab61259 Mon Sep 17 00:00:00 2001 From: George Katsitadze Date: Thu, 7 May 2026 14:09:34 -0700 Subject: [PATCH 01/13] feat(coderd/database): add workspace build orchestration storage Add the workspace_build_orchestrations table, SQLC queries, generated store wrappers, and dbauthz coverage for durable follow-up workspace build intent. This includes insert, claim, completion, failure, cancellation, and retry update queries, plus authorization for creating orchestration rows and template version pin handling for child builds. Ref: https://linear.app/codercom/issue/PLAT-143/add-workspace-restart-functionality-to-api Ref: https://github.com/coder/coder/issues/5800 --- coderd/database/check_constraint.go | 92 ++--- coderd/database/dbauthz/dbauthz.go | 100 +++++ coderd/database/dbauthz/dbauthz_test.go | 159 ++++++++ coderd/database/dbmetrics/querymetrics.go | 56 +++ coderd/database/dbmock/dbmock.go | 105 ++++++ coderd/database/dump.sql | 57 +++ coderd/database/foreign_key_constraint.go | 288 ++++++++------- ...10_workspace_build_orchestrations.down.sql | 1 + ...0510_workspace_build_orchestrations.up.sql | 58 +++ ...0510_workspace_build_orchestrations.up.sql | 19 + coderd/database/models.go | 23 ++ coderd/database/querier.go | 9 + coderd/database/querier_test.go | 222 +++++++++++ coderd/database/queries.sql.go | 346 ++++++++++++++++++ .../queries/workspacebuildorchestrations.sql | 122 ++++++ coderd/database/unique_constraint.go | 3 + 16 files changed, 1475 insertions(+), 185 deletions(-) create mode 100644 coderd/database/migrations/000510_workspace_build_orchestrations.down.sql create mode 100644 coderd/database/migrations/000510_workspace_build_orchestrations.up.sql create mode 100644 coderd/database/migrations/testdata/fixtures/000510_workspace_build_orchestrations.up.sql create mode 100644 coderd/database/queries/workspacebuildorchestrations.sql diff --git a/coderd/database/check_constraint.go b/coderd/database/check_constraint.go index 5682341ef9a..7ba2ea816a0 100644 --- a/coderd/database/check_constraint.go +++ b/coderd/database/check_constraint.go @@ -6,47 +6,53 @@ type CheckConstraint string // CheckConstraint enums. const ( - 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 - 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 - 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 - 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 + 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 + 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 + 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 + 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 + CheckWorkspaceBuildOrchestrationsChildRichParameterValuesChec CheckConstraint = "workspace_build_orchestrations_child_rich_parameter_values_chec" // 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 ) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 74aa3f9f468..68ee1adbea9 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3672,6 +3672,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.ResourceSystem); 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 @@ -5137,6 +5144,13 @@ func (q *querier) GetWorkspaceBuildMetricsByResourceID(ctx context.Context, id u return q.db.GetWorkspaceBuildMetricsByResourceID(ctx, id) } +func (q *querier) GetWorkspaceBuildOrchestrationByParentBuildID(ctx context.Context, parentBuildID uuid.UUID) (database.WorkspaceBuildOrchestration, error) { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { + return database.WorkspaceBuildOrchestration{}, err + } + return q.db.GetWorkspaceBuildOrchestrationByParentBuildID(ctx, parentBuildID) +} + func (q *querier) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]database.WorkspaceBuildParameter, error) { // Authorized call to get the workspace build. If we can read the build, // we can read the params. @@ -6022,6 +6036,64 @@ 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) { + 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) { + arg.ChildTemplateVersionID = uuid.NullUUID{} + } 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) @@ -7867,6 +7939,34 @@ func (q *querier) UpdateWorkspaceBuildFlagsByID(ctx context.Context, arg databas return q.db.UpdateWorkspaceBuildFlagsByID(ctx, arg) } +func (q *querier) UpdateWorkspaceBuildOrchestrationCanceledByID(ctx context.Context, arg database.UpdateWorkspaceBuildOrchestrationCanceledByIDParams) (database.WorkspaceBuildOrchestration, error) { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); 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.ResourceSystem); 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.ResourceSystem); 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.ResourceSystem); 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 diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 2cff1d6c8ce..5d01979a171 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -3775,6 +3775,165 @@ 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, + }, + } + insertArg := arg + insertArg.ChildTemplateVersionID = uuid.NullUUID{} + 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 non-template admins have the requested durable + // template version pin stripped before insert. + dbm.EXPECT().InsertWorkspaceBuildOrchestration(gomock.Any(), insertArg).Return(orchestration, nil).AnyTimes() + check.Args(arg). + Asserts( + w, policy.ActionWorkspaceStop, + w, policy.ActionWorkspaceStart, + t, policy.ActionUpdate, + ). + Returns(orchestration). + 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.ResourceSystem, policy.ActionRead). + Returns(orchestration) + })) + s.Run("GetWorkspaceBuildOrchestrationByParentBuildID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + orchestration := testutil.Fake(s.T(), faker, database.WorkspaceBuildOrchestration{}) + dbm.EXPECT().GetWorkspaceBuildOrchestrationByParentBuildID(gomock.Any(), orchestration.ParentBuildID).Return(orchestration, nil).AnyTimes() + check.Args(orchestration.ParentBuildID). + Asserts(rbac.ResourceSystem, 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.ResourceSystem, 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.ResourceSystem, 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.ResourceSystem, 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.ResourceSystem, policy.ActionUpdate). + Returns(orchestration) + })) 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{ diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index fd4537ccec7..6e16074fddd 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -2129,6 +2129,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) @@ -3441,6 +3449,14 @@ func (m queryMetricsStore) GetWorkspaceBuildMetricsByResourceID(ctx context.Cont return r0, r1 } +func (m queryMetricsStore) GetWorkspaceBuildOrchestrationByParentBuildID(ctx context.Context, parentBuildID uuid.UUID) (database.WorkspaceBuildOrchestration, error) { + start := time.Now() + r0, r1 := m.s.GetWorkspaceBuildOrchestrationByParentBuildID(ctx, parentBuildID) + m.queryLatencies.WithLabelValues("GetWorkspaceBuildOrchestrationByParentBuildID").Observe(time.Since(start).Seconds()) + m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetWorkspaceBuildOrchestrationByParentBuildID").Inc() + return r0, r1 +} + func (m queryMetricsStore) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]database.WorkspaceBuildParameter, error) { start := time.Now() r0, r1 := m.s.GetWorkspaceBuildParameters(ctx, workspaceBuildID) @@ -4273,6 +4289,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) @@ -5609,6 +5633,38 @@ func (m queryMetricsStore) UpdateWorkspaceBuildFlagsByID(ctx context.Context, ar 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) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 36f8429e8fa..c6ec567858d 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -3960,6 +3960,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() @@ -6450,6 +6465,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaceBuildMetricsByResourceID(ctx, id an return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceBuildMetricsByResourceID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceBuildMetricsByResourceID), ctx, id) } +// GetWorkspaceBuildOrchestrationByParentBuildID mocks base method. +func (m *MockStore) GetWorkspaceBuildOrchestrationByParentBuildID(ctx context.Context, parentBuildID uuid.UUID) (database.WorkspaceBuildOrchestration, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWorkspaceBuildOrchestrationByParentBuildID", ctx, parentBuildID) + ret0, _ := ret[0].(database.WorkspaceBuildOrchestration) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetWorkspaceBuildOrchestrationByParentBuildID indicates an expected call of GetWorkspaceBuildOrchestrationByParentBuildID. +func (mr *MockStoreMockRecorder) GetWorkspaceBuildOrchestrationByParentBuildID(ctx, parentBuildID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceBuildOrchestrationByParentBuildID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceBuildOrchestrationByParentBuildID), ctx, parentBuildID) +} + // GetWorkspaceBuildParameters mocks base method. func (m *MockStore) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]database.WorkspaceBuildParameter, error) { m.ctrl.T.Helper() @@ -8010,6 +8040,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() @@ -10557,6 +10602,66 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceBuildFlagsByID(ctx, arg any) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceBuildFlagsByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceBuildFlagsByID), 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() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 9c2df36cabd..8e96846ed4a 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -3440,6 +3440,40 @@ 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, + 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_rich_parameter_values_chec CHECK ((jsonb_typeof(child_rich_parameter_values) = 'array'::text)), + 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.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, @@ -4049,6 +4083,15 @@ 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); @@ -4287,6 +4330,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); @@ -4866,6 +4911,18 @@ 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_orchestration_child_template_version_prese_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_build_id_fkey FOREIGN KEY (child_build_id) REFERENCES workspace_builds(id) ON DELETE CASCADE; + +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_id_fkey FOREIGN KEY (parent_build_id) REFERENCES workspace_builds(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; diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index 624f3229b6e..9ebc974e0c9 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -6,146 +6,150 @@ type ForeignKeyConstraint string // ForeignKeyConstraint enums. const ( - ForeignKeyAiProviderKeysAPIKeyKeyID ForeignKeyConstraint = "ai_provider_keys_api_key_key_id_fkey" // ALTER TABLE ONLY ai_provider_keys ADD CONSTRAINT ai_provider_keys_api_key_key_id_fkey FOREIGN KEY (api_key_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyAiProviderKeysProviderID ForeignKeyConstraint = "ai_provider_keys_provider_id_fkey" // ALTER TABLE ONLY ai_provider_keys ADD CONSTRAINT ai_provider_keys_provider_id_fkey FOREIGN KEY (provider_id) REFERENCES ai_providers(id) ON DELETE CASCADE; - ForeignKeyAiProvidersSettingsKeyID ForeignKeyConstraint = "ai_providers_settings_key_id_fkey" // ALTER TABLE ONLY ai_providers ADD CONSTRAINT ai_providers_settings_key_id_fkey FOREIGN KEY (settings_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyAiSeatStateUserID ForeignKeyConstraint = "ai_seat_state_user_id_fkey" // ALTER TABLE ONLY ai_seat_state ADD CONSTRAINT ai_seat_state_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyAibridgeInterceptionsInitiatorID ForeignKeyConstraint = "aibridge_interceptions_initiator_id_fkey" // ALTER TABLE ONLY aibridge_interceptions ADD CONSTRAINT aibridge_interceptions_initiator_id_fkey FOREIGN KEY (initiator_id) REFERENCES users(id); - ForeignKeyAPIKeysUserIDUUID ForeignKeyConstraint = "api_keys_user_id_uuid_fkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyBoundaryLogsSessionID ForeignKeyConstraint = "boundary_logs_session_id_fkey" // ALTER TABLE ONLY boundary_logs ADD CONSTRAINT boundary_logs_session_id_fkey FOREIGN KEY (session_id) REFERENCES boundary_sessions(id) ON DELETE CASCADE; - ForeignKeyBoundarySessionsWorkspaceAgentID ForeignKeyConstraint = "boundary_sessions_workspace_agent_id_fkey" // ALTER TABLE ONLY boundary_sessions ADD CONSTRAINT boundary_sessions_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id); - ForeignKeyChatDebugRunsChatID ForeignKeyConstraint = "chat_debug_runs_chat_id_fkey" // ALTER TABLE ONLY chat_debug_runs ADD CONSTRAINT chat_debug_runs_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; - ForeignKeyChatDebugStepsChatID ForeignKeyConstraint = "chat_debug_steps_chat_id_fkey" // ALTER TABLE ONLY chat_debug_steps ADD CONSTRAINT chat_debug_steps_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; - ForeignKeyChatDiffStatusesChatID ForeignKeyConstraint = "chat_diff_statuses_chat_id_fkey" // ALTER TABLE ONLY chat_diff_statuses ADD CONSTRAINT chat_diff_statuses_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; - ForeignKeyChatFileLinksChatID ForeignKeyConstraint = "chat_file_links_chat_id_fkey" // ALTER TABLE ONLY chat_file_links ADD CONSTRAINT chat_file_links_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; - ForeignKeyChatFileLinksFileID ForeignKeyConstraint = "chat_file_links_file_id_fkey" // ALTER TABLE ONLY chat_file_links ADD CONSTRAINT chat_file_links_file_id_fkey FOREIGN KEY (file_id) REFERENCES chat_files(id) ON DELETE CASCADE; - ForeignKeyChatFilesOrganizationID ForeignKeyConstraint = "chat_files_organization_id_fkey" // ALTER TABLE ONLY chat_files ADD CONSTRAINT chat_files_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyChatFilesOwnerID ForeignKeyConstraint = "chat_files_owner_id_fkey" // ALTER TABLE ONLY chat_files ADD CONSTRAINT chat_files_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyChatMessagesAPIKeyID ForeignKeyConstraint = "chat_messages_api_key_id_fkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_api_key_id_fkey FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE SET NULL; - ForeignKeyChatMessagesChatID ForeignKeyConstraint = "chat_messages_chat_id_fkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; - ForeignKeyChatMessagesModelConfigID ForeignKeyConstraint = "chat_messages_model_config_id_fkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_model_config_id_fkey FOREIGN KEY (model_config_id) REFERENCES chat_model_configs(id); - ForeignKeyChatModelConfigsAiProviderID ForeignKeyConstraint = "chat_model_configs_ai_provider_id_fkey" // ALTER TABLE ONLY chat_model_configs ADD CONSTRAINT chat_model_configs_ai_provider_id_fkey FOREIGN KEY (ai_provider_id) REFERENCES ai_providers(id); - ForeignKeyChatModelConfigsCreatedBy ForeignKeyConstraint = "chat_model_configs_created_by_fkey" // ALTER TABLE ONLY chat_model_configs ADD CONSTRAINT chat_model_configs_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id); - ForeignKeyChatModelConfigsUpdatedBy ForeignKeyConstraint = "chat_model_configs_updated_by_fkey" // ALTER TABLE ONLY chat_model_configs ADD CONSTRAINT chat_model_configs_updated_by_fkey FOREIGN KEY (updated_by) REFERENCES users(id); - ForeignKeyChatQueuedMessagesAPIKeyID ForeignKeyConstraint = "chat_queued_messages_api_key_id_fkey" // ALTER TABLE ONLY chat_queued_messages ADD CONSTRAINT chat_queued_messages_api_key_id_fkey FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE SET NULL; - ForeignKeyChatQueuedMessagesChatID ForeignKeyConstraint = "chat_queued_messages_chat_id_fkey" // ALTER TABLE ONLY chat_queued_messages ADD CONSTRAINT chat_queued_messages_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; - ForeignKeyChatsAgentID ForeignKeyConstraint = "chats_agent_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE SET NULL; - ForeignKeyChatsBuildID ForeignKeyConstraint = "chats_build_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_build_id_fkey FOREIGN KEY (build_id) REFERENCES workspace_builds(id) ON DELETE SET NULL; - ForeignKeyChatsLastModelConfigID ForeignKeyConstraint = "chats_last_model_config_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_last_model_config_id_fkey FOREIGN KEY (last_model_config_id) REFERENCES chat_model_configs(id); - ForeignKeyChatsOrganizationID ForeignKeyConstraint = "chats_organization_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyChatsOwnerID ForeignKeyConstraint = "chats_owner_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyChatsParentChatID ForeignKeyConstraint = "chats_parent_chat_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_parent_chat_id_fkey FOREIGN KEY (parent_chat_id) REFERENCES chats(id) ON DELETE SET NULL; - ForeignKeyChatsRootChatID ForeignKeyConstraint = "chats_root_chat_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_root_chat_id_fkey FOREIGN KEY (root_chat_id) REFERENCES chats(id) ON DELETE SET NULL; - ForeignKeyChatsWorkspaceID ForeignKeyConstraint = "chats_workspace_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE SET NULL; - ForeignKeyConnectionLogsOrganizationID ForeignKeyConstraint = "connection_logs_organization_id_fkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyConnectionLogsWorkspaceID ForeignKeyConstraint = "connection_logs_workspace_id_fkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; - ForeignKeyConnectionLogsWorkspaceOwnerID ForeignKeyConstraint = "connection_logs_workspace_owner_id_fkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_workspace_owner_id_fkey FOREIGN KEY (workspace_owner_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyCryptoKeysSecretKeyID ForeignKeyConstraint = "crypto_keys_secret_key_id_fkey" // ALTER TABLE ONLY crypto_keys ADD CONSTRAINT crypto_keys_secret_key_id_fkey FOREIGN KEY (secret_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyFkChatDebugStepsRunChat ForeignKeyConstraint = "fk_chat_debug_steps_run_chat" // ALTER TABLE ONLY chat_debug_steps ADD CONSTRAINT fk_chat_debug_steps_run_chat FOREIGN KEY (run_id, chat_id) REFERENCES chat_debug_runs(id, chat_id) ON DELETE CASCADE; - ForeignKeyFkOauth2ProviderAppTokensUserID ForeignKeyConstraint = "fk_oauth2_provider_app_tokens_user_id" // ALTER TABLE ONLY oauth2_provider_app_tokens ADD CONSTRAINT fk_oauth2_provider_app_tokens_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyGitAuthLinksOauthAccessTokenKeyID ForeignKeyConstraint = "git_auth_links_oauth_access_token_key_id_fkey" // ALTER TABLE ONLY external_auth_links ADD CONSTRAINT git_auth_links_oauth_access_token_key_id_fkey FOREIGN KEY (oauth_access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyGitAuthLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "git_auth_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY external_auth_links ADD CONSTRAINT git_auth_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyGitSSHKeysUserID ForeignKeyConstraint = "gitsshkeys_user_id_fkey" // ALTER TABLE ONLY gitsshkeys ADD CONSTRAINT gitsshkeys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); - ForeignKeyGroupAiBudgetsGroupID ForeignKeyConstraint = "group_ai_budgets_group_id_fkey" // ALTER TABLE ONLY group_ai_budgets ADD CONSTRAINT group_ai_budgets_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE; - ForeignKeyGroupMembersGroupID ForeignKeyConstraint = "group_members_group_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE; - ForeignKeyGroupMembersUserID ForeignKeyConstraint = "group_members_user_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyGroupsOrganizationID ForeignKeyConstraint = "groups_organization_id_fkey" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyInboxNotificationsTemplateID ForeignKeyConstraint = "inbox_notifications_template_id_fkey" // ALTER TABLE ONLY inbox_notifications ADD CONSTRAINT inbox_notifications_template_id_fkey FOREIGN KEY (template_id) REFERENCES notification_templates(id) ON DELETE CASCADE; - ForeignKeyInboxNotificationsUserID ForeignKeyConstraint = "inbox_notifications_user_id_fkey" // ALTER TABLE ONLY inbox_notifications ADD CONSTRAINT inbox_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyJfrogXrayScansAgentID ForeignKeyConstraint = "jfrog_xray_scans_agent_id_fkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyJfrogXrayScansWorkspaceID ForeignKeyConstraint = "jfrog_xray_scans_workspace_id_fkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; - ForeignKeyMcpServerConfigsAPIKeyValueKeyID ForeignKeyConstraint = "mcp_server_configs_api_key_value_key_id_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_api_key_value_key_id_fkey FOREIGN KEY (api_key_value_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyMcpServerConfigsCreatedBy ForeignKeyConstraint = "mcp_server_configs_created_by_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL; - ForeignKeyMcpServerConfigsCustomHeadersKeyID ForeignKeyConstraint = "mcp_server_configs_custom_headers_key_id_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_custom_headers_key_id_fkey FOREIGN KEY (custom_headers_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyMcpServerConfigsOauth2ClientSecretKeyID ForeignKeyConstraint = "mcp_server_configs_oauth2_client_secret_key_id_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_oauth2_client_secret_key_id_fkey FOREIGN KEY (oauth2_client_secret_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyMcpServerConfigsUpdatedBy ForeignKeyConstraint = "mcp_server_configs_updated_by_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_updated_by_fkey FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL; - ForeignKeyMcpServerUserTokensAccessTokenKeyID ForeignKeyConstraint = "mcp_server_user_tokens_access_token_key_id_fkey" // ALTER TABLE ONLY mcp_server_user_tokens ADD CONSTRAINT mcp_server_user_tokens_access_token_key_id_fkey FOREIGN KEY (access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyMcpServerUserTokensMcpServerConfigID ForeignKeyConstraint = "mcp_server_user_tokens_mcp_server_config_id_fkey" // ALTER TABLE ONLY mcp_server_user_tokens ADD CONSTRAINT mcp_server_user_tokens_mcp_server_config_id_fkey FOREIGN KEY (mcp_server_config_id) REFERENCES mcp_server_configs(id) ON DELETE CASCADE; - ForeignKeyMcpServerUserTokensRefreshTokenKeyID ForeignKeyConstraint = "mcp_server_user_tokens_refresh_token_key_id_fkey" // ALTER TABLE ONLY mcp_server_user_tokens ADD CONSTRAINT mcp_server_user_tokens_refresh_token_key_id_fkey FOREIGN KEY (refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyMcpServerUserTokensUserID ForeignKeyConstraint = "mcp_server_user_tokens_user_id_fkey" // ALTER TABLE ONLY mcp_server_user_tokens ADD CONSTRAINT mcp_server_user_tokens_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyNotificationMessagesNotificationTemplateID ForeignKeyConstraint = "notification_messages_notification_template_id_fkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE; - ForeignKeyNotificationMessagesUserID ForeignKeyConstraint = "notification_messages_user_id_fkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyNotificationPreferencesNotificationTemplateID ForeignKeyConstraint = "notification_preferences_notification_template_id_fkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE; - ForeignKeyNotificationPreferencesUserID ForeignKeyConstraint = "notification_preferences_user_id_fkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyOauth2ProviderAppCodesAppID ForeignKeyConstraint = "oauth2_provider_app_codes_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE; - ForeignKeyOauth2ProviderAppCodesUserID ForeignKeyConstraint = "oauth2_provider_app_codes_user_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyOauth2ProviderAppSecretsAppID ForeignKeyConstraint = "oauth2_provider_app_secrets_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_secrets ADD CONSTRAINT oauth2_provider_app_secrets_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE; - ForeignKeyOauth2ProviderAppTokensAPIKeyID ForeignKeyConstraint = "oauth2_provider_app_tokens_api_key_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_tokens ADD CONSTRAINT oauth2_provider_app_tokens_api_key_id_fkey FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE CASCADE; - ForeignKeyOauth2ProviderAppTokensAppSecretID ForeignKeyConstraint = "oauth2_provider_app_tokens_app_secret_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_tokens ADD CONSTRAINT oauth2_provider_app_tokens_app_secret_id_fkey FOREIGN KEY (app_secret_id) REFERENCES oauth2_provider_app_secrets(id) ON DELETE CASCADE; - ForeignKeyOrganizationMembersOrganizationIDUUID ForeignKeyConstraint = "organization_members_organization_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_organization_id_uuid_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyOrganizationMembersUserIDUUID ForeignKeyConstraint = "organization_members_user_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyParameterSchemasJobID ForeignKeyConstraint = "parameter_schemas_job_id_fkey" // ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; - ForeignKeyProvisionerDaemonsKeyID ForeignKeyConstraint = "provisioner_daemons_key_id_fkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_key_id_fkey FOREIGN KEY (key_id) REFERENCES provisioner_keys(id) ON DELETE CASCADE; - ForeignKeyProvisionerDaemonsOrganizationID ForeignKeyConstraint = "provisioner_daemons_organization_id_fkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyProvisionerJobLogsJobID ForeignKeyConstraint = "provisioner_job_logs_job_id_fkey" // ALTER TABLE ONLY provisioner_job_logs ADD CONSTRAINT provisioner_job_logs_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; - ForeignKeyProvisionerJobTimingsJobID ForeignKeyConstraint = "provisioner_job_timings_job_id_fkey" // ALTER TABLE ONLY provisioner_job_timings ADD CONSTRAINT provisioner_job_timings_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; - ForeignKeyProvisionerJobsOrganizationID ForeignKeyConstraint = "provisioner_jobs_organization_id_fkey" // ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyProvisionerKeysOrganizationID ForeignKeyConstraint = "provisioner_keys_organization_id_fkey" // ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyTailnetPeersCoordinatorID ForeignKeyConstraint = "tailnet_peers_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_peers ADD CONSTRAINT tailnet_peers_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; - ForeignKeyTailnetTunnelsCoordinatorID ForeignKeyConstraint = "tailnet_tunnels_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; - ForeignKeyTaskSnapshotsTaskID ForeignKeyConstraint = "task_snapshots_task_id_fkey" // ALTER TABLE ONLY task_snapshots ADD CONSTRAINT task_snapshots_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; - ForeignKeyTaskWorkspaceAppsTaskID ForeignKeyConstraint = "task_workspace_apps_task_id_fkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; - ForeignKeyTaskWorkspaceAppsWorkspaceAgentID ForeignKeyConstraint = "task_workspace_apps_workspace_agent_id_fkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyTaskWorkspaceAppsWorkspaceAppID ForeignKeyConstraint = "task_workspace_apps_workspace_app_id_fkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_workspace_app_id_fkey FOREIGN KEY (workspace_app_id) REFERENCES workspace_apps(id) ON DELETE CASCADE; - ForeignKeyTasksOrganizationID ForeignKeyConstraint = "tasks_organization_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyTasksOwnerID ForeignKeyConstraint = "tasks_owner_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyTasksTemplateVersionID ForeignKeyConstraint = "tasks_template_version_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; - ForeignKeyTasksWorkspaceID ForeignKeyConstraint = "tasks_workspace_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionParametersTemplateVersionID ForeignKeyConstraint = "template_version_parameters_template_version_id_fkey" // ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionPresetParametTemplateVersionPresetID ForeignKeyConstraint = "template_version_preset_paramet_template_version_preset_id_fkey" // ALTER TABLE ONLY template_version_preset_parameters ADD CONSTRAINT template_version_preset_paramet_template_version_preset_id_fkey FOREIGN KEY (template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionPresetPrebuildSchedulesPresetID ForeignKeyConstraint = "template_version_preset_prebuild_schedules_preset_id_fkey" // ALTER TABLE ONLY template_version_preset_prebuild_schedules ADD CONSTRAINT template_version_preset_prebuild_schedules_preset_id_fkey FOREIGN KEY (preset_id) REFERENCES template_version_presets(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionPresetsTemplateVersionID ForeignKeyConstraint = "template_version_presets_template_version_id_fkey" // ALTER TABLE ONLY template_version_presets ADD CONSTRAINT template_version_presets_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionTerraformValuesCachedModuleFiles ForeignKeyConstraint = "template_version_terraform_values_cached_module_files_fkey" // ALTER TABLE ONLY template_version_terraform_values ADD CONSTRAINT template_version_terraform_values_cached_module_files_fkey FOREIGN KEY (cached_module_files) REFERENCES files(id); - ForeignKeyTemplateVersionTerraformValuesTemplateVersionID ForeignKeyConstraint = "template_version_terraform_values_template_version_id_fkey" // ALTER TABLE ONLY template_version_terraform_values ADD CONSTRAINT template_version_terraform_values_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionVariablesTemplateVersionID ForeignKeyConstraint = "template_version_variables_template_version_id_fkey" // ALTER TABLE ONLY template_version_variables ADD CONSTRAINT template_version_variables_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionWorkspaceTagsTemplateVersionID ForeignKeyConstraint = "template_version_workspace_tags_template_version_id_fkey" // ALTER TABLE ONLY template_version_workspace_tags ADD CONSTRAINT template_version_workspace_tags_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionsCreatedBy ForeignKeyConstraint = "template_versions_created_by_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT; - ForeignKeyTemplateVersionsOrganizationID ForeignKeyConstraint = "template_versions_organization_id_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionsTemplateID ForeignKeyConstraint = "template_versions_template_id_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_template_id_fkey FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE CASCADE; - ForeignKeyTemplatesCreatedBy ForeignKeyConstraint = "templates_created_by_fkey" // ALTER TABLE ONLY templates ADD CONSTRAINT templates_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT; - ForeignKeyTemplatesOrganizationID ForeignKeyConstraint = "templates_organization_id_fkey" // ALTER TABLE ONLY templates ADD CONSTRAINT templates_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyUserAiProviderKeysAiProviderID ForeignKeyConstraint = "user_ai_provider_keys_ai_provider_id_fkey" // ALTER TABLE ONLY user_ai_provider_keys ADD CONSTRAINT user_ai_provider_keys_ai_provider_id_fkey FOREIGN KEY (ai_provider_id) REFERENCES ai_providers(id) ON DELETE CASCADE; - ForeignKeyUserAiProviderKeysAPIKeyKeyID ForeignKeyConstraint = "user_ai_provider_keys_api_key_key_id_fkey" // ALTER TABLE ONLY user_ai_provider_keys ADD CONSTRAINT user_ai_provider_keys_api_key_key_id_fkey FOREIGN KEY (api_key_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyUserAiProviderKeysUserID ForeignKeyConstraint = "user_ai_provider_keys_user_id_fkey" // ALTER TABLE ONLY user_ai_provider_keys ADD CONSTRAINT user_ai_provider_keys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyUserConfigsUserID ForeignKeyConstraint = "user_configs_user_id_fkey" // ALTER TABLE ONLY user_configs ADD CONSTRAINT user_configs_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyUserDeletedUserID ForeignKeyConstraint = "user_deleted_user_id_fkey" // ALTER TABLE ONLY user_deleted ADD CONSTRAINT user_deleted_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); - ForeignKeyUserLinksOauthAccessTokenKeyID ForeignKeyConstraint = "user_links_oauth_access_token_key_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_oauth_access_token_key_id_fkey FOREIGN KEY (oauth_access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyUserLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "user_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyUserLinksUserID ForeignKeyConstraint = "user_links_user_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyUserSecretsUserID ForeignKeyConstraint = "user_secrets_user_id_fkey" // ALTER TABLE ONLY user_secrets ADD CONSTRAINT user_secrets_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyUserSecretsValueKeyID ForeignKeyConstraint = "user_secrets_value_key_id_fkey" // ALTER TABLE ONLY user_secrets ADD CONSTRAINT user_secrets_value_key_id_fkey FOREIGN KEY (value_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyUserSkillsUserID ForeignKeyConstraint = "user_skills_user_id_fkey" // ALTER TABLE ONLY user_skills ADD CONSTRAINT user_skills_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyUserStatusChangesUserID ForeignKeyConstraint = "user_status_changes_user_id_fkey" // ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); - ForeignKeyWebpushSubscriptionsUserID ForeignKeyConstraint = "webpush_subscriptions_user_id_fkey" // ALTER TABLE ONLY webpush_subscriptions ADD CONSTRAINT webpush_subscriptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentDevcontainersSubagentID ForeignKeyConstraint = "workspace_agent_devcontainers_subagent_id_fkey" // ALTER TABLE ONLY workspace_agent_devcontainers ADD CONSTRAINT workspace_agent_devcontainers_subagent_id_fkey FOREIGN KEY (subagent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentDevcontainersWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_devcontainers_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_devcontainers ADD CONSTRAINT workspace_agent_devcontainers_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentLogSourcesWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_log_sources_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentMemoryResourceMonitorsAgentID ForeignKeyConstraint = "workspace_agent_memory_resource_monitors_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_memory_resource_monitors ADD CONSTRAINT workspace_agent_memory_resource_monitors_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentMetadataWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_metadata_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_metadata ADD CONSTRAINT workspace_agent_metadata_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentPortShareWorkspaceID ForeignKeyConstraint = "workspace_agent_port_share_workspace_id_fkey" // ALTER TABLE ONLY workspace_agent_port_share ADD CONSTRAINT workspace_agent_port_share_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentScriptTimingsScriptID ForeignKeyConstraint = "workspace_agent_script_timings_script_id_fkey" // ALTER TABLE ONLY workspace_agent_script_timings ADD CONSTRAINT workspace_agent_script_timings_script_id_fkey FOREIGN KEY (script_id) REFERENCES workspace_agent_scripts(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentScriptsWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_scripts_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_scripts ADD CONSTRAINT workspace_agent_scripts_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentStartupLogsAgentID ForeignKeyConstraint = "workspace_agent_startup_logs_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_logs ADD CONSTRAINT workspace_agent_startup_logs_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentVolumeResourceMonitorsAgentID ForeignKeyConstraint = "workspace_agent_volume_resource_monitors_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_volume_resource_monitors ADD CONSTRAINT workspace_agent_volume_resource_monitors_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentsParentID ForeignKeyConstraint = "workspace_agents_parent_id_fkey" // ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentsResourceID ForeignKeyConstraint = "workspace_agents_resource_id_fkey" // ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAppAuditSessionsAgentID ForeignKeyConstraint = "workspace_app_audit_sessions_agent_id_fkey" // ALTER TABLE ONLY workspace_app_audit_sessions ADD CONSTRAINT workspace_app_audit_sessions_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAppStatsAgentID ForeignKeyConstraint = "workspace_app_stats_agent_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id); - ForeignKeyWorkspaceAppStatsUserID ForeignKeyConstraint = "workspace_app_stats_user_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); - ForeignKeyWorkspaceAppStatsWorkspaceID ForeignKeyConstraint = "workspace_app_stats_workspace_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id); - ForeignKeyWorkspaceAppStatusesAgentID ForeignKeyConstraint = "workspace_app_statuses_agent_id_fkey" // ALTER TABLE ONLY workspace_app_statuses ADD CONSTRAINT workspace_app_statuses_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id); - 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; - 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; - ForeignKeyWorkspaceBuildsTemplateVersionPresetID ForeignKeyConstraint = "workspace_builds_template_version_preset_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_template_version_preset_id_fkey FOREIGN KEY (template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE SET NULL; - ForeignKeyWorkspaceBuildsWorkspaceID ForeignKeyConstraint = "workspace_builds_workspace_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; - ForeignKeyWorkspaceModulesJobID ForeignKeyConstraint = "workspace_modules_job_id_fkey" // ALTER TABLE ONLY workspace_modules ADD CONSTRAINT workspace_modules_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; - ForeignKeyWorkspaceResourceMetadataWorkspaceResourceID ForeignKeyConstraint = "workspace_resource_metadata_workspace_resource_id_fkey" // ALTER TABLE ONLY workspace_resource_metadata ADD CONSTRAINT workspace_resource_metadata_workspace_resource_id_fkey FOREIGN KEY (workspace_resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE; - ForeignKeyWorkspaceResourcesJobID ForeignKeyConstraint = "workspace_resources_job_id_fkey" // ALTER TABLE ONLY workspace_resources ADD CONSTRAINT workspace_resources_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; - ForeignKeyWorkspacesOrganizationID ForeignKeyConstraint = "workspaces_organization_id_fkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT; - ForeignKeyWorkspacesOwnerID ForeignKeyConstraint = "workspaces_owner_id_fkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT; - ForeignKeyWorkspacesTemplateID ForeignKeyConstraint = "workspaces_template_id_fkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_template_id_fkey FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE RESTRICT; + ForeignKeyAiProviderKeysAPIKeyKeyID ForeignKeyConstraint = "ai_provider_keys_api_key_key_id_fkey" // ALTER TABLE ONLY ai_provider_keys ADD CONSTRAINT ai_provider_keys_api_key_key_id_fkey FOREIGN KEY (api_key_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyAiProviderKeysProviderID ForeignKeyConstraint = "ai_provider_keys_provider_id_fkey" // ALTER TABLE ONLY ai_provider_keys ADD CONSTRAINT ai_provider_keys_provider_id_fkey FOREIGN KEY (provider_id) REFERENCES ai_providers(id) ON DELETE CASCADE; + ForeignKeyAiProvidersSettingsKeyID ForeignKeyConstraint = "ai_providers_settings_key_id_fkey" // ALTER TABLE ONLY ai_providers ADD CONSTRAINT ai_providers_settings_key_id_fkey FOREIGN KEY (settings_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyAiSeatStateUserID ForeignKeyConstraint = "ai_seat_state_user_id_fkey" // ALTER TABLE ONLY ai_seat_state ADD CONSTRAINT ai_seat_state_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyAibridgeInterceptionsInitiatorID ForeignKeyConstraint = "aibridge_interceptions_initiator_id_fkey" // ALTER TABLE ONLY aibridge_interceptions ADD CONSTRAINT aibridge_interceptions_initiator_id_fkey FOREIGN KEY (initiator_id) REFERENCES users(id); + ForeignKeyAPIKeysUserIDUUID ForeignKeyConstraint = "api_keys_user_id_uuid_fkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyBoundaryLogsSessionID ForeignKeyConstraint = "boundary_logs_session_id_fkey" // ALTER TABLE ONLY boundary_logs ADD CONSTRAINT boundary_logs_session_id_fkey FOREIGN KEY (session_id) REFERENCES boundary_sessions(id) ON DELETE CASCADE; + ForeignKeyBoundarySessionsWorkspaceAgentID ForeignKeyConstraint = "boundary_sessions_workspace_agent_id_fkey" // ALTER TABLE ONLY boundary_sessions ADD CONSTRAINT boundary_sessions_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id); + ForeignKeyChatDebugRunsChatID ForeignKeyConstraint = "chat_debug_runs_chat_id_fkey" // ALTER TABLE ONLY chat_debug_runs ADD CONSTRAINT chat_debug_runs_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; + ForeignKeyChatDebugStepsChatID ForeignKeyConstraint = "chat_debug_steps_chat_id_fkey" // ALTER TABLE ONLY chat_debug_steps ADD CONSTRAINT chat_debug_steps_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; + ForeignKeyChatDiffStatusesChatID ForeignKeyConstraint = "chat_diff_statuses_chat_id_fkey" // ALTER TABLE ONLY chat_diff_statuses ADD CONSTRAINT chat_diff_statuses_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; + ForeignKeyChatFileLinksChatID ForeignKeyConstraint = "chat_file_links_chat_id_fkey" // ALTER TABLE ONLY chat_file_links ADD CONSTRAINT chat_file_links_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; + ForeignKeyChatFileLinksFileID ForeignKeyConstraint = "chat_file_links_file_id_fkey" // ALTER TABLE ONLY chat_file_links ADD CONSTRAINT chat_file_links_file_id_fkey FOREIGN KEY (file_id) REFERENCES chat_files(id) ON DELETE CASCADE; + ForeignKeyChatFilesOrganizationID ForeignKeyConstraint = "chat_files_organization_id_fkey" // ALTER TABLE ONLY chat_files ADD CONSTRAINT chat_files_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyChatFilesOwnerID ForeignKeyConstraint = "chat_files_owner_id_fkey" // ALTER TABLE ONLY chat_files ADD CONSTRAINT chat_files_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyChatMessagesAPIKeyID ForeignKeyConstraint = "chat_messages_api_key_id_fkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_api_key_id_fkey FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE SET NULL; + ForeignKeyChatMessagesChatID ForeignKeyConstraint = "chat_messages_chat_id_fkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; + ForeignKeyChatMessagesModelConfigID ForeignKeyConstraint = "chat_messages_model_config_id_fkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_model_config_id_fkey FOREIGN KEY (model_config_id) REFERENCES chat_model_configs(id); + ForeignKeyChatModelConfigsAiProviderID ForeignKeyConstraint = "chat_model_configs_ai_provider_id_fkey" // ALTER TABLE ONLY chat_model_configs ADD CONSTRAINT chat_model_configs_ai_provider_id_fkey FOREIGN KEY (ai_provider_id) REFERENCES ai_providers(id); + ForeignKeyChatModelConfigsCreatedBy ForeignKeyConstraint = "chat_model_configs_created_by_fkey" // ALTER TABLE ONLY chat_model_configs ADD CONSTRAINT chat_model_configs_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id); + ForeignKeyChatModelConfigsUpdatedBy ForeignKeyConstraint = "chat_model_configs_updated_by_fkey" // ALTER TABLE ONLY chat_model_configs ADD CONSTRAINT chat_model_configs_updated_by_fkey FOREIGN KEY (updated_by) REFERENCES users(id); + ForeignKeyChatQueuedMessagesAPIKeyID ForeignKeyConstraint = "chat_queued_messages_api_key_id_fkey" // ALTER TABLE ONLY chat_queued_messages ADD CONSTRAINT chat_queued_messages_api_key_id_fkey FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE SET NULL; + ForeignKeyChatQueuedMessagesChatID ForeignKeyConstraint = "chat_queued_messages_chat_id_fkey" // ALTER TABLE ONLY chat_queued_messages ADD CONSTRAINT chat_queued_messages_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; + ForeignKeyChatsAgentID ForeignKeyConstraint = "chats_agent_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE SET NULL; + ForeignKeyChatsBuildID ForeignKeyConstraint = "chats_build_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_build_id_fkey FOREIGN KEY (build_id) REFERENCES workspace_builds(id) ON DELETE SET NULL; + ForeignKeyChatsLastModelConfigID ForeignKeyConstraint = "chats_last_model_config_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_last_model_config_id_fkey FOREIGN KEY (last_model_config_id) REFERENCES chat_model_configs(id); + ForeignKeyChatsOrganizationID ForeignKeyConstraint = "chats_organization_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyChatsOwnerID ForeignKeyConstraint = "chats_owner_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyChatsParentChatID ForeignKeyConstraint = "chats_parent_chat_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_parent_chat_id_fkey FOREIGN KEY (parent_chat_id) REFERENCES chats(id) ON DELETE SET NULL; + ForeignKeyChatsRootChatID ForeignKeyConstraint = "chats_root_chat_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_root_chat_id_fkey FOREIGN KEY (root_chat_id) REFERENCES chats(id) ON DELETE SET NULL; + ForeignKeyChatsWorkspaceID ForeignKeyConstraint = "chats_workspace_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE SET NULL; + ForeignKeyConnectionLogsOrganizationID ForeignKeyConstraint = "connection_logs_organization_id_fkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyConnectionLogsWorkspaceID ForeignKeyConstraint = "connection_logs_workspace_id_fkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; + ForeignKeyConnectionLogsWorkspaceOwnerID ForeignKeyConstraint = "connection_logs_workspace_owner_id_fkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_workspace_owner_id_fkey FOREIGN KEY (workspace_owner_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyCryptoKeysSecretKeyID ForeignKeyConstraint = "crypto_keys_secret_key_id_fkey" // ALTER TABLE ONLY crypto_keys ADD CONSTRAINT crypto_keys_secret_key_id_fkey FOREIGN KEY (secret_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyFkChatDebugStepsRunChat ForeignKeyConstraint = "fk_chat_debug_steps_run_chat" // ALTER TABLE ONLY chat_debug_steps ADD CONSTRAINT fk_chat_debug_steps_run_chat FOREIGN KEY (run_id, chat_id) REFERENCES chat_debug_runs(id, chat_id) ON DELETE CASCADE; + ForeignKeyFkOauth2ProviderAppTokensUserID ForeignKeyConstraint = "fk_oauth2_provider_app_tokens_user_id" // ALTER TABLE ONLY oauth2_provider_app_tokens ADD CONSTRAINT fk_oauth2_provider_app_tokens_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyGitAuthLinksOauthAccessTokenKeyID ForeignKeyConstraint = "git_auth_links_oauth_access_token_key_id_fkey" // ALTER TABLE ONLY external_auth_links ADD CONSTRAINT git_auth_links_oauth_access_token_key_id_fkey FOREIGN KEY (oauth_access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyGitAuthLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "git_auth_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY external_auth_links ADD CONSTRAINT git_auth_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyGitSSHKeysUserID ForeignKeyConstraint = "gitsshkeys_user_id_fkey" // ALTER TABLE ONLY gitsshkeys ADD CONSTRAINT gitsshkeys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); + ForeignKeyGroupAiBudgetsGroupID ForeignKeyConstraint = "group_ai_budgets_group_id_fkey" // ALTER TABLE ONLY group_ai_budgets ADD CONSTRAINT group_ai_budgets_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE; + ForeignKeyGroupMembersGroupID ForeignKeyConstraint = "group_members_group_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE; + ForeignKeyGroupMembersUserID ForeignKeyConstraint = "group_members_user_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyGroupsOrganizationID ForeignKeyConstraint = "groups_organization_id_fkey" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyInboxNotificationsTemplateID ForeignKeyConstraint = "inbox_notifications_template_id_fkey" // ALTER TABLE ONLY inbox_notifications ADD CONSTRAINT inbox_notifications_template_id_fkey FOREIGN KEY (template_id) REFERENCES notification_templates(id) ON DELETE CASCADE; + ForeignKeyInboxNotificationsUserID ForeignKeyConstraint = "inbox_notifications_user_id_fkey" // ALTER TABLE ONLY inbox_notifications ADD CONSTRAINT inbox_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyJfrogXrayScansAgentID ForeignKeyConstraint = "jfrog_xray_scans_agent_id_fkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyJfrogXrayScansWorkspaceID ForeignKeyConstraint = "jfrog_xray_scans_workspace_id_fkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; + ForeignKeyMcpServerConfigsAPIKeyValueKeyID ForeignKeyConstraint = "mcp_server_configs_api_key_value_key_id_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_api_key_value_key_id_fkey FOREIGN KEY (api_key_value_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyMcpServerConfigsCreatedBy ForeignKeyConstraint = "mcp_server_configs_created_by_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL; + ForeignKeyMcpServerConfigsCustomHeadersKeyID ForeignKeyConstraint = "mcp_server_configs_custom_headers_key_id_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_custom_headers_key_id_fkey FOREIGN KEY (custom_headers_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyMcpServerConfigsOauth2ClientSecretKeyID ForeignKeyConstraint = "mcp_server_configs_oauth2_client_secret_key_id_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_oauth2_client_secret_key_id_fkey FOREIGN KEY (oauth2_client_secret_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyMcpServerConfigsUpdatedBy ForeignKeyConstraint = "mcp_server_configs_updated_by_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_updated_by_fkey FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL; + ForeignKeyMcpServerUserTokensAccessTokenKeyID ForeignKeyConstraint = "mcp_server_user_tokens_access_token_key_id_fkey" // ALTER TABLE ONLY mcp_server_user_tokens ADD CONSTRAINT mcp_server_user_tokens_access_token_key_id_fkey FOREIGN KEY (access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyMcpServerUserTokensMcpServerConfigID ForeignKeyConstraint = "mcp_server_user_tokens_mcp_server_config_id_fkey" // ALTER TABLE ONLY mcp_server_user_tokens ADD CONSTRAINT mcp_server_user_tokens_mcp_server_config_id_fkey FOREIGN KEY (mcp_server_config_id) REFERENCES mcp_server_configs(id) ON DELETE CASCADE; + ForeignKeyMcpServerUserTokensRefreshTokenKeyID ForeignKeyConstraint = "mcp_server_user_tokens_refresh_token_key_id_fkey" // ALTER TABLE ONLY mcp_server_user_tokens ADD CONSTRAINT mcp_server_user_tokens_refresh_token_key_id_fkey FOREIGN KEY (refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyMcpServerUserTokensUserID ForeignKeyConstraint = "mcp_server_user_tokens_user_id_fkey" // ALTER TABLE ONLY mcp_server_user_tokens ADD CONSTRAINT mcp_server_user_tokens_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyNotificationMessagesNotificationTemplateID ForeignKeyConstraint = "notification_messages_notification_template_id_fkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE; + ForeignKeyNotificationMessagesUserID ForeignKeyConstraint = "notification_messages_user_id_fkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyNotificationPreferencesNotificationTemplateID ForeignKeyConstraint = "notification_preferences_notification_template_id_fkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE; + ForeignKeyNotificationPreferencesUserID ForeignKeyConstraint = "notification_preferences_user_id_fkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyOauth2ProviderAppCodesAppID ForeignKeyConstraint = "oauth2_provider_app_codes_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE; + ForeignKeyOauth2ProviderAppCodesUserID ForeignKeyConstraint = "oauth2_provider_app_codes_user_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyOauth2ProviderAppSecretsAppID ForeignKeyConstraint = "oauth2_provider_app_secrets_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_secrets ADD CONSTRAINT oauth2_provider_app_secrets_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE; + ForeignKeyOauth2ProviderAppTokensAPIKeyID ForeignKeyConstraint = "oauth2_provider_app_tokens_api_key_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_tokens ADD CONSTRAINT oauth2_provider_app_tokens_api_key_id_fkey FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE CASCADE; + ForeignKeyOauth2ProviderAppTokensAppSecretID ForeignKeyConstraint = "oauth2_provider_app_tokens_app_secret_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_tokens ADD CONSTRAINT oauth2_provider_app_tokens_app_secret_id_fkey FOREIGN KEY (app_secret_id) REFERENCES oauth2_provider_app_secrets(id) ON DELETE CASCADE; + ForeignKeyOrganizationMembersOrganizationIDUUID ForeignKeyConstraint = "organization_members_organization_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_organization_id_uuid_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyOrganizationMembersUserIDUUID ForeignKeyConstraint = "organization_members_user_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyParameterSchemasJobID ForeignKeyConstraint = "parameter_schemas_job_id_fkey" // ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; + ForeignKeyProvisionerDaemonsKeyID ForeignKeyConstraint = "provisioner_daemons_key_id_fkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_key_id_fkey FOREIGN KEY (key_id) REFERENCES provisioner_keys(id) ON DELETE CASCADE; + ForeignKeyProvisionerDaemonsOrganizationID ForeignKeyConstraint = "provisioner_daemons_organization_id_fkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyProvisionerJobLogsJobID ForeignKeyConstraint = "provisioner_job_logs_job_id_fkey" // ALTER TABLE ONLY provisioner_job_logs ADD CONSTRAINT provisioner_job_logs_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; + ForeignKeyProvisionerJobTimingsJobID ForeignKeyConstraint = "provisioner_job_timings_job_id_fkey" // ALTER TABLE ONLY provisioner_job_timings ADD CONSTRAINT provisioner_job_timings_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; + ForeignKeyProvisionerJobsOrganizationID ForeignKeyConstraint = "provisioner_jobs_organization_id_fkey" // ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyProvisionerKeysOrganizationID ForeignKeyConstraint = "provisioner_keys_organization_id_fkey" // ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyTailnetPeersCoordinatorID ForeignKeyConstraint = "tailnet_peers_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_peers ADD CONSTRAINT tailnet_peers_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; + ForeignKeyTailnetTunnelsCoordinatorID ForeignKeyConstraint = "tailnet_tunnels_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; + ForeignKeyTaskSnapshotsTaskID ForeignKeyConstraint = "task_snapshots_task_id_fkey" // ALTER TABLE ONLY task_snapshots ADD CONSTRAINT task_snapshots_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; + ForeignKeyTaskWorkspaceAppsTaskID ForeignKeyConstraint = "task_workspace_apps_task_id_fkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; + ForeignKeyTaskWorkspaceAppsWorkspaceAgentID ForeignKeyConstraint = "task_workspace_apps_workspace_agent_id_fkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyTaskWorkspaceAppsWorkspaceAppID ForeignKeyConstraint = "task_workspace_apps_workspace_app_id_fkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_workspace_app_id_fkey FOREIGN KEY (workspace_app_id) REFERENCES workspace_apps(id) ON DELETE CASCADE; + ForeignKeyTasksOrganizationID ForeignKeyConstraint = "tasks_organization_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyTasksOwnerID ForeignKeyConstraint = "tasks_owner_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyTasksTemplateVersionID ForeignKeyConstraint = "tasks_template_version_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; + ForeignKeyTasksWorkspaceID ForeignKeyConstraint = "tasks_workspace_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionParametersTemplateVersionID ForeignKeyConstraint = "template_version_parameters_template_version_id_fkey" // ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionPresetParametTemplateVersionPresetID ForeignKeyConstraint = "template_version_preset_paramet_template_version_preset_id_fkey" // ALTER TABLE ONLY template_version_preset_parameters ADD CONSTRAINT template_version_preset_paramet_template_version_preset_id_fkey FOREIGN KEY (template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionPresetPrebuildSchedulesPresetID ForeignKeyConstraint = "template_version_preset_prebuild_schedules_preset_id_fkey" // ALTER TABLE ONLY template_version_preset_prebuild_schedules ADD CONSTRAINT template_version_preset_prebuild_schedules_preset_id_fkey FOREIGN KEY (preset_id) REFERENCES template_version_presets(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionPresetsTemplateVersionID ForeignKeyConstraint = "template_version_presets_template_version_id_fkey" // ALTER TABLE ONLY template_version_presets ADD CONSTRAINT template_version_presets_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionTerraformValuesCachedModuleFiles ForeignKeyConstraint = "template_version_terraform_values_cached_module_files_fkey" // ALTER TABLE ONLY template_version_terraform_values ADD CONSTRAINT template_version_terraform_values_cached_module_files_fkey FOREIGN KEY (cached_module_files) REFERENCES files(id); + ForeignKeyTemplateVersionTerraformValuesTemplateVersionID ForeignKeyConstraint = "template_version_terraform_values_template_version_id_fkey" // ALTER TABLE ONLY template_version_terraform_values ADD CONSTRAINT template_version_terraform_values_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionVariablesTemplateVersionID ForeignKeyConstraint = "template_version_variables_template_version_id_fkey" // ALTER TABLE ONLY template_version_variables ADD CONSTRAINT template_version_variables_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionWorkspaceTagsTemplateVersionID ForeignKeyConstraint = "template_version_workspace_tags_template_version_id_fkey" // ALTER TABLE ONLY template_version_workspace_tags ADD CONSTRAINT template_version_workspace_tags_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionsCreatedBy ForeignKeyConstraint = "template_versions_created_by_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT; + ForeignKeyTemplateVersionsOrganizationID ForeignKeyConstraint = "template_versions_organization_id_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionsTemplateID ForeignKeyConstraint = "template_versions_template_id_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_template_id_fkey FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE CASCADE; + ForeignKeyTemplatesCreatedBy ForeignKeyConstraint = "templates_created_by_fkey" // ALTER TABLE ONLY templates ADD CONSTRAINT templates_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT; + ForeignKeyTemplatesOrganizationID ForeignKeyConstraint = "templates_organization_id_fkey" // ALTER TABLE ONLY templates ADD CONSTRAINT templates_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyUserAiProviderKeysAiProviderID ForeignKeyConstraint = "user_ai_provider_keys_ai_provider_id_fkey" // ALTER TABLE ONLY user_ai_provider_keys ADD CONSTRAINT user_ai_provider_keys_ai_provider_id_fkey FOREIGN KEY (ai_provider_id) REFERENCES ai_providers(id) ON DELETE CASCADE; + ForeignKeyUserAiProviderKeysAPIKeyKeyID ForeignKeyConstraint = "user_ai_provider_keys_api_key_key_id_fkey" // ALTER TABLE ONLY user_ai_provider_keys ADD CONSTRAINT user_ai_provider_keys_api_key_key_id_fkey FOREIGN KEY (api_key_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyUserAiProviderKeysUserID ForeignKeyConstraint = "user_ai_provider_keys_user_id_fkey" // ALTER TABLE ONLY user_ai_provider_keys ADD CONSTRAINT user_ai_provider_keys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyUserConfigsUserID ForeignKeyConstraint = "user_configs_user_id_fkey" // ALTER TABLE ONLY user_configs ADD CONSTRAINT user_configs_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyUserDeletedUserID ForeignKeyConstraint = "user_deleted_user_id_fkey" // ALTER TABLE ONLY user_deleted ADD CONSTRAINT user_deleted_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); + ForeignKeyUserLinksOauthAccessTokenKeyID ForeignKeyConstraint = "user_links_oauth_access_token_key_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_oauth_access_token_key_id_fkey FOREIGN KEY (oauth_access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyUserLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "user_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyUserLinksUserID ForeignKeyConstraint = "user_links_user_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyUserSecretsUserID ForeignKeyConstraint = "user_secrets_user_id_fkey" // ALTER TABLE ONLY user_secrets ADD CONSTRAINT user_secrets_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyUserSecretsValueKeyID ForeignKeyConstraint = "user_secrets_value_key_id_fkey" // ALTER TABLE ONLY user_secrets ADD CONSTRAINT user_secrets_value_key_id_fkey FOREIGN KEY (value_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyUserSkillsUserID ForeignKeyConstraint = "user_skills_user_id_fkey" // ALTER TABLE ONLY user_skills ADD CONSTRAINT user_skills_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyUserStatusChangesUserID ForeignKeyConstraint = "user_status_changes_user_id_fkey" // ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); + ForeignKeyWebpushSubscriptionsUserID ForeignKeyConstraint = "webpush_subscriptions_user_id_fkey" // ALTER TABLE ONLY webpush_subscriptions ADD CONSTRAINT webpush_subscriptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentDevcontainersSubagentID ForeignKeyConstraint = "workspace_agent_devcontainers_subagent_id_fkey" // ALTER TABLE ONLY workspace_agent_devcontainers ADD CONSTRAINT workspace_agent_devcontainers_subagent_id_fkey FOREIGN KEY (subagent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentDevcontainersWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_devcontainers_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_devcontainers ADD CONSTRAINT workspace_agent_devcontainers_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentLogSourcesWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_log_sources_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentMemoryResourceMonitorsAgentID ForeignKeyConstraint = "workspace_agent_memory_resource_monitors_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_memory_resource_monitors ADD CONSTRAINT workspace_agent_memory_resource_monitors_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentMetadataWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_metadata_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_metadata ADD CONSTRAINT workspace_agent_metadata_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentPortShareWorkspaceID ForeignKeyConstraint = "workspace_agent_port_share_workspace_id_fkey" // ALTER TABLE ONLY workspace_agent_port_share ADD CONSTRAINT workspace_agent_port_share_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentScriptTimingsScriptID ForeignKeyConstraint = "workspace_agent_script_timings_script_id_fkey" // ALTER TABLE ONLY workspace_agent_script_timings ADD CONSTRAINT workspace_agent_script_timings_script_id_fkey FOREIGN KEY (script_id) REFERENCES workspace_agent_scripts(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentScriptsWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_scripts_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_scripts ADD CONSTRAINT workspace_agent_scripts_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentStartupLogsAgentID ForeignKeyConstraint = "workspace_agent_startup_logs_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_logs ADD CONSTRAINT workspace_agent_startup_logs_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentVolumeResourceMonitorsAgentID ForeignKeyConstraint = "workspace_agent_volume_resource_monitors_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_volume_resource_monitors ADD CONSTRAINT workspace_agent_volume_resource_monitors_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentsParentID ForeignKeyConstraint = "workspace_agents_parent_id_fkey" // ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentsResourceID ForeignKeyConstraint = "workspace_agents_resource_id_fkey" // ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAppAuditSessionsAgentID ForeignKeyConstraint = "workspace_app_audit_sessions_agent_id_fkey" // ALTER TABLE ONLY workspace_app_audit_sessions ADD CONSTRAINT workspace_app_audit_sessions_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAppStatsAgentID ForeignKeyConstraint = "workspace_app_stats_agent_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id); + ForeignKeyWorkspaceAppStatsUserID ForeignKeyConstraint = "workspace_app_stats_user_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); + ForeignKeyWorkspaceAppStatsWorkspaceID ForeignKeyConstraint = "workspace_app_stats_workspace_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id); + ForeignKeyWorkspaceAppStatusesAgentID ForeignKeyConstraint = "workspace_app_statuses_agent_id_fkey" // ALTER TABLE ONLY workspace_app_statuses ADD CONSTRAINT workspace_app_statuses_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id); + 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; + ForeignKeyWorkspaceBuildOrchestrationChildTemplateVersionPrese ForeignKeyConstraint = "workspace_build_orchestration_child_template_version_prese_fkey" // ALTER TABLE ONLY workspace_build_orchestrations ADD CONSTRAINT workspace_build_orchestration_child_template_version_prese_fkey FOREIGN KEY (child_template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE SET NULL; + ForeignKeyWorkspaceBuildOrchestrationsChildBuildID ForeignKeyConstraint = "workspace_build_orchestrations_child_build_id_fkey" // ALTER TABLE ONLY workspace_build_orchestrations ADD CONSTRAINT workspace_build_orchestrations_child_build_id_fkey FOREIGN KEY (child_build_id) REFERENCES workspace_builds(id) ON DELETE CASCADE; + 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; + ForeignKeyWorkspaceBuildOrchestrationsParentBuildID ForeignKeyConstraint = "workspace_build_orchestrations_parent_build_id_fkey" // ALTER TABLE ONLY workspace_build_orchestrations ADD CONSTRAINT workspace_build_orchestrations_parent_build_id_fkey FOREIGN KEY (parent_build_id) REFERENCES workspace_builds(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; + ForeignKeyWorkspaceBuildsTemplateVersionPresetID ForeignKeyConstraint = "workspace_builds_template_version_preset_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_template_version_preset_id_fkey FOREIGN KEY (template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE SET NULL; + ForeignKeyWorkspaceBuildsWorkspaceID ForeignKeyConstraint = "workspace_builds_workspace_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; + ForeignKeyWorkspaceModulesJobID ForeignKeyConstraint = "workspace_modules_job_id_fkey" // ALTER TABLE ONLY workspace_modules ADD CONSTRAINT workspace_modules_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; + ForeignKeyWorkspaceResourceMetadataWorkspaceResourceID ForeignKeyConstraint = "workspace_resource_metadata_workspace_resource_id_fkey" // ALTER TABLE ONLY workspace_resource_metadata ADD CONSTRAINT workspace_resource_metadata_workspace_resource_id_fkey FOREIGN KEY (workspace_resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE; + ForeignKeyWorkspaceResourcesJobID ForeignKeyConstraint = "workspace_resources_job_id_fkey" // ALTER TABLE ONLY workspace_resources ADD CONSTRAINT workspace_resources_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; + ForeignKeyWorkspacesOrganizationID ForeignKeyConstraint = "workspaces_organization_id_fkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT; + ForeignKeyWorkspacesOwnerID ForeignKeyConstraint = "workspaces_owner_id_fkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT; + ForeignKeyWorkspacesTemplateID ForeignKeyConstraint = "workspaces_template_id_fkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_template_id_fkey FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE RESTRICT; ) diff --git a/coderd/database/migrations/000510_workspace_build_orchestrations.down.sql b/coderd/database/migrations/000510_workspace_build_orchestrations.down.sql new file mode 100644 index 00000000000..e2dfb303488 --- /dev/null +++ b/coderd/database/migrations/000510_workspace_build_orchestrations.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS workspace_build_orchestrations; diff --git a/coderd/database/migrations/000510_workspace_build_orchestrations.up.sql b/coderd/database/migrations/000510_workspace_build_orchestrations.up.sql new file mode 100644 index 00000000000..ad6f2dd2833 --- /dev/null +++ b/coderd/database/migrations/000510_workspace_build_orchestrations.up.sql @@ -0,0 +1,58 @@ +CREATE TABLE workspace_build_orchestrations ( + id UUID PRIMARY KEY NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + parent_build_id UUID UNIQUE NOT NULL REFERENCES workspace_builds(id) ON DELETE CASCADE, + child_build_id UUID UNIQUE REFERENCES workspace_builds(id) ON DELETE CASCADE, + child_transition workspace_transition NOT NULL, + child_template_version_id UUID REFERENCES template_versions(id) ON DELETE CASCADE, + child_template_version_preset_id UUID REFERENCES template_version_presets(id) ON DELETE SET NULL, + 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_rich_parameter_values_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 + ), + -- 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') + ) +); + +-- 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.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.'; diff --git a/coderd/database/migrations/testdata/fixtures/000510_workspace_build_orchestrations.up.sql b/coderd/database/migrations/testdata/fixtures/000510_workspace_build_orchestrations.up.sql new file mode 100644 index 00000000000..6e3f5556a47 --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000510_workspace_build_orchestrations.up.sql @@ -0,0 +1,19 @@ +INSERT INTO workspace_build_orchestrations ( + id, + created_at, + updated_at, + parent_build_id, + child_transition +) +SELECT + '4e983a68-9b8a-4d4e-a4d6-5f2dd73551c2'::uuid, + NOW(), + NOW(), + id, + 'start'::workspace_transition +FROM + workspace_builds +ORDER BY + created_at, id +LIMIT 1 +ON CONFLICT DO NOTHING; diff --git a/coderd/database/models.go b/coderd/database/models.go index 940904385a4..d895882d316 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -6120,6 +6120,29 @@ 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"` + // 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 diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 6b16e0771a1..90104cd0061 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -516,6 +516,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) @@ -865,6 +868,7 @@ type sqlcQuerier interface { // Returns build metadata for e2e workspace build duration metrics. // Also checks if all agents are ready and returns the worst status. GetWorkspaceBuildMetricsByResourceID(ctx context.Context, id uuid.UUID) (GetWorkspaceBuildMetricsByResourceIDRow, error) + GetWorkspaceBuildOrchestrationByParentBuildID(ctx context.Context, parentBuildID uuid.UUID) (WorkspaceBuildOrchestration, error) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]WorkspaceBuildParameter, error) // Fetches the provisioner state of a workspace build, joined through to the // template so that dbauthz can enforce policy.ActionUpdate on the template. @@ -1011,6 +1015,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) @@ -1328,6 +1333,10 @@ type sqlcQuerier interface { UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) error UpdateWorkspaceBuildDeadlineByID(ctx context.Context, arg UpdateWorkspaceBuildDeadlineByIDParams) error UpdateWorkspaceBuildFlagsByID(ctx context.Context, arg UpdateWorkspaceBuildFlagsByIDParams) 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) diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index f181e2e94bc..53f914e7679 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -8423,6 +8423,228 @@ 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) + + 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 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) + 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 TestWorkspaceBuildDeadlineConstraint(t *testing.T) { t.Parallel() diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 2ba4b923def..0ffaa4726cd 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -32958,6 +32958,352 @@ func (q *sqlQuerier) InsertWorkspaceAppStats(ctx context.Context, arg InsertWork return err } +const getNextPendingWorkspaceBuildOrchestrationForUpdate = `-- name: GetNextPendingWorkspaceBuildOrchestrationForUpdate :one +SELECT + wbo.id, wbo.created_at, wbo.updated_at, 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.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 getWorkspaceBuildOrchestrationByParentBuildID = `-- name: GetWorkspaceBuildOrchestrationByParentBuildID :one +SELECT + id, created_at, updated_at, 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 +FROM + workspace_build_orchestrations +WHERE + parent_build_id = $1 +` + +func (q *sqlQuerier) GetWorkspaceBuildOrchestrationByParentBuildID(ctx context.Context, parentBuildID uuid.UUID) (WorkspaceBuildOrchestration, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceBuildOrchestrationByParentBuildID, parentBuildID) + var i WorkspaceBuildOrchestration + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &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, + 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, + $5, + $6, + $7, + $8, + $9, + $10, + 'pending', + NULL +) +RETURNING id, created_at, updated_at, 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.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, 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.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, 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.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, 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.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, 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.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 ( diff --git a/coderd/database/queries/workspacebuildorchestrations.sql b/coderd/database/queries/workspacebuildorchestrations.sql new file mode 100644 index 00000000000..c21dad9c8fb --- /dev/null +++ b/coderd/database/queries/workspacebuildorchestrations.sql @@ -0,0 +1,122 @@ +-- name: InsertWorkspaceBuildOrchestration :one +INSERT INTO workspace_build_orchestrations ( + id, + created_at, + updated_at, + parent_build_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, + @child_transition, + @child_template_version_id, + @child_template_version_preset_id, + @child_rich_parameter_values, + @child_log_level, + @child_reason, + 'pending', + NULL +) +RETURNING *; + +-- name: GetWorkspaceBuildOrchestrationByParentBuildID :one +SELECT + * +FROM + workspace_build_orchestrations +WHERE + parent_build_id = @parent_build_id; + +-- 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 *; diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index 8ef517a9cbb..9df658a640e 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -124,6 +124,9 @@ 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); 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); From 2e6d134c90e97af021707c10b93b649b63faf494 Mon Sep 17 00:00:00 2001 From: George Katsitadze Date: Mon, 15 Jun 2026 11:56:48 -0700 Subject: [PATCH 02/13] reject template version pinning with an error for non-template admins --- coderd/database/dbauthz/dbauthz.go | 2 +- coderd/database/dbauthz/dbauthz_test.go | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 68ee1adbea9..e9feeae0afd 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -6085,7 +6085,7 @@ func (q *querier) InsertWorkspaceBuildOrchestration(ctx context.Context, arg dat err = q.authorizeContext(ctx, policy.ActionUpdate, template) var notAuthorized NotAuthorizedError if xerrors.As(err, ¬Authorized) { - arg.ChildTemplateVersionID = uuid.NullUUID{} + return database.WorkspaceBuildOrchestration{}, err } else if err != nil { return database.WorkspaceBuildOrchestration{}, xerrors.Errorf("cannot pin template version for child build: %w", err) } diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 5d01979a171..4de86d27dc7 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -3825,22 +3825,18 @@ func (s *MethodTestSuite) TestWorkspace() { Valid: true, }, } - insertArg := arg - insertArg.ChildTemplateVersionID = uuid.NullUUID{} - 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 non-template admins have the requested durable - // template version pin stripped before insert. - dbm.EXPECT().InsertWorkspaceBuildOrchestration(gomock.Any(), insertArg).Return(orchestration, 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, ). - Returns(orchestration). + 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") From bff0b76b93c13b7e1a38bfc7d2a2effff73ae2e7 Mon Sep 17 00:00:00 2001 From: George Katsitadze Date: Mon, 15 Jun 2026 15:43:28 -0700 Subject: [PATCH 03/13] get rid of query that's only used in tests --- coderd/database/dbauthz/dbauthz.go | 7 ---- coderd/database/dbauthz/dbauthz_test.go | 7 ---- coderd/database/dbmetrics/querymetrics.go | 8 ----- coderd/database/dbmock/dbmock.go | 15 --------- coderd/database/querier.go | 1 - coderd/database/queries.sql.go | 32 ------------------- .../queries/workspacebuildorchestrations.sql | 8 ----- 7 files changed, 78 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index e9feeae0afd..a79a5d73054 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -5144,13 +5144,6 @@ func (q *querier) GetWorkspaceBuildMetricsByResourceID(ctx context.Context, id u return q.db.GetWorkspaceBuildMetricsByResourceID(ctx, id) } -func (q *querier) GetWorkspaceBuildOrchestrationByParentBuildID(ctx context.Context, parentBuildID uuid.UUID) (database.WorkspaceBuildOrchestration, error) { - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { - return database.WorkspaceBuildOrchestration{}, err - } - return q.db.GetWorkspaceBuildOrchestrationByParentBuildID(ctx, parentBuildID) -} - func (q *querier) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]database.WorkspaceBuildParameter, error) { // Authorized call to get the workspace build. If we can read the build, // we can read the params. diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 4de86d27dc7..0939eb4c861 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -3874,13 +3874,6 @@ func (s *MethodTestSuite) TestWorkspace() { Asserts(rbac.ResourceSystem, policy.ActionRead). Returns(orchestration) })) - s.Run("GetWorkspaceBuildOrchestrationByParentBuildID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { - orchestration := testutil.Fake(s.T(), faker, database.WorkspaceBuildOrchestration{}) - dbm.EXPECT().GetWorkspaceBuildOrchestrationByParentBuildID(gomock.Any(), orchestration.ParentBuildID).Return(orchestration, nil).AnyTimes() - check.Args(orchestration.ParentBuildID). - Asserts(rbac.ResourceSystem, 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{ diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 6e16074fddd..992647a5cdf 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -3449,14 +3449,6 @@ func (m queryMetricsStore) GetWorkspaceBuildMetricsByResourceID(ctx context.Cont return r0, r1 } -func (m queryMetricsStore) GetWorkspaceBuildOrchestrationByParentBuildID(ctx context.Context, parentBuildID uuid.UUID) (database.WorkspaceBuildOrchestration, error) { - start := time.Now() - r0, r1 := m.s.GetWorkspaceBuildOrchestrationByParentBuildID(ctx, parentBuildID) - m.queryLatencies.WithLabelValues("GetWorkspaceBuildOrchestrationByParentBuildID").Observe(time.Since(start).Seconds()) - m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetWorkspaceBuildOrchestrationByParentBuildID").Inc() - return r0, r1 -} - func (m queryMetricsStore) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]database.WorkspaceBuildParameter, error) { start := time.Now() r0, r1 := m.s.GetWorkspaceBuildParameters(ctx, workspaceBuildID) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index c6ec567858d..3daae3b7a50 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -6465,21 +6465,6 @@ func (mr *MockStoreMockRecorder) GetWorkspaceBuildMetricsByResourceID(ctx, id an return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceBuildMetricsByResourceID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceBuildMetricsByResourceID), ctx, id) } -// GetWorkspaceBuildOrchestrationByParentBuildID mocks base method. -func (m *MockStore) GetWorkspaceBuildOrchestrationByParentBuildID(ctx context.Context, parentBuildID uuid.UUID) (database.WorkspaceBuildOrchestration, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetWorkspaceBuildOrchestrationByParentBuildID", ctx, parentBuildID) - ret0, _ := ret[0].(database.WorkspaceBuildOrchestration) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetWorkspaceBuildOrchestrationByParentBuildID indicates an expected call of GetWorkspaceBuildOrchestrationByParentBuildID. -func (mr *MockStoreMockRecorder) GetWorkspaceBuildOrchestrationByParentBuildID(ctx, parentBuildID any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceBuildOrchestrationByParentBuildID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceBuildOrchestrationByParentBuildID), ctx, parentBuildID) -} - // GetWorkspaceBuildParameters mocks base method. func (m *MockStore) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]database.WorkspaceBuildParameter, error) { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 90104cd0061..86fb4b7a02d 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -868,7 +868,6 @@ type sqlcQuerier interface { // Returns build metadata for e2e workspace build duration metrics. // Also checks if all agents are ready and returns the worst status. GetWorkspaceBuildMetricsByResourceID(ctx context.Context, id uuid.UUID) (GetWorkspaceBuildMetricsByResourceIDRow, error) - GetWorkspaceBuildOrchestrationByParentBuildID(ctx context.Context, parentBuildID uuid.UUID) (WorkspaceBuildOrchestration, error) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]WorkspaceBuildParameter, error) // Fetches the provisioner state of a workspace build, joined through to the // template so that dbauthz can enforce policy.ActionUpdate on the template. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 0ffaa4726cd..a041ae09a85 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -33006,38 +33006,6 @@ func (q *sqlQuerier) GetNextPendingWorkspaceBuildOrchestrationForUpdate(ctx cont return i, err } -const getWorkspaceBuildOrchestrationByParentBuildID = `-- name: GetWorkspaceBuildOrchestrationByParentBuildID :one -SELECT - id, created_at, updated_at, 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 -FROM - workspace_build_orchestrations -WHERE - parent_build_id = $1 -` - -func (q *sqlQuerier) GetWorkspaceBuildOrchestrationByParentBuildID(ctx context.Context, parentBuildID uuid.UUID) (WorkspaceBuildOrchestration, error) { - row := q.db.QueryRowContext(ctx, getWorkspaceBuildOrchestrationByParentBuildID, parentBuildID) - var i WorkspaceBuildOrchestration - err := row.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &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, diff --git a/coderd/database/queries/workspacebuildorchestrations.sql b/coderd/database/queries/workspacebuildorchestrations.sql index c21dad9c8fb..8ec94c6c872 100644 --- a/coderd/database/queries/workspacebuildorchestrations.sql +++ b/coderd/database/queries/workspacebuildorchestrations.sql @@ -29,14 +29,6 @@ VALUES ( ) RETURNING *; --- name: GetWorkspaceBuildOrchestrationByParentBuildID :one -SELECT - * -FROM - workspace_build_orchestrations -WHERE - parent_build_id = @parent_build_id; - -- name: GetNextPendingWorkspaceBuildOrchestrationForUpdate :one -- Must be called from within a transaction. The row lock is released -- when the transaction ends. From 6f19402dfba79137e0bc469109bc56d685c8d27b Mon Sep 17 00:00:00 2001 From: George Katsitadze Date: Mon, 15 Jun 2026 16:11:32 -0700 Subject: [PATCH 04/13] enforce parent_build.workspace_id == child_build.workspace_id in DB --- coderd/database/dump.sql | 10 ++- coderd/database/foreign_key_constraint.go | 4 +- ...10_workspace_build_orchestrations.down.sql | 3 + ...0510_workspace_build_orchestrations.up.sql | 27 +++++- ...0510_workspace_build_orchestrations.up.sql | 2 + coderd/database/models.go | 2 + coderd/database/querier_test.go | 86 +++++++++++++++++++ coderd/database/queries.sql.go | 20 +++-- .../queries/workspacebuildorchestrations.sql | 2 + coderd/database/unique_constraint.go | 1 + 10 files changed, 144 insertions(+), 13 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 8e96846ed4a..78d39eee5d4 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -3444,6 +3444,7 @@ 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, @@ -3466,6 +3467,8 @@ CREATE TABLE workspace_build_orchestrations ( 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.'; @@ -4095,6 +4098,9 @@ ALTER TABLE ONLY workspace_build_orchestrations 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); @@ -4915,13 +4921,13 @@ ALTER TABLE ONLY workspace_build_orchestrations ADD CONSTRAINT workspace_build_orchestration_child_template_version_prese_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_build_id_fkey FOREIGN KEY (child_build_id) REFERENCES workspace_builds(id) ON DELETE CASCADE; + 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_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_id_fkey FOREIGN KEY (parent_build_id) REFERENCES workspace_builds(id) ON DELETE CASCADE; + 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; diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index 9ebc974e0c9..6469b549787 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -138,9 +138,9 @@ const ( 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; ForeignKeyWorkspaceBuildOrchestrationChildTemplateVersionPrese ForeignKeyConstraint = "workspace_build_orchestration_child_template_version_prese_fkey" // ALTER TABLE ONLY workspace_build_orchestrations ADD CONSTRAINT workspace_build_orchestration_child_template_version_prese_fkey FOREIGN KEY (child_template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE SET NULL; - ForeignKeyWorkspaceBuildOrchestrationsChildBuildID ForeignKeyConstraint = "workspace_build_orchestrations_child_build_id_fkey" // ALTER TABLE ONLY workspace_build_orchestrations ADD CONSTRAINT workspace_build_orchestrations_child_build_id_fkey FOREIGN KEY (child_build_id) REFERENCES workspace_builds(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; 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; - ForeignKeyWorkspaceBuildOrchestrationsParentBuildID ForeignKeyConstraint = "workspace_build_orchestrations_parent_build_id_fkey" // ALTER TABLE ONLY workspace_build_orchestrations ADD CONSTRAINT workspace_build_orchestrations_parent_build_id_fkey FOREIGN KEY (parent_build_id) REFERENCES workspace_builds(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; diff --git a/coderd/database/migrations/000510_workspace_build_orchestrations.down.sql b/coderd/database/migrations/000510_workspace_build_orchestrations.down.sql index e2dfb303488..0bae8de175a 100644 --- a/coderd/database/migrations/000510_workspace_build_orchestrations.down.sql +++ b/coderd/database/migrations/000510_workspace_build_orchestrations.down.sql @@ -1 +1,4 @@ DROP TABLE IF EXISTS workspace_build_orchestrations; + +ALTER TABLE workspace_builds + DROP CONSTRAINT IF EXISTS workspace_builds_id_workspace_id_key; diff --git a/coderd/database/migrations/000510_workspace_build_orchestrations.up.sql b/coderd/database/migrations/000510_workspace_build_orchestrations.up.sql index ad6f2dd2833..c0ba67f6d54 100644 --- a/coderd/database/migrations/000510_workspace_build_orchestrations.up.sql +++ b/coderd/database/migrations/000510_workspace_build_orchestrations.up.sql @@ -1,9 +1,17 @@ +-- 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); + CREATE TABLE workspace_build_orchestrations ( id UUID PRIMARY KEY NOT NULL, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL, - parent_build_id UUID UNIQUE NOT NULL REFERENCES workspace_builds(id) ON DELETE CASCADE, - child_build_id UUID UNIQUE REFERENCES workspace_builds(id) ON DELETE CASCADE, + 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 REFERENCES template_version_presets(id) ON DELETE SET NULL, @@ -33,7 +41,17 @@ CREATE TABLE workspace_build_orchestrations ( -- log level is either unset or debug. CONSTRAINT workspace_build_orchestrations_child_log_level_check CHECK ( child_log_level IN ('', 'debug') - ) + ), + -- 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 @@ -48,6 +66,9 @@ COMMENT ON TABLE workspace_build_orchestrations IS 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.'; diff --git a/coderd/database/migrations/testdata/fixtures/000510_workspace_build_orchestrations.up.sql b/coderd/database/migrations/testdata/fixtures/000510_workspace_build_orchestrations.up.sql index 6e3f5556a47..213a848a51c 100644 --- a/coderd/database/migrations/testdata/fixtures/000510_workspace_build_orchestrations.up.sql +++ b/coderd/database/migrations/testdata/fixtures/000510_workspace_build_orchestrations.up.sql @@ -2,6 +2,7 @@ INSERT INTO workspace_build_orchestrations ( id, created_at, updated_at, + workspace_id, parent_build_id, child_transition ) @@ -9,6 +10,7 @@ SELECT '4e983a68-9b8a-4d4e-a4d6-5f2dd73551c2'::uuid, NOW(), NOW(), + workspace_id, id, 'start'::workspace_transition FROM diff --git a/coderd/database/models.go b/coderd/database/models.go index d895882d316..0bd382f31ae 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -6125,6 +6125,8 @@ 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. diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 53f914e7679..9278382cc0c 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -8473,6 +8473,7 @@ func TestUpdateWorkspaceBuildOrchestrationRetryByIDMaxAttempts(t *testing.T) { ChildRichParameterValues: json.RawMessage("[]"), }) require.NoError(t, err) + require.Equal(t, workspace.ID, orchestration.WorkspaceID) const maxAttemptCount = 3 const retryError = "some retryable child build failure" @@ -8580,6 +8581,7 @@ func TestGetNextPendingWorkspaceBuildOrchestrationForUpdateRetryDelay(t *testing ChildRichParameterValues: json.RawMessage("[]"), }) require.NoError(t, err) + require.Equal(t, workspace.ID, orchestration.WorkspaceID) return orchestration } @@ -8645,6 +8647,90 @@ func TestGetNextPendingWorkspaceBuildOrchestrationForUpdateRetryDelay(t *testing 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 TestWorkspaceBuildDeadlineConstraint(t *testing.T) { t.Parallel() diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index a041ae09a85..ad77d8c9c7b 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -32960,7 +32960,7 @@ func (q *sqlQuerier) InsertWorkspaceAppStats(ctx context.Context, arg InsertWork const getNextPendingWorkspaceBuildOrchestrationForUpdate = `-- name: GetNextPendingWorkspaceBuildOrchestrationForUpdate :one SELECT - wbo.id, wbo.created_at, wbo.updated_at, 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 + 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 @@ -32990,6 +32990,7 @@ func (q *sqlQuerier) GetNextPendingWorkspaceBuildOrchestrationForUpdate(ctx cont &i.ID, &i.CreatedAt, &i.UpdatedAt, + &i.WorkspaceID, &i.ParentBuildID, &i.ChildBuildID, &i.ChildTransition, @@ -33012,6 +33013,7 @@ INSERT INTO workspace_build_orchestrations ( created_at, updated_at, parent_build_id, + workspace_id, child_transition, child_template_version_id, child_template_version_preset_id, @@ -33026,6 +33028,7 @@ VALUES ( $2, $3, $4, + (SELECT workspace_id FROM workspace_builds WHERE id = $4), $5, $6, $7, @@ -33035,7 +33038,7 @@ VALUES ( 'pending', NULL ) -RETURNING id, created_at, updated_at, 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 +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 { @@ -33069,6 +33072,7 @@ func (q *sqlQuerier) InsertWorkspaceBuildOrchestration(ctx context.Context, arg &i.ID, &i.CreatedAt, &i.UpdatedAt, + &i.WorkspaceID, &i.ParentBuildID, &i.ChildBuildID, &i.ChildTransition, @@ -33096,7 +33100,7 @@ SET WHERE id = $2 AND status = 'pending' -RETURNING id, created_at, updated_at, 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 +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 { @@ -33111,6 +33115,7 @@ func (q *sqlQuerier) UpdateWorkspaceBuildOrchestrationCanceledByID(ctx context.C &i.ID, &i.CreatedAt, &i.UpdatedAt, + &i.WorkspaceID, &i.ParentBuildID, &i.ChildBuildID, &i.ChildTransition, @@ -33139,7 +33144,7 @@ SET WHERE id = $3 AND status = 'pending' -RETURNING id, created_at, updated_at, 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 +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 { @@ -33155,6 +33160,7 @@ func (q *sqlQuerier) UpdateWorkspaceBuildOrchestrationCompletedByID(ctx context. &i.ID, &i.CreatedAt, &i.UpdatedAt, + &i.WorkspaceID, &i.ParentBuildID, &i.ChildBuildID, &i.ChildTransition, @@ -33182,7 +33188,7 @@ SET WHERE id = $3 AND status = 'pending' -RETURNING id, created_at, updated_at, 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 +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 { @@ -33198,6 +33204,7 @@ func (q *sqlQuerier) UpdateWorkspaceBuildOrchestrationFailedByID(ctx context.Con &i.ID, &i.CreatedAt, &i.UpdatedAt, + &i.WorkspaceID, &i.ParentBuildID, &i.ChildBuildID, &i.ChildTransition, @@ -33232,7 +33239,7 @@ SET WHERE id = $5 AND status = 'pending' -RETURNING id, created_at, updated_at, 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 +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 { @@ -33256,6 +33263,7 @@ func (q *sqlQuerier) UpdateWorkspaceBuildOrchestrationRetryByID(ctx context.Cont &i.ID, &i.CreatedAt, &i.UpdatedAt, + &i.WorkspaceID, &i.ParentBuildID, &i.ChildBuildID, &i.ChildTransition, diff --git a/coderd/database/queries/workspacebuildorchestrations.sql b/coderd/database/queries/workspacebuildorchestrations.sql index 8ec94c6c872..f256d89a7ad 100644 --- a/coderd/database/queries/workspacebuildorchestrations.sql +++ b/coderd/database/queries/workspacebuildorchestrations.sql @@ -4,6 +4,7 @@ INSERT INTO workspace_build_orchestrations ( created_at, updated_at, parent_build_id, + workspace_id, child_transition, child_template_version_id, child_template_version_preset_id, @@ -18,6 +19,7 @@ VALUES ( @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, diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index 9df658a640e..c0944cf5b2a 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -128,6 +128,7 @@ const ( 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); From 995c5fdd4caaa754cacfb00f1454d57fdbedebc7 Mon Sep 17 00:00:00 2001 From: George Katsitadze Date: Mon, 15 Jun 2026 21:50:21 -0700 Subject: [PATCH 05/13] add constraints for child template version presets --- coderd/database/check_constraint.go | 1 + coderd/database/dump.sql | 11 +- coderd/database/foreign_key_constraint.go | 293 +++++++++--------- ...10_workspace_build_orchestrations.down.sql | 3 + ...0510_workspace_build_orchestrations.up.sql | 19 +- coderd/database/querier_test.go | 143 +++++++++ coderd/database/unique_constraint.go | 1 + 7 files changed, 322 insertions(+), 149 deletions(-) diff --git a/coderd/database/check_constraint.go b/coderd/database/check_constraint.go index 7ba2ea816a0..01c2dd19f47 100644 --- a/coderd/database/check_constraint.go +++ b/coderd/database/check_constraint.go @@ -51,6 +51,7 @@ const ( 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 + CheckWorkspaceBuildOrchestrationsChildPresetVersionCheck CheckConstraint = "workspace_build_orchestrations_child_preset_version_check" // workspace_build_orchestrations CheckWorkspaceBuildOrchestrationsChildRichParameterValuesChec CheckConstraint = "workspace_build_orchestrations_child_rich_parameter_values_chec" // 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 diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 78d39eee5d4..079ebea2041 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -3459,6 +3459,7 @@ CREATE TABLE workspace_build_orchestrations ( 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_preset_version_check CHECK (((child_template_version_preset_id IS NULL) OR (child_template_version_id IS NOT NULL))), CONSTRAINT workspace_build_orchestrations_child_rich_parameter_values_chec CHECK ((jsonb_typeof(child_rich_parameter_values) = 'array'::text)), 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))), @@ -3978,6 +3979,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); @@ -4918,10 +4922,13 @@ 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_orchestration_child_template_version_prese_fkey FOREIGN KEY (child_template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE SET NULL; + 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_build_workspace_id_fkey FOREIGN KEY (child_build_id, workspace_id) REFERENCES workspace_builds(id, workspace_id) ON DELETE CASCADE; + 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; diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index 6469b549787..d41afdb8993 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -6,150 +6,151 @@ type ForeignKeyConstraint string // ForeignKeyConstraint enums. const ( - ForeignKeyAiProviderKeysAPIKeyKeyID ForeignKeyConstraint = "ai_provider_keys_api_key_key_id_fkey" // ALTER TABLE ONLY ai_provider_keys ADD CONSTRAINT ai_provider_keys_api_key_key_id_fkey FOREIGN KEY (api_key_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyAiProviderKeysProviderID ForeignKeyConstraint = "ai_provider_keys_provider_id_fkey" // ALTER TABLE ONLY ai_provider_keys ADD CONSTRAINT ai_provider_keys_provider_id_fkey FOREIGN KEY (provider_id) REFERENCES ai_providers(id) ON DELETE CASCADE; - ForeignKeyAiProvidersSettingsKeyID ForeignKeyConstraint = "ai_providers_settings_key_id_fkey" // ALTER TABLE ONLY ai_providers ADD CONSTRAINT ai_providers_settings_key_id_fkey FOREIGN KEY (settings_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyAiSeatStateUserID ForeignKeyConstraint = "ai_seat_state_user_id_fkey" // ALTER TABLE ONLY ai_seat_state ADD CONSTRAINT ai_seat_state_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyAibridgeInterceptionsInitiatorID ForeignKeyConstraint = "aibridge_interceptions_initiator_id_fkey" // ALTER TABLE ONLY aibridge_interceptions ADD CONSTRAINT aibridge_interceptions_initiator_id_fkey FOREIGN KEY (initiator_id) REFERENCES users(id); - ForeignKeyAPIKeysUserIDUUID ForeignKeyConstraint = "api_keys_user_id_uuid_fkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyBoundaryLogsSessionID ForeignKeyConstraint = "boundary_logs_session_id_fkey" // ALTER TABLE ONLY boundary_logs ADD CONSTRAINT boundary_logs_session_id_fkey FOREIGN KEY (session_id) REFERENCES boundary_sessions(id) ON DELETE CASCADE; - ForeignKeyBoundarySessionsWorkspaceAgentID ForeignKeyConstraint = "boundary_sessions_workspace_agent_id_fkey" // ALTER TABLE ONLY boundary_sessions ADD CONSTRAINT boundary_sessions_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id); - ForeignKeyChatDebugRunsChatID ForeignKeyConstraint = "chat_debug_runs_chat_id_fkey" // ALTER TABLE ONLY chat_debug_runs ADD CONSTRAINT chat_debug_runs_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; - ForeignKeyChatDebugStepsChatID ForeignKeyConstraint = "chat_debug_steps_chat_id_fkey" // ALTER TABLE ONLY chat_debug_steps ADD CONSTRAINT chat_debug_steps_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; - ForeignKeyChatDiffStatusesChatID ForeignKeyConstraint = "chat_diff_statuses_chat_id_fkey" // ALTER TABLE ONLY chat_diff_statuses ADD CONSTRAINT chat_diff_statuses_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; - ForeignKeyChatFileLinksChatID ForeignKeyConstraint = "chat_file_links_chat_id_fkey" // ALTER TABLE ONLY chat_file_links ADD CONSTRAINT chat_file_links_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; - ForeignKeyChatFileLinksFileID ForeignKeyConstraint = "chat_file_links_file_id_fkey" // ALTER TABLE ONLY chat_file_links ADD CONSTRAINT chat_file_links_file_id_fkey FOREIGN KEY (file_id) REFERENCES chat_files(id) ON DELETE CASCADE; - ForeignKeyChatFilesOrganizationID ForeignKeyConstraint = "chat_files_organization_id_fkey" // ALTER TABLE ONLY chat_files ADD CONSTRAINT chat_files_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyChatFilesOwnerID ForeignKeyConstraint = "chat_files_owner_id_fkey" // ALTER TABLE ONLY chat_files ADD CONSTRAINT chat_files_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyChatMessagesAPIKeyID ForeignKeyConstraint = "chat_messages_api_key_id_fkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_api_key_id_fkey FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE SET NULL; - ForeignKeyChatMessagesChatID ForeignKeyConstraint = "chat_messages_chat_id_fkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; - ForeignKeyChatMessagesModelConfigID ForeignKeyConstraint = "chat_messages_model_config_id_fkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_model_config_id_fkey FOREIGN KEY (model_config_id) REFERENCES chat_model_configs(id); - ForeignKeyChatModelConfigsAiProviderID ForeignKeyConstraint = "chat_model_configs_ai_provider_id_fkey" // ALTER TABLE ONLY chat_model_configs ADD CONSTRAINT chat_model_configs_ai_provider_id_fkey FOREIGN KEY (ai_provider_id) REFERENCES ai_providers(id); - ForeignKeyChatModelConfigsCreatedBy ForeignKeyConstraint = "chat_model_configs_created_by_fkey" // ALTER TABLE ONLY chat_model_configs ADD CONSTRAINT chat_model_configs_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id); - ForeignKeyChatModelConfigsUpdatedBy ForeignKeyConstraint = "chat_model_configs_updated_by_fkey" // ALTER TABLE ONLY chat_model_configs ADD CONSTRAINT chat_model_configs_updated_by_fkey FOREIGN KEY (updated_by) REFERENCES users(id); - ForeignKeyChatQueuedMessagesAPIKeyID ForeignKeyConstraint = "chat_queued_messages_api_key_id_fkey" // ALTER TABLE ONLY chat_queued_messages ADD CONSTRAINT chat_queued_messages_api_key_id_fkey FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE SET NULL; - ForeignKeyChatQueuedMessagesChatID ForeignKeyConstraint = "chat_queued_messages_chat_id_fkey" // ALTER TABLE ONLY chat_queued_messages ADD CONSTRAINT chat_queued_messages_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; - ForeignKeyChatsAgentID ForeignKeyConstraint = "chats_agent_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE SET NULL; - ForeignKeyChatsBuildID ForeignKeyConstraint = "chats_build_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_build_id_fkey FOREIGN KEY (build_id) REFERENCES workspace_builds(id) ON DELETE SET NULL; - ForeignKeyChatsLastModelConfigID ForeignKeyConstraint = "chats_last_model_config_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_last_model_config_id_fkey FOREIGN KEY (last_model_config_id) REFERENCES chat_model_configs(id); - ForeignKeyChatsOrganizationID ForeignKeyConstraint = "chats_organization_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyChatsOwnerID ForeignKeyConstraint = "chats_owner_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyChatsParentChatID ForeignKeyConstraint = "chats_parent_chat_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_parent_chat_id_fkey FOREIGN KEY (parent_chat_id) REFERENCES chats(id) ON DELETE SET NULL; - ForeignKeyChatsRootChatID ForeignKeyConstraint = "chats_root_chat_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_root_chat_id_fkey FOREIGN KEY (root_chat_id) REFERENCES chats(id) ON DELETE SET NULL; - ForeignKeyChatsWorkspaceID ForeignKeyConstraint = "chats_workspace_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE SET NULL; - ForeignKeyConnectionLogsOrganizationID ForeignKeyConstraint = "connection_logs_organization_id_fkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyConnectionLogsWorkspaceID ForeignKeyConstraint = "connection_logs_workspace_id_fkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; - ForeignKeyConnectionLogsWorkspaceOwnerID ForeignKeyConstraint = "connection_logs_workspace_owner_id_fkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_workspace_owner_id_fkey FOREIGN KEY (workspace_owner_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyCryptoKeysSecretKeyID ForeignKeyConstraint = "crypto_keys_secret_key_id_fkey" // ALTER TABLE ONLY crypto_keys ADD CONSTRAINT crypto_keys_secret_key_id_fkey FOREIGN KEY (secret_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyFkChatDebugStepsRunChat ForeignKeyConstraint = "fk_chat_debug_steps_run_chat" // ALTER TABLE ONLY chat_debug_steps ADD CONSTRAINT fk_chat_debug_steps_run_chat FOREIGN KEY (run_id, chat_id) REFERENCES chat_debug_runs(id, chat_id) ON DELETE CASCADE; - ForeignKeyFkOauth2ProviderAppTokensUserID ForeignKeyConstraint = "fk_oauth2_provider_app_tokens_user_id" // ALTER TABLE ONLY oauth2_provider_app_tokens ADD CONSTRAINT fk_oauth2_provider_app_tokens_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyGitAuthLinksOauthAccessTokenKeyID ForeignKeyConstraint = "git_auth_links_oauth_access_token_key_id_fkey" // ALTER TABLE ONLY external_auth_links ADD CONSTRAINT git_auth_links_oauth_access_token_key_id_fkey FOREIGN KEY (oauth_access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyGitAuthLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "git_auth_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY external_auth_links ADD CONSTRAINT git_auth_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyGitSSHKeysUserID ForeignKeyConstraint = "gitsshkeys_user_id_fkey" // ALTER TABLE ONLY gitsshkeys ADD CONSTRAINT gitsshkeys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); - ForeignKeyGroupAiBudgetsGroupID ForeignKeyConstraint = "group_ai_budgets_group_id_fkey" // ALTER TABLE ONLY group_ai_budgets ADD CONSTRAINT group_ai_budgets_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE; - ForeignKeyGroupMembersGroupID ForeignKeyConstraint = "group_members_group_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE; - ForeignKeyGroupMembersUserID ForeignKeyConstraint = "group_members_user_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyGroupsOrganizationID ForeignKeyConstraint = "groups_organization_id_fkey" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyInboxNotificationsTemplateID ForeignKeyConstraint = "inbox_notifications_template_id_fkey" // ALTER TABLE ONLY inbox_notifications ADD CONSTRAINT inbox_notifications_template_id_fkey FOREIGN KEY (template_id) REFERENCES notification_templates(id) ON DELETE CASCADE; - ForeignKeyInboxNotificationsUserID ForeignKeyConstraint = "inbox_notifications_user_id_fkey" // ALTER TABLE ONLY inbox_notifications ADD CONSTRAINT inbox_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyJfrogXrayScansAgentID ForeignKeyConstraint = "jfrog_xray_scans_agent_id_fkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyJfrogXrayScansWorkspaceID ForeignKeyConstraint = "jfrog_xray_scans_workspace_id_fkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; - ForeignKeyMcpServerConfigsAPIKeyValueKeyID ForeignKeyConstraint = "mcp_server_configs_api_key_value_key_id_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_api_key_value_key_id_fkey FOREIGN KEY (api_key_value_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyMcpServerConfigsCreatedBy ForeignKeyConstraint = "mcp_server_configs_created_by_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL; - ForeignKeyMcpServerConfigsCustomHeadersKeyID ForeignKeyConstraint = "mcp_server_configs_custom_headers_key_id_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_custom_headers_key_id_fkey FOREIGN KEY (custom_headers_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyMcpServerConfigsOauth2ClientSecretKeyID ForeignKeyConstraint = "mcp_server_configs_oauth2_client_secret_key_id_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_oauth2_client_secret_key_id_fkey FOREIGN KEY (oauth2_client_secret_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyMcpServerConfigsUpdatedBy ForeignKeyConstraint = "mcp_server_configs_updated_by_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_updated_by_fkey FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL; - ForeignKeyMcpServerUserTokensAccessTokenKeyID ForeignKeyConstraint = "mcp_server_user_tokens_access_token_key_id_fkey" // ALTER TABLE ONLY mcp_server_user_tokens ADD CONSTRAINT mcp_server_user_tokens_access_token_key_id_fkey FOREIGN KEY (access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyMcpServerUserTokensMcpServerConfigID ForeignKeyConstraint = "mcp_server_user_tokens_mcp_server_config_id_fkey" // ALTER TABLE ONLY mcp_server_user_tokens ADD CONSTRAINT mcp_server_user_tokens_mcp_server_config_id_fkey FOREIGN KEY (mcp_server_config_id) REFERENCES mcp_server_configs(id) ON DELETE CASCADE; - ForeignKeyMcpServerUserTokensRefreshTokenKeyID ForeignKeyConstraint = "mcp_server_user_tokens_refresh_token_key_id_fkey" // ALTER TABLE ONLY mcp_server_user_tokens ADD CONSTRAINT mcp_server_user_tokens_refresh_token_key_id_fkey FOREIGN KEY (refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyMcpServerUserTokensUserID ForeignKeyConstraint = "mcp_server_user_tokens_user_id_fkey" // ALTER TABLE ONLY mcp_server_user_tokens ADD CONSTRAINT mcp_server_user_tokens_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyNotificationMessagesNotificationTemplateID ForeignKeyConstraint = "notification_messages_notification_template_id_fkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE; - ForeignKeyNotificationMessagesUserID ForeignKeyConstraint = "notification_messages_user_id_fkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyNotificationPreferencesNotificationTemplateID ForeignKeyConstraint = "notification_preferences_notification_template_id_fkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE; - ForeignKeyNotificationPreferencesUserID ForeignKeyConstraint = "notification_preferences_user_id_fkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyOauth2ProviderAppCodesAppID ForeignKeyConstraint = "oauth2_provider_app_codes_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE; - ForeignKeyOauth2ProviderAppCodesUserID ForeignKeyConstraint = "oauth2_provider_app_codes_user_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyOauth2ProviderAppSecretsAppID ForeignKeyConstraint = "oauth2_provider_app_secrets_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_secrets ADD CONSTRAINT oauth2_provider_app_secrets_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE; - ForeignKeyOauth2ProviderAppTokensAPIKeyID ForeignKeyConstraint = "oauth2_provider_app_tokens_api_key_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_tokens ADD CONSTRAINT oauth2_provider_app_tokens_api_key_id_fkey FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE CASCADE; - ForeignKeyOauth2ProviderAppTokensAppSecretID ForeignKeyConstraint = "oauth2_provider_app_tokens_app_secret_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_tokens ADD CONSTRAINT oauth2_provider_app_tokens_app_secret_id_fkey FOREIGN KEY (app_secret_id) REFERENCES oauth2_provider_app_secrets(id) ON DELETE CASCADE; - ForeignKeyOrganizationMembersOrganizationIDUUID ForeignKeyConstraint = "organization_members_organization_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_organization_id_uuid_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyOrganizationMembersUserIDUUID ForeignKeyConstraint = "organization_members_user_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyParameterSchemasJobID ForeignKeyConstraint = "parameter_schemas_job_id_fkey" // ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; - ForeignKeyProvisionerDaemonsKeyID ForeignKeyConstraint = "provisioner_daemons_key_id_fkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_key_id_fkey FOREIGN KEY (key_id) REFERENCES provisioner_keys(id) ON DELETE CASCADE; - ForeignKeyProvisionerDaemonsOrganizationID ForeignKeyConstraint = "provisioner_daemons_organization_id_fkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyProvisionerJobLogsJobID ForeignKeyConstraint = "provisioner_job_logs_job_id_fkey" // ALTER TABLE ONLY provisioner_job_logs ADD CONSTRAINT provisioner_job_logs_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; - ForeignKeyProvisionerJobTimingsJobID ForeignKeyConstraint = "provisioner_job_timings_job_id_fkey" // ALTER TABLE ONLY provisioner_job_timings ADD CONSTRAINT provisioner_job_timings_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; - ForeignKeyProvisionerJobsOrganizationID ForeignKeyConstraint = "provisioner_jobs_organization_id_fkey" // ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyProvisionerKeysOrganizationID ForeignKeyConstraint = "provisioner_keys_organization_id_fkey" // ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyTailnetPeersCoordinatorID ForeignKeyConstraint = "tailnet_peers_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_peers ADD CONSTRAINT tailnet_peers_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; - ForeignKeyTailnetTunnelsCoordinatorID ForeignKeyConstraint = "tailnet_tunnels_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; - ForeignKeyTaskSnapshotsTaskID ForeignKeyConstraint = "task_snapshots_task_id_fkey" // ALTER TABLE ONLY task_snapshots ADD CONSTRAINT task_snapshots_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; - ForeignKeyTaskWorkspaceAppsTaskID ForeignKeyConstraint = "task_workspace_apps_task_id_fkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; - ForeignKeyTaskWorkspaceAppsWorkspaceAgentID ForeignKeyConstraint = "task_workspace_apps_workspace_agent_id_fkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyTaskWorkspaceAppsWorkspaceAppID ForeignKeyConstraint = "task_workspace_apps_workspace_app_id_fkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_workspace_app_id_fkey FOREIGN KEY (workspace_app_id) REFERENCES workspace_apps(id) ON DELETE CASCADE; - ForeignKeyTasksOrganizationID ForeignKeyConstraint = "tasks_organization_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyTasksOwnerID ForeignKeyConstraint = "tasks_owner_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyTasksTemplateVersionID ForeignKeyConstraint = "tasks_template_version_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; - ForeignKeyTasksWorkspaceID ForeignKeyConstraint = "tasks_workspace_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionParametersTemplateVersionID ForeignKeyConstraint = "template_version_parameters_template_version_id_fkey" // ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionPresetParametTemplateVersionPresetID ForeignKeyConstraint = "template_version_preset_paramet_template_version_preset_id_fkey" // ALTER TABLE ONLY template_version_preset_parameters ADD CONSTRAINT template_version_preset_paramet_template_version_preset_id_fkey FOREIGN KEY (template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionPresetPrebuildSchedulesPresetID ForeignKeyConstraint = "template_version_preset_prebuild_schedules_preset_id_fkey" // ALTER TABLE ONLY template_version_preset_prebuild_schedules ADD CONSTRAINT template_version_preset_prebuild_schedules_preset_id_fkey FOREIGN KEY (preset_id) REFERENCES template_version_presets(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionPresetsTemplateVersionID ForeignKeyConstraint = "template_version_presets_template_version_id_fkey" // ALTER TABLE ONLY template_version_presets ADD CONSTRAINT template_version_presets_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionTerraformValuesCachedModuleFiles ForeignKeyConstraint = "template_version_terraform_values_cached_module_files_fkey" // ALTER TABLE ONLY template_version_terraform_values ADD CONSTRAINT template_version_terraform_values_cached_module_files_fkey FOREIGN KEY (cached_module_files) REFERENCES files(id); - ForeignKeyTemplateVersionTerraformValuesTemplateVersionID ForeignKeyConstraint = "template_version_terraform_values_template_version_id_fkey" // ALTER TABLE ONLY template_version_terraform_values ADD CONSTRAINT template_version_terraform_values_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionVariablesTemplateVersionID ForeignKeyConstraint = "template_version_variables_template_version_id_fkey" // ALTER TABLE ONLY template_version_variables ADD CONSTRAINT template_version_variables_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionWorkspaceTagsTemplateVersionID ForeignKeyConstraint = "template_version_workspace_tags_template_version_id_fkey" // ALTER TABLE ONLY template_version_workspace_tags ADD CONSTRAINT template_version_workspace_tags_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionsCreatedBy ForeignKeyConstraint = "template_versions_created_by_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT; - ForeignKeyTemplateVersionsOrganizationID ForeignKeyConstraint = "template_versions_organization_id_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionsTemplateID ForeignKeyConstraint = "template_versions_template_id_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_template_id_fkey FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE CASCADE; - ForeignKeyTemplatesCreatedBy ForeignKeyConstraint = "templates_created_by_fkey" // ALTER TABLE ONLY templates ADD CONSTRAINT templates_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT; - ForeignKeyTemplatesOrganizationID ForeignKeyConstraint = "templates_organization_id_fkey" // ALTER TABLE ONLY templates ADD CONSTRAINT templates_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyUserAiProviderKeysAiProviderID ForeignKeyConstraint = "user_ai_provider_keys_ai_provider_id_fkey" // ALTER TABLE ONLY user_ai_provider_keys ADD CONSTRAINT user_ai_provider_keys_ai_provider_id_fkey FOREIGN KEY (ai_provider_id) REFERENCES ai_providers(id) ON DELETE CASCADE; - ForeignKeyUserAiProviderKeysAPIKeyKeyID ForeignKeyConstraint = "user_ai_provider_keys_api_key_key_id_fkey" // ALTER TABLE ONLY user_ai_provider_keys ADD CONSTRAINT user_ai_provider_keys_api_key_key_id_fkey FOREIGN KEY (api_key_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyUserAiProviderKeysUserID ForeignKeyConstraint = "user_ai_provider_keys_user_id_fkey" // ALTER TABLE ONLY user_ai_provider_keys ADD CONSTRAINT user_ai_provider_keys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyUserConfigsUserID ForeignKeyConstraint = "user_configs_user_id_fkey" // ALTER TABLE ONLY user_configs ADD CONSTRAINT user_configs_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyUserDeletedUserID ForeignKeyConstraint = "user_deleted_user_id_fkey" // ALTER TABLE ONLY user_deleted ADD CONSTRAINT user_deleted_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); - ForeignKeyUserLinksOauthAccessTokenKeyID ForeignKeyConstraint = "user_links_oauth_access_token_key_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_oauth_access_token_key_id_fkey FOREIGN KEY (oauth_access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyUserLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "user_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyUserLinksUserID ForeignKeyConstraint = "user_links_user_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyUserSecretsUserID ForeignKeyConstraint = "user_secrets_user_id_fkey" // ALTER TABLE ONLY user_secrets ADD CONSTRAINT user_secrets_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyUserSecretsValueKeyID ForeignKeyConstraint = "user_secrets_value_key_id_fkey" // ALTER TABLE ONLY user_secrets ADD CONSTRAINT user_secrets_value_key_id_fkey FOREIGN KEY (value_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyUserSkillsUserID ForeignKeyConstraint = "user_skills_user_id_fkey" // ALTER TABLE ONLY user_skills ADD CONSTRAINT user_skills_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyUserStatusChangesUserID ForeignKeyConstraint = "user_status_changes_user_id_fkey" // ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); - ForeignKeyWebpushSubscriptionsUserID ForeignKeyConstraint = "webpush_subscriptions_user_id_fkey" // ALTER TABLE ONLY webpush_subscriptions ADD CONSTRAINT webpush_subscriptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentDevcontainersSubagentID ForeignKeyConstraint = "workspace_agent_devcontainers_subagent_id_fkey" // ALTER TABLE ONLY workspace_agent_devcontainers ADD CONSTRAINT workspace_agent_devcontainers_subagent_id_fkey FOREIGN KEY (subagent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentDevcontainersWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_devcontainers_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_devcontainers ADD CONSTRAINT workspace_agent_devcontainers_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentLogSourcesWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_log_sources_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentMemoryResourceMonitorsAgentID ForeignKeyConstraint = "workspace_agent_memory_resource_monitors_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_memory_resource_monitors ADD CONSTRAINT workspace_agent_memory_resource_monitors_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentMetadataWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_metadata_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_metadata ADD CONSTRAINT workspace_agent_metadata_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentPortShareWorkspaceID ForeignKeyConstraint = "workspace_agent_port_share_workspace_id_fkey" // ALTER TABLE ONLY workspace_agent_port_share ADD CONSTRAINT workspace_agent_port_share_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentScriptTimingsScriptID ForeignKeyConstraint = "workspace_agent_script_timings_script_id_fkey" // ALTER TABLE ONLY workspace_agent_script_timings ADD CONSTRAINT workspace_agent_script_timings_script_id_fkey FOREIGN KEY (script_id) REFERENCES workspace_agent_scripts(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentScriptsWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_scripts_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_scripts ADD CONSTRAINT workspace_agent_scripts_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentStartupLogsAgentID ForeignKeyConstraint = "workspace_agent_startup_logs_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_logs ADD CONSTRAINT workspace_agent_startup_logs_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentVolumeResourceMonitorsAgentID ForeignKeyConstraint = "workspace_agent_volume_resource_monitors_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_volume_resource_monitors ADD CONSTRAINT workspace_agent_volume_resource_monitors_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentsParentID ForeignKeyConstraint = "workspace_agents_parent_id_fkey" // ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentsResourceID ForeignKeyConstraint = "workspace_agents_resource_id_fkey" // ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAppAuditSessionsAgentID ForeignKeyConstraint = "workspace_app_audit_sessions_agent_id_fkey" // ALTER TABLE ONLY workspace_app_audit_sessions ADD CONSTRAINT workspace_app_audit_sessions_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAppStatsAgentID ForeignKeyConstraint = "workspace_app_stats_agent_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id); - ForeignKeyWorkspaceAppStatsUserID ForeignKeyConstraint = "workspace_app_stats_user_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); - ForeignKeyWorkspaceAppStatsWorkspaceID ForeignKeyConstraint = "workspace_app_stats_workspace_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id); - ForeignKeyWorkspaceAppStatusesAgentID ForeignKeyConstraint = "workspace_app_statuses_agent_id_fkey" // ALTER TABLE ONLY workspace_app_statuses ADD CONSTRAINT workspace_app_statuses_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id); - 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; - ForeignKeyWorkspaceBuildOrchestrationChildTemplateVersionPrese ForeignKeyConstraint = "workspace_build_orchestration_child_template_version_prese_fkey" // ALTER TABLE ONLY workspace_build_orchestrations ADD CONSTRAINT workspace_build_orchestration_child_template_version_prese_fkey FOREIGN KEY (child_template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE SET NULL; - 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; - 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; - ForeignKeyWorkspaceBuildsTemplateVersionPresetID ForeignKeyConstraint = "workspace_builds_template_version_preset_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_template_version_preset_id_fkey FOREIGN KEY (template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE SET NULL; - ForeignKeyWorkspaceBuildsWorkspaceID ForeignKeyConstraint = "workspace_builds_workspace_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; - ForeignKeyWorkspaceModulesJobID ForeignKeyConstraint = "workspace_modules_job_id_fkey" // ALTER TABLE ONLY workspace_modules ADD CONSTRAINT workspace_modules_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; - ForeignKeyWorkspaceResourceMetadataWorkspaceResourceID ForeignKeyConstraint = "workspace_resource_metadata_workspace_resource_id_fkey" // ALTER TABLE ONLY workspace_resource_metadata ADD CONSTRAINT workspace_resource_metadata_workspace_resource_id_fkey FOREIGN KEY (workspace_resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE; - ForeignKeyWorkspaceResourcesJobID ForeignKeyConstraint = "workspace_resources_job_id_fkey" // ALTER TABLE ONLY workspace_resources ADD CONSTRAINT workspace_resources_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; - ForeignKeyWorkspacesOrganizationID ForeignKeyConstraint = "workspaces_organization_id_fkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT; - ForeignKeyWorkspacesOwnerID ForeignKeyConstraint = "workspaces_owner_id_fkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT; - ForeignKeyWorkspacesTemplateID ForeignKeyConstraint = "workspaces_template_id_fkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_template_id_fkey FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE RESTRICT; + ForeignKeyAiProviderKeysAPIKeyKeyID ForeignKeyConstraint = "ai_provider_keys_api_key_key_id_fkey" // ALTER TABLE ONLY ai_provider_keys ADD CONSTRAINT ai_provider_keys_api_key_key_id_fkey FOREIGN KEY (api_key_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyAiProviderKeysProviderID ForeignKeyConstraint = "ai_provider_keys_provider_id_fkey" // ALTER TABLE ONLY ai_provider_keys ADD CONSTRAINT ai_provider_keys_provider_id_fkey FOREIGN KEY (provider_id) REFERENCES ai_providers(id) ON DELETE CASCADE; + ForeignKeyAiProvidersSettingsKeyID ForeignKeyConstraint = "ai_providers_settings_key_id_fkey" // ALTER TABLE ONLY ai_providers ADD CONSTRAINT ai_providers_settings_key_id_fkey FOREIGN KEY (settings_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyAiSeatStateUserID ForeignKeyConstraint = "ai_seat_state_user_id_fkey" // ALTER TABLE ONLY ai_seat_state ADD CONSTRAINT ai_seat_state_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyAibridgeInterceptionsInitiatorID ForeignKeyConstraint = "aibridge_interceptions_initiator_id_fkey" // ALTER TABLE ONLY aibridge_interceptions ADD CONSTRAINT aibridge_interceptions_initiator_id_fkey FOREIGN KEY (initiator_id) REFERENCES users(id); + ForeignKeyAPIKeysUserIDUUID ForeignKeyConstraint = "api_keys_user_id_uuid_fkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyBoundaryLogsSessionID ForeignKeyConstraint = "boundary_logs_session_id_fkey" // ALTER TABLE ONLY boundary_logs ADD CONSTRAINT boundary_logs_session_id_fkey FOREIGN KEY (session_id) REFERENCES boundary_sessions(id) ON DELETE CASCADE; + ForeignKeyBoundarySessionsWorkspaceAgentID ForeignKeyConstraint = "boundary_sessions_workspace_agent_id_fkey" // ALTER TABLE ONLY boundary_sessions ADD CONSTRAINT boundary_sessions_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id); + ForeignKeyChatDebugRunsChatID ForeignKeyConstraint = "chat_debug_runs_chat_id_fkey" // ALTER TABLE ONLY chat_debug_runs ADD CONSTRAINT chat_debug_runs_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; + ForeignKeyChatDebugStepsChatID ForeignKeyConstraint = "chat_debug_steps_chat_id_fkey" // ALTER TABLE ONLY chat_debug_steps ADD CONSTRAINT chat_debug_steps_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; + ForeignKeyChatDiffStatusesChatID ForeignKeyConstraint = "chat_diff_statuses_chat_id_fkey" // ALTER TABLE ONLY chat_diff_statuses ADD CONSTRAINT chat_diff_statuses_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; + ForeignKeyChatFileLinksChatID ForeignKeyConstraint = "chat_file_links_chat_id_fkey" // ALTER TABLE ONLY chat_file_links ADD CONSTRAINT chat_file_links_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; + ForeignKeyChatFileLinksFileID ForeignKeyConstraint = "chat_file_links_file_id_fkey" // ALTER TABLE ONLY chat_file_links ADD CONSTRAINT chat_file_links_file_id_fkey FOREIGN KEY (file_id) REFERENCES chat_files(id) ON DELETE CASCADE; + ForeignKeyChatFilesOrganizationID ForeignKeyConstraint = "chat_files_organization_id_fkey" // ALTER TABLE ONLY chat_files ADD CONSTRAINT chat_files_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyChatFilesOwnerID ForeignKeyConstraint = "chat_files_owner_id_fkey" // ALTER TABLE ONLY chat_files ADD CONSTRAINT chat_files_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyChatMessagesAPIKeyID ForeignKeyConstraint = "chat_messages_api_key_id_fkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_api_key_id_fkey FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE SET NULL; + ForeignKeyChatMessagesChatID ForeignKeyConstraint = "chat_messages_chat_id_fkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; + ForeignKeyChatMessagesModelConfigID ForeignKeyConstraint = "chat_messages_model_config_id_fkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_model_config_id_fkey FOREIGN KEY (model_config_id) REFERENCES chat_model_configs(id); + ForeignKeyChatModelConfigsAiProviderID ForeignKeyConstraint = "chat_model_configs_ai_provider_id_fkey" // ALTER TABLE ONLY chat_model_configs ADD CONSTRAINT chat_model_configs_ai_provider_id_fkey FOREIGN KEY (ai_provider_id) REFERENCES ai_providers(id); + ForeignKeyChatModelConfigsCreatedBy ForeignKeyConstraint = "chat_model_configs_created_by_fkey" // ALTER TABLE ONLY chat_model_configs ADD CONSTRAINT chat_model_configs_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id); + ForeignKeyChatModelConfigsUpdatedBy ForeignKeyConstraint = "chat_model_configs_updated_by_fkey" // ALTER TABLE ONLY chat_model_configs ADD CONSTRAINT chat_model_configs_updated_by_fkey FOREIGN KEY (updated_by) REFERENCES users(id); + ForeignKeyChatQueuedMessagesAPIKeyID ForeignKeyConstraint = "chat_queued_messages_api_key_id_fkey" // ALTER TABLE ONLY chat_queued_messages ADD CONSTRAINT chat_queued_messages_api_key_id_fkey FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE SET NULL; + ForeignKeyChatQueuedMessagesChatID ForeignKeyConstraint = "chat_queued_messages_chat_id_fkey" // ALTER TABLE ONLY chat_queued_messages ADD CONSTRAINT chat_queued_messages_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; + ForeignKeyChatsAgentID ForeignKeyConstraint = "chats_agent_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE SET NULL; + ForeignKeyChatsBuildID ForeignKeyConstraint = "chats_build_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_build_id_fkey FOREIGN KEY (build_id) REFERENCES workspace_builds(id) ON DELETE SET NULL; + ForeignKeyChatsLastModelConfigID ForeignKeyConstraint = "chats_last_model_config_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_last_model_config_id_fkey FOREIGN KEY (last_model_config_id) REFERENCES chat_model_configs(id); + ForeignKeyChatsOrganizationID ForeignKeyConstraint = "chats_organization_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyChatsOwnerID ForeignKeyConstraint = "chats_owner_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyChatsParentChatID ForeignKeyConstraint = "chats_parent_chat_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_parent_chat_id_fkey FOREIGN KEY (parent_chat_id) REFERENCES chats(id) ON DELETE SET NULL; + ForeignKeyChatsRootChatID ForeignKeyConstraint = "chats_root_chat_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_root_chat_id_fkey FOREIGN KEY (root_chat_id) REFERENCES chats(id) ON DELETE SET NULL; + ForeignKeyChatsWorkspaceID ForeignKeyConstraint = "chats_workspace_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE SET NULL; + ForeignKeyConnectionLogsOrganizationID ForeignKeyConstraint = "connection_logs_organization_id_fkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyConnectionLogsWorkspaceID ForeignKeyConstraint = "connection_logs_workspace_id_fkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; + ForeignKeyConnectionLogsWorkspaceOwnerID ForeignKeyConstraint = "connection_logs_workspace_owner_id_fkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_workspace_owner_id_fkey FOREIGN KEY (workspace_owner_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyCryptoKeysSecretKeyID ForeignKeyConstraint = "crypto_keys_secret_key_id_fkey" // ALTER TABLE ONLY crypto_keys ADD CONSTRAINT crypto_keys_secret_key_id_fkey FOREIGN KEY (secret_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyFkChatDebugStepsRunChat ForeignKeyConstraint = "fk_chat_debug_steps_run_chat" // ALTER TABLE ONLY chat_debug_steps ADD CONSTRAINT fk_chat_debug_steps_run_chat FOREIGN KEY (run_id, chat_id) REFERENCES chat_debug_runs(id, chat_id) ON DELETE CASCADE; + ForeignKeyFkOauth2ProviderAppTokensUserID ForeignKeyConstraint = "fk_oauth2_provider_app_tokens_user_id" // ALTER TABLE ONLY oauth2_provider_app_tokens ADD CONSTRAINT fk_oauth2_provider_app_tokens_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyGitAuthLinksOauthAccessTokenKeyID ForeignKeyConstraint = "git_auth_links_oauth_access_token_key_id_fkey" // ALTER TABLE ONLY external_auth_links ADD CONSTRAINT git_auth_links_oauth_access_token_key_id_fkey FOREIGN KEY (oauth_access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyGitAuthLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "git_auth_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY external_auth_links ADD CONSTRAINT git_auth_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyGitSSHKeysUserID ForeignKeyConstraint = "gitsshkeys_user_id_fkey" // ALTER TABLE ONLY gitsshkeys ADD CONSTRAINT gitsshkeys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); + ForeignKeyGroupAiBudgetsGroupID ForeignKeyConstraint = "group_ai_budgets_group_id_fkey" // ALTER TABLE ONLY group_ai_budgets ADD CONSTRAINT group_ai_budgets_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE; + ForeignKeyGroupMembersGroupID ForeignKeyConstraint = "group_members_group_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE; + ForeignKeyGroupMembersUserID ForeignKeyConstraint = "group_members_user_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyGroupsOrganizationID ForeignKeyConstraint = "groups_organization_id_fkey" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyInboxNotificationsTemplateID ForeignKeyConstraint = "inbox_notifications_template_id_fkey" // ALTER TABLE ONLY inbox_notifications ADD CONSTRAINT inbox_notifications_template_id_fkey FOREIGN KEY (template_id) REFERENCES notification_templates(id) ON DELETE CASCADE; + ForeignKeyInboxNotificationsUserID ForeignKeyConstraint = "inbox_notifications_user_id_fkey" // ALTER TABLE ONLY inbox_notifications ADD CONSTRAINT inbox_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyJfrogXrayScansAgentID ForeignKeyConstraint = "jfrog_xray_scans_agent_id_fkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyJfrogXrayScansWorkspaceID ForeignKeyConstraint = "jfrog_xray_scans_workspace_id_fkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; + ForeignKeyMcpServerConfigsAPIKeyValueKeyID ForeignKeyConstraint = "mcp_server_configs_api_key_value_key_id_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_api_key_value_key_id_fkey FOREIGN KEY (api_key_value_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyMcpServerConfigsCreatedBy ForeignKeyConstraint = "mcp_server_configs_created_by_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL; + ForeignKeyMcpServerConfigsCustomHeadersKeyID ForeignKeyConstraint = "mcp_server_configs_custom_headers_key_id_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_custom_headers_key_id_fkey FOREIGN KEY (custom_headers_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyMcpServerConfigsOauth2ClientSecretKeyID ForeignKeyConstraint = "mcp_server_configs_oauth2_client_secret_key_id_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_oauth2_client_secret_key_id_fkey FOREIGN KEY (oauth2_client_secret_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyMcpServerConfigsUpdatedBy ForeignKeyConstraint = "mcp_server_configs_updated_by_fkey" // ALTER TABLE ONLY mcp_server_configs ADD CONSTRAINT mcp_server_configs_updated_by_fkey FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL; + ForeignKeyMcpServerUserTokensAccessTokenKeyID ForeignKeyConstraint = "mcp_server_user_tokens_access_token_key_id_fkey" // ALTER TABLE ONLY mcp_server_user_tokens ADD CONSTRAINT mcp_server_user_tokens_access_token_key_id_fkey FOREIGN KEY (access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyMcpServerUserTokensMcpServerConfigID ForeignKeyConstraint = "mcp_server_user_tokens_mcp_server_config_id_fkey" // ALTER TABLE ONLY mcp_server_user_tokens ADD CONSTRAINT mcp_server_user_tokens_mcp_server_config_id_fkey FOREIGN KEY (mcp_server_config_id) REFERENCES mcp_server_configs(id) ON DELETE CASCADE; + ForeignKeyMcpServerUserTokensRefreshTokenKeyID ForeignKeyConstraint = "mcp_server_user_tokens_refresh_token_key_id_fkey" // ALTER TABLE ONLY mcp_server_user_tokens ADD CONSTRAINT mcp_server_user_tokens_refresh_token_key_id_fkey FOREIGN KEY (refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyMcpServerUserTokensUserID ForeignKeyConstraint = "mcp_server_user_tokens_user_id_fkey" // ALTER TABLE ONLY mcp_server_user_tokens ADD CONSTRAINT mcp_server_user_tokens_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyNotificationMessagesNotificationTemplateID ForeignKeyConstraint = "notification_messages_notification_template_id_fkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE; + ForeignKeyNotificationMessagesUserID ForeignKeyConstraint = "notification_messages_user_id_fkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyNotificationPreferencesNotificationTemplateID ForeignKeyConstraint = "notification_preferences_notification_template_id_fkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE; + ForeignKeyNotificationPreferencesUserID ForeignKeyConstraint = "notification_preferences_user_id_fkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyOauth2ProviderAppCodesAppID ForeignKeyConstraint = "oauth2_provider_app_codes_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE; + ForeignKeyOauth2ProviderAppCodesUserID ForeignKeyConstraint = "oauth2_provider_app_codes_user_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyOauth2ProviderAppSecretsAppID ForeignKeyConstraint = "oauth2_provider_app_secrets_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_secrets ADD CONSTRAINT oauth2_provider_app_secrets_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE; + ForeignKeyOauth2ProviderAppTokensAPIKeyID ForeignKeyConstraint = "oauth2_provider_app_tokens_api_key_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_tokens ADD CONSTRAINT oauth2_provider_app_tokens_api_key_id_fkey FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE CASCADE; + ForeignKeyOauth2ProviderAppTokensAppSecretID ForeignKeyConstraint = "oauth2_provider_app_tokens_app_secret_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_tokens ADD CONSTRAINT oauth2_provider_app_tokens_app_secret_id_fkey FOREIGN KEY (app_secret_id) REFERENCES oauth2_provider_app_secrets(id) ON DELETE CASCADE; + ForeignKeyOrganizationMembersOrganizationIDUUID ForeignKeyConstraint = "organization_members_organization_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_organization_id_uuid_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyOrganizationMembersUserIDUUID ForeignKeyConstraint = "organization_members_user_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyParameterSchemasJobID ForeignKeyConstraint = "parameter_schemas_job_id_fkey" // ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; + ForeignKeyProvisionerDaemonsKeyID ForeignKeyConstraint = "provisioner_daemons_key_id_fkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_key_id_fkey FOREIGN KEY (key_id) REFERENCES provisioner_keys(id) ON DELETE CASCADE; + ForeignKeyProvisionerDaemonsOrganizationID ForeignKeyConstraint = "provisioner_daemons_organization_id_fkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyProvisionerJobLogsJobID ForeignKeyConstraint = "provisioner_job_logs_job_id_fkey" // ALTER TABLE ONLY provisioner_job_logs ADD CONSTRAINT provisioner_job_logs_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; + ForeignKeyProvisionerJobTimingsJobID ForeignKeyConstraint = "provisioner_job_timings_job_id_fkey" // ALTER TABLE ONLY provisioner_job_timings ADD CONSTRAINT provisioner_job_timings_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; + ForeignKeyProvisionerJobsOrganizationID ForeignKeyConstraint = "provisioner_jobs_organization_id_fkey" // ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyProvisionerKeysOrganizationID ForeignKeyConstraint = "provisioner_keys_organization_id_fkey" // ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyTailnetPeersCoordinatorID ForeignKeyConstraint = "tailnet_peers_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_peers ADD CONSTRAINT tailnet_peers_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; + ForeignKeyTailnetTunnelsCoordinatorID ForeignKeyConstraint = "tailnet_tunnels_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; + ForeignKeyTaskSnapshotsTaskID ForeignKeyConstraint = "task_snapshots_task_id_fkey" // ALTER TABLE ONLY task_snapshots ADD CONSTRAINT task_snapshots_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; + ForeignKeyTaskWorkspaceAppsTaskID ForeignKeyConstraint = "task_workspace_apps_task_id_fkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; + ForeignKeyTaskWorkspaceAppsWorkspaceAgentID ForeignKeyConstraint = "task_workspace_apps_workspace_agent_id_fkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyTaskWorkspaceAppsWorkspaceAppID ForeignKeyConstraint = "task_workspace_apps_workspace_app_id_fkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_workspace_app_id_fkey FOREIGN KEY (workspace_app_id) REFERENCES workspace_apps(id) ON DELETE CASCADE; + ForeignKeyTasksOrganizationID ForeignKeyConstraint = "tasks_organization_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyTasksOwnerID ForeignKeyConstraint = "tasks_owner_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyTasksTemplateVersionID ForeignKeyConstraint = "tasks_template_version_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; + ForeignKeyTasksWorkspaceID ForeignKeyConstraint = "tasks_workspace_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionParametersTemplateVersionID ForeignKeyConstraint = "template_version_parameters_template_version_id_fkey" // ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionPresetParametTemplateVersionPresetID ForeignKeyConstraint = "template_version_preset_paramet_template_version_preset_id_fkey" // ALTER TABLE ONLY template_version_preset_parameters ADD CONSTRAINT template_version_preset_paramet_template_version_preset_id_fkey FOREIGN KEY (template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionPresetPrebuildSchedulesPresetID ForeignKeyConstraint = "template_version_preset_prebuild_schedules_preset_id_fkey" // ALTER TABLE ONLY template_version_preset_prebuild_schedules ADD CONSTRAINT template_version_preset_prebuild_schedules_preset_id_fkey FOREIGN KEY (preset_id) REFERENCES template_version_presets(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionPresetsTemplateVersionID ForeignKeyConstraint = "template_version_presets_template_version_id_fkey" // ALTER TABLE ONLY template_version_presets ADD CONSTRAINT template_version_presets_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionTerraformValuesCachedModuleFiles ForeignKeyConstraint = "template_version_terraform_values_cached_module_files_fkey" // ALTER TABLE ONLY template_version_terraform_values ADD CONSTRAINT template_version_terraform_values_cached_module_files_fkey FOREIGN KEY (cached_module_files) REFERENCES files(id); + ForeignKeyTemplateVersionTerraformValuesTemplateVersionID ForeignKeyConstraint = "template_version_terraform_values_template_version_id_fkey" // ALTER TABLE ONLY template_version_terraform_values ADD CONSTRAINT template_version_terraform_values_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionVariablesTemplateVersionID ForeignKeyConstraint = "template_version_variables_template_version_id_fkey" // ALTER TABLE ONLY template_version_variables ADD CONSTRAINT template_version_variables_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionWorkspaceTagsTemplateVersionID ForeignKeyConstraint = "template_version_workspace_tags_template_version_id_fkey" // ALTER TABLE ONLY template_version_workspace_tags ADD CONSTRAINT template_version_workspace_tags_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionsCreatedBy ForeignKeyConstraint = "template_versions_created_by_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT; + ForeignKeyTemplateVersionsOrganizationID ForeignKeyConstraint = "template_versions_organization_id_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionsTemplateID ForeignKeyConstraint = "template_versions_template_id_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_template_id_fkey FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE CASCADE; + ForeignKeyTemplatesCreatedBy ForeignKeyConstraint = "templates_created_by_fkey" // ALTER TABLE ONLY templates ADD CONSTRAINT templates_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT; + ForeignKeyTemplatesOrganizationID ForeignKeyConstraint = "templates_organization_id_fkey" // ALTER TABLE ONLY templates ADD CONSTRAINT templates_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyUserAiProviderKeysAiProviderID ForeignKeyConstraint = "user_ai_provider_keys_ai_provider_id_fkey" // ALTER TABLE ONLY user_ai_provider_keys ADD CONSTRAINT user_ai_provider_keys_ai_provider_id_fkey FOREIGN KEY (ai_provider_id) REFERENCES ai_providers(id) ON DELETE CASCADE; + ForeignKeyUserAiProviderKeysAPIKeyKeyID ForeignKeyConstraint = "user_ai_provider_keys_api_key_key_id_fkey" // ALTER TABLE ONLY user_ai_provider_keys ADD CONSTRAINT user_ai_provider_keys_api_key_key_id_fkey FOREIGN KEY (api_key_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyUserAiProviderKeysUserID ForeignKeyConstraint = "user_ai_provider_keys_user_id_fkey" // ALTER TABLE ONLY user_ai_provider_keys ADD CONSTRAINT user_ai_provider_keys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyUserConfigsUserID ForeignKeyConstraint = "user_configs_user_id_fkey" // ALTER TABLE ONLY user_configs ADD CONSTRAINT user_configs_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyUserDeletedUserID ForeignKeyConstraint = "user_deleted_user_id_fkey" // ALTER TABLE ONLY user_deleted ADD CONSTRAINT user_deleted_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); + ForeignKeyUserLinksOauthAccessTokenKeyID ForeignKeyConstraint = "user_links_oauth_access_token_key_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_oauth_access_token_key_id_fkey FOREIGN KEY (oauth_access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyUserLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "user_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyUserLinksUserID ForeignKeyConstraint = "user_links_user_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyUserSecretsUserID ForeignKeyConstraint = "user_secrets_user_id_fkey" // ALTER TABLE ONLY user_secrets ADD CONSTRAINT user_secrets_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyUserSecretsValueKeyID ForeignKeyConstraint = "user_secrets_value_key_id_fkey" // ALTER TABLE ONLY user_secrets ADD CONSTRAINT user_secrets_value_key_id_fkey FOREIGN KEY (value_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyUserSkillsUserID ForeignKeyConstraint = "user_skills_user_id_fkey" // ALTER TABLE ONLY user_skills ADD CONSTRAINT user_skills_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyUserStatusChangesUserID ForeignKeyConstraint = "user_status_changes_user_id_fkey" // ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); + ForeignKeyWebpushSubscriptionsUserID ForeignKeyConstraint = "webpush_subscriptions_user_id_fkey" // ALTER TABLE ONLY webpush_subscriptions ADD CONSTRAINT webpush_subscriptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentDevcontainersSubagentID ForeignKeyConstraint = "workspace_agent_devcontainers_subagent_id_fkey" // ALTER TABLE ONLY workspace_agent_devcontainers ADD CONSTRAINT workspace_agent_devcontainers_subagent_id_fkey FOREIGN KEY (subagent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentDevcontainersWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_devcontainers_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_devcontainers ADD CONSTRAINT workspace_agent_devcontainers_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentLogSourcesWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_log_sources_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentMemoryResourceMonitorsAgentID ForeignKeyConstraint = "workspace_agent_memory_resource_monitors_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_memory_resource_monitors ADD CONSTRAINT workspace_agent_memory_resource_monitors_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentMetadataWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_metadata_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_metadata ADD CONSTRAINT workspace_agent_metadata_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentPortShareWorkspaceID ForeignKeyConstraint = "workspace_agent_port_share_workspace_id_fkey" // ALTER TABLE ONLY workspace_agent_port_share ADD CONSTRAINT workspace_agent_port_share_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentScriptTimingsScriptID ForeignKeyConstraint = "workspace_agent_script_timings_script_id_fkey" // ALTER TABLE ONLY workspace_agent_script_timings ADD CONSTRAINT workspace_agent_script_timings_script_id_fkey FOREIGN KEY (script_id) REFERENCES workspace_agent_scripts(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentScriptsWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_scripts_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_scripts ADD CONSTRAINT workspace_agent_scripts_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentStartupLogsAgentID ForeignKeyConstraint = "workspace_agent_startup_logs_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_logs ADD CONSTRAINT workspace_agent_startup_logs_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentVolumeResourceMonitorsAgentID ForeignKeyConstraint = "workspace_agent_volume_resource_monitors_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_volume_resource_monitors ADD CONSTRAINT workspace_agent_volume_resource_monitors_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentsParentID ForeignKeyConstraint = "workspace_agents_parent_id_fkey" // ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentsResourceID ForeignKeyConstraint = "workspace_agents_resource_id_fkey" // ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAppAuditSessionsAgentID ForeignKeyConstraint = "workspace_app_audit_sessions_agent_id_fkey" // ALTER TABLE ONLY workspace_app_audit_sessions ADD CONSTRAINT workspace_app_audit_sessions_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAppStatsAgentID ForeignKeyConstraint = "workspace_app_stats_agent_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id); + ForeignKeyWorkspaceAppStatsUserID ForeignKeyConstraint = "workspace_app_stats_user_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); + ForeignKeyWorkspaceAppStatsWorkspaceID ForeignKeyConstraint = "workspace_app_stats_workspace_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id); + ForeignKeyWorkspaceAppStatusesAgentID ForeignKeyConstraint = "workspace_app_statuses_agent_id_fkey" // ALTER TABLE ONLY workspace_app_statuses ADD CONSTRAINT workspace_app_statuses_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id); + 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; + ForeignKeyWorkspaceBuildsTemplateVersionPresetID ForeignKeyConstraint = "workspace_builds_template_version_preset_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_template_version_preset_id_fkey FOREIGN KEY (template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE SET NULL; + ForeignKeyWorkspaceBuildsWorkspaceID ForeignKeyConstraint = "workspace_builds_workspace_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; + ForeignKeyWorkspaceModulesJobID ForeignKeyConstraint = "workspace_modules_job_id_fkey" // ALTER TABLE ONLY workspace_modules ADD CONSTRAINT workspace_modules_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; + ForeignKeyWorkspaceResourceMetadataWorkspaceResourceID ForeignKeyConstraint = "workspace_resource_metadata_workspace_resource_id_fkey" // ALTER TABLE ONLY workspace_resource_metadata ADD CONSTRAINT workspace_resource_metadata_workspace_resource_id_fkey FOREIGN KEY (workspace_resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE; + ForeignKeyWorkspaceResourcesJobID ForeignKeyConstraint = "workspace_resources_job_id_fkey" // ALTER TABLE ONLY workspace_resources ADD CONSTRAINT workspace_resources_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; + ForeignKeyWorkspacesOrganizationID ForeignKeyConstraint = "workspaces_organization_id_fkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT; + ForeignKeyWorkspacesOwnerID ForeignKeyConstraint = "workspaces_owner_id_fkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT; + ForeignKeyWorkspacesTemplateID ForeignKeyConstraint = "workspaces_template_id_fkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_template_id_fkey FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE RESTRICT; ) diff --git a/coderd/database/migrations/000510_workspace_build_orchestrations.down.sql b/coderd/database/migrations/000510_workspace_build_orchestrations.down.sql index 0bae8de175a..b84b4fae00d 100644 --- a/coderd/database/migrations/000510_workspace_build_orchestrations.down.sql +++ b/coderd/database/migrations/000510_workspace_build_orchestrations.down.sql @@ -1,4 +1,7 @@ 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; diff --git a/coderd/database/migrations/000510_workspace_build_orchestrations.up.sql b/coderd/database/migrations/000510_workspace_build_orchestrations.up.sql index c0ba67f6d54..14829762678 100644 --- a/coderd/database/migrations/000510_workspace_build_orchestrations.up.sql +++ b/coderd/database/migrations/000510_workspace_build_orchestrations.up.sql @@ -5,6 +5,10 @@ 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, @@ -14,7 +18,7 @@ CREATE TABLE workspace_build_orchestrations ( 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 REFERENCES template_version_presets(id) ON DELETE SET NULL, + 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, @@ -37,11 +41,24 @@ CREATE TABLE workspace_build_orchestrations ( 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 diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 9278382cc0c..fd7e0138815 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -8731,6 +8731,149 @@ func TestUpdateWorkspaceBuildOrchestrationCompletedByIDWorkspaceMismatch(t *test 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() diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index c0944cf5b2a..1dc598814de 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -88,6 +88,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); From f4351c99b4748335d8b8387704a15c9f66ca5eca Mon Sep 17 00:00:00 2001 From: George Katsitadze Date: Tue, 16 Jun 2026 12:26:17 -0700 Subject: [PATCH 06/13] rename constraints with long names --- coderd/database/check_constraint.go | 100 +++++++++--------- coderd/database/dump.sql | 2 +- ...0510_workspace_build_orchestrations.up.sql | 2 +- 3 files changed, 52 insertions(+), 52 deletions(-) diff --git a/coderd/database/check_constraint.go b/coderd/database/check_constraint.go index 01c2dd19f47..455c4443f01 100644 --- a/coderd/database/check_constraint.go +++ b/coderd/database/check_constraint.go @@ -6,54 +6,54 @@ type CheckConstraint string // CheckConstraint enums. const ( - 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 - 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 - 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 - 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 - CheckWorkspaceBuildOrchestrationsChildPresetVersionCheck CheckConstraint = "workspace_build_orchestrations_child_preset_version_check" // workspace_build_orchestrations - CheckWorkspaceBuildOrchestrationsChildRichParameterValuesChec CheckConstraint = "workspace_build_orchestrations_child_rich_parameter_values_chec" // 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 + 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 + 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 + 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 + 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 ) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 079ebea2041..eae4e552d5d 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -3459,8 +3459,8 @@ CREATE TABLE workspace_build_orchestrations ( 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_child_rich_parameter_values_chec CHECK ((jsonb_typeof(child_rich_parameter_values) = 'array'::text)), 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]))) diff --git a/coderd/database/migrations/000510_workspace_build_orchestrations.up.sql b/coderd/database/migrations/000510_workspace_build_orchestrations.up.sql index 14829762678..40aa64ba94f 100644 --- a/coderd/database/migrations/000510_workspace_build_orchestrations.up.sql +++ b/coderd/database/migrations/000510_workspace_build_orchestrations.up.sql @@ -32,7 +32,7 @@ CREATE TABLE workspace_build_orchestrations ( CONSTRAINT workspace_build_orchestrations_completed_child_check CHECK ( status <> 'completed' OR child_build_id IS NOT NULL ), - CONSTRAINT workspace_build_orchestrations_child_rich_parameter_values_check CHECK ( + CONSTRAINT workspace_build_orchestrations_child_parameters_check CHECK ( jsonb_typeof(child_rich_parameter_values) = 'array' ), CONSTRAINT workspace_build_orchestrations_attempt_count_check CHECK ( From 46aff2b44f301705b4a6d8d1590796b1c8a0846c Mon Sep 17 00:00:00 2001 From: George Katsitadze Date: Tue, 16 Jun 2026 12:54:20 -0700 Subject: [PATCH 07/13] add ResourceWorkspaceBuildOrchestration and use it for read/update authz --- coderd/apidoc/docs.go | 12 +++++ coderd/apidoc/swagger.json | 12 +++++ coderd/database/dbauthz/dbauthz.go | 59 +++++++++++++------------ coderd/database/dbauthz/dbauthz_test.go | 10 ++--- coderd/rbac/object_gen.go | 11 +++++ coderd/rbac/policy/policy.go | 8 ++++ coderd/rbac/roles.go | 2 +- coderd/rbac/roles_test.go | 9 ++++ coderd/rbac/scopes_constants_gen.go | 12 +++++ codersdk/apikey_scopes_gen.go | 5 +++ codersdk/rbacresources_gen.go | 2 + docs/reference/api/members.md | 40 ++++++++--------- docs/reference/api/schemas.md | 12 ++--- docs/reference/api/users.md | 10 ++--- site/src/api/rbacresourcesGenerated.ts | 6 +++ site/src/api/typesGenerated.ts | 12 +++++ 16 files changed, 156 insertions(+), 66 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 65f2c1927c2..130c230da04 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -15434,6 +15434,11 @@ const docTemplate = `{ "workspace_agent_resource_monitor:create", "workspace_agent_resource_monitor:read", "workspace_agent_resource_monitor:update", + "workspace_build_orchestration:*", + "workspace_build_orchestration:create", + "workspace_build_orchestration:delete", + "workspace_build_orchestration:read", + "workspace_build_orchestration:update", "workspace_dormant:*", "workspace_dormant:application_connect", "workspace_dormant:create", @@ -15660,6 +15665,11 @@ const docTemplate = `{ "APIKeyScopeWorkspaceAgentResourceMonitorCreate", "APIKeyScopeWorkspaceAgentResourceMonitorRead", "APIKeyScopeWorkspaceAgentResourceMonitorUpdate", + "APIKeyScopeWorkspaceBuildOrchestrationAll", + "APIKeyScopeWorkspaceBuildOrchestrationCreate", + "APIKeyScopeWorkspaceBuildOrchestrationDelete", + "APIKeyScopeWorkspaceBuildOrchestrationRead", + "APIKeyScopeWorkspaceBuildOrchestrationUpdate", "APIKeyScopeWorkspaceDormantAll", "APIKeyScopeWorkspaceDormantApplicationConnect", "APIKeyScopeWorkspaceDormantCreate", @@ -22320,6 +22330,7 @@ const docTemplate = `{ "workspace", "workspace_agent_devcontainers", "workspace_agent_resource_monitor", + "workspace_build_orchestration", "workspace_dormant", "workspace_proxy" ], @@ -22370,6 +22381,7 @@ const docTemplate = `{ "ResourceWorkspace", "ResourceWorkspaceAgentDevcontainers", "ResourceWorkspaceAgentResourceMonitor", + "ResourceWorkspaceBuildOrchestration", "ResourceWorkspaceDormant", "ResourceWorkspaceProxy" ] diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 33ffe3e4b4f..e3eb598f8b6 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -13830,6 +13830,11 @@ "workspace_agent_resource_monitor:create", "workspace_agent_resource_monitor:read", "workspace_agent_resource_monitor:update", + "workspace_build_orchestration:*", + "workspace_build_orchestration:create", + "workspace_build_orchestration:delete", + "workspace_build_orchestration:read", + "workspace_build_orchestration:update", "workspace_dormant:*", "workspace_dormant:application_connect", "workspace_dormant:create", @@ -14056,6 +14061,11 @@ "APIKeyScopeWorkspaceAgentResourceMonitorCreate", "APIKeyScopeWorkspaceAgentResourceMonitorRead", "APIKeyScopeWorkspaceAgentResourceMonitorUpdate", + "APIKeyScopeWorkspaceBuildOrchestrationAll", + "APIKeyScopeWorkspaceBuildOrchestrationCreate", + "APIKeyScopeWorkspaceBuildOrchestrationDelete", + "APIKeyScopeWorkspaceBuildOrchestrationRead", + "APIKeyScopeWorkspaceBuildOrchestrationUpdate", "APIKeyScopeWorkspaceDormantAll", "APIKeyScopeWorkspaceDormantApplicationConnect", "APIKeyScopeWorkspaceDormantCreate", @@ -20463,6 +20473,7 @@ "workspace", "workspace_agent_devcontainers", "workspace_agent_resource_monitor", + "workspace_build_orchestration", "workspace_dormant", "workspace_proxy" ], @@ -20513,6 +20524,7 @@ "ResourceWorkspace", "ResourceWorkspaceAgentDevcontainers", "ResourceWorkspaceAgentResourceMonitor", + "ResourceWorkspaceBuildOrchestration", "ResourceWorkspaceDormant", "ResourceWorkspaceProxy" ] diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index a79a5d73054..620ae1ae4a3 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -432,30 +432,31 @@ 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.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}, }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, @@ -3673,7 +3674,7 @@ func (q *querier) GetMCPServerUserTokensByUserID(ctx context.Context, userID uui } func (q *querier) GetNextPendingWorkspaceBuildOrchestrationForUpdate(ctx context.Context) (database.WorkspaceBuildOrchestration, error) { - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspaceBuildOrchestration.AnyOrganization()); err != nil { return database.WorkspaceBuildOrchestration{}, err } return q.db.GetNextPendingWorkspaceBuildOrchestrationForUpdate(ctx) @@ -7933,28 +7934,28 @@ func (q *querier) UpdateWorkspaceBuildFlagsByID(ctx context.Context, arg databas } func (q *querier) UpdateWorkspaceBuildOrchestrationCanceledByID(ctx context.Context, arg database.UpdateWorkspaceBuildOrchestrationCanceledByIDParams) (database.WorkspaceBuildOrchestration, error) { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { + 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.ResourceSystem); err != nil { + 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.ResourceSystem); err != nil { + 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.ResourceSystem); err != nil { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspaceBuildOrchestration.AnyOrganization()); err != nil { return database.WorkspaceBuildOrchestration{}, err } return q.db.UpdateWorkspaceBuildOrchestrationRetryByID(ctx, arg) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 0939eb4c861..c4c66774349 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -3871,7 +3871,7 @@ func (s *MethodTestSuite) TestWorkspace() { orchestration := testutil.Fake(s.T(), faker, database.WorkspaceBuildOrchestration{}) dbm.EXPECT().GetNextPendingWorkspaceBuildOrchestrationForUpdate(gomock.Any()).Return(orchestration, nil).AnyTimes() check.Args(). - Asserts(rbac.ResourceSystem, policy.ActionRead). + Asserts(rbac.ResourceWorkspaceBuildOrchestration.AnyOrganization(), policy.ActionRead). Returns(orchestration) })) s.Run("UpdateWorkspaceBuildOrchestrationCanceledByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { @@ -3882,7 +3882,7 @@ func (s *MethodTestSuite) TestWorkspace() { } dbm.EXPECT().UpdateWorkspaceBuildOrchestrationCanceledByID(gomock.Any(), arg).Return(orchestration, nil).AnyTimes() check.Args(arg). - Asserts(rbac.ResourceSystem, policy.ActionUpdate). + Asserts(rbac.ResourceWorkspaceBuildOrchestration.AnyOrganization(), policy.ActionUpdate). Returns(orchestration) })) s.Run("UpdateWorkspaceBuildOrchestrationCompletedByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { @@ -3894,7 +3894,7 @@ func (s *MethodTestSuite) TestWorkspace() { } dbm.EXPECT().UpdateWorkspaceBuildOrchestrationCompletedByID(gomock.Any(), arg).Return(orchestration, nil).AnyTimes() check.Args(arg). - Asserts(rbac.ResourceSystem, policy.ActionUpdate). + Asserts(rbac.ResourceWorkspaceBuildOrchestration.AnyOrganization(), policy.ActionUpdate). Returns(orchestration) })) s.Run("UpdateWorkspaceBuildOrchestrationFailedByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { @@ -3906,7 +3906,7 @@ func (s *MethodTestSuite) TestWorkspace() { } dbm.EXPECT().UpdateWorkspaceBuildOrchestrationFailedByID(gomock.Any(), arg).Return(orchestration, nil).AnyTimes() check.Args(arg). - Asserts(rbac.ResourceSystem, policy.ActionUpdate). + Asserts(rbac.ResourceWorkspaceBuildOrchestration.AnyOrganization(), policy.ActionUpdate). Returns(orchestration) })) s.Run("UpdateWorkspaceBuildOrchestrationRetryByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { @@ -3920,7 +3920,7 @@ func (s *MethodTestSuite) TestWorkspace() { } dbm.EXPECT().UpdateWorkspaceBuildOrchestrationRetryByID(gomock.Any(), arg).Return(orchestration, nil).AnyTimes() check.Args(arg). - Asserts(rbac.ResourceSystem, policy.ActionUpdate). + Asserts(rbac.ResourceWorkspaceBuildOrchestration.AnyOrganization(), policy.ActionUpdate). Returns(orchestration) })) s.Run("Start/InsertWorkspaceBuildParameters", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index 340221f611c..6b028a1f0e5 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -438,6 +438,16 @@ var ( Type: "workspace_agent_resource_monitor", } + // ResourceWorkspaceBuildOrchestration + // Valid Actions + // - "ActionCreate" :: create a workspace build orchestration + // - "ActionDelete" :: delete a workspace build orchestration + // - "ActionRead" :: read a workspace build orchestration + // - "ActionUpdate" :: update a workspace build orchestration + ResourceWorkspaceBuildOrchestration = Object{ + Type: "workspace_build_orchestration", + } + // ResourceWorkspaceDormant // Valid Actions // - "ActionApplicationConnect" :: connect to workspace apps via browser @@ -515,6 +525,7 @@ func AllResources() []Objecter { ResourceWorkspace, ResourceWorkspaceAgentDevcontainers, ResourceWorkspaceAgentResourceMonitor, + ResourceWorkspaceBuildOrchestration, ResourceWorkspaceDormant, ResourceWorkspaceProxy, } diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index 7d7a42110db..9fb37634f38 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -140,6 +140,14 @@ var RBACPermissions = map[string]PermissionDefinition{ ActionRead: "read and use a workspace proxy", }, }, + "workspace_build_orchestration": { + Actions: map[Action]ActionDefinition{ + ActionCreate: "create a workspace build orchestration", + ActionRead: "read a workspace build orchestration", + ActionUpdate: "update a workspace build orchestration", + ActionDelete: "delete a workspace build orchestration", + }, + }, "license": { Actions: map[Action]ActionDefinition{ ActionCreate: "create a license", diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index cbaf49f9c0e..08268b356ee 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -465,7 +465,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { // Org admins should not have workspace exec perms. organizationID.String(): { Org: append( - allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceAssignRole, ResourceUserSecret, ResourceBoundaryUsage, ResourceAiSeat), + allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceAssignRole, ResourceUserSecret, ResourceBoundaryUsage, ResourceAiSeat, ResourceWorkspaceBuildOrchestration), Permissions(map[string][]policy.Action{ ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH), ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent}, diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index 0170d308e06..1fa6f06cbca 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -906,6 +906,15 @@ func TestRolePermissions(t *testing.T) { false: {}, }, }, + { + Name: "WorkspaceBuildOrchestration", + Actions: crud, + Resource: rbac.ResourceWorkspaceBuildOrchestration.InOrg(orgID), + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, orgAdmin, templateAdmin, orgTemplateAdmin, userAdmin, orgUserAdmin, orgAuditor}, + }, + }, { // Any owner/admin across may access any users' preferences // Members may not access other members' preferences diff --git a/coderd/rbac/scopes_constants_gen.go b/coderd/rbac/scopes_constants_gen.go index c12cba430ae..a56066c150d 100644 --- a/coderd/rbac/scopes_constants_gen.go +++ b/coderd/rbac/scopes_constants_gen.go @@ -157,6 +157,10 @@ const ( ScopeWorkspaceAgentResourceMonitorCreate ScopeName = "workspace_agent_resource_monitor:create" ScopeWorkspaceAgentResourceMonitorRead ScopeName = "workspace_agent_resource_monitor:read" ScopeWorkspaceAgentResourceMonitorUpdate ScopeName = "workspace_agent_resource_monitor:update" + ScopeWorkspaceBuildOrchestrationCreate ScopeName = "workspace_build_orchestration:create" + ScopeWorkspaceBuildOrchestrationDelete ScopeName = "workspace_build_orchestration:delete" + ScopeWorkspaceBuildOrchestrationRead ScopeName = "workspace_build_orchestration:read" + ScopeWorkspaceBuildOrchestrationUpdate ScopeName = "workspace_build_orchestration:update" ScopeWorkspaceDormantApplicationConnect ScopeName = "workspace_dormant:application_connect" ScopeWorkspaceDormantCreate ScopeName = "workspace_dormant:create" ScopeWorkspaceDormantCreateAgent ScopeName = "workspace_dormant:create_agent" @@ -334,6 +338,10 @@ func (e ScopeName) Valid() bool { ScopeWorkspaceAgentResourceMonitorCreate, ScopeWorkspaceAgentResourceMonitorRead, ScopeWorkspaceAgentResourceMonitorUpdate, + ScopeWorkspaceBuildOrchestrationCreate, + ScopeWorkspaceBuildOrchestrationDelete, + ScopeWorkspaceBuildOrchestrationRead, + ScopeWorkspaceBuildOrchestrationUpdate, ScopeWorkspaceDormantApplicationConnect, ScopeWorkspaceDormantCreate, ScopeWorkspaceDormantCreateAgent, @@ -512,6 +520,10 @@ func AllScopeNameValues() []ScopeName { ScopeWorkspaceAgentResourceMonitorCreate, ScopeWorkspaceAgentResourceMonitorRead, ScopeWorkspaceAgentResourceMonitorUpdate, + ScopeWorkspaceBuildOrchestrationCreate, + ScopeWorkspaceBuildOrchestrationDelete, + ScopeWorkspaceBuildOrchestrationRead, + ScopeWorkspaceBuildOrchestrationUpdate, ScopeWorkspaceDormantApplicationConnect, ScopeWorkspaceDormantCreate, ScopeWorkspaceDormantCreateAgent, diff --git a/codersdk/apikey_scopes_gen.go b/codersdk/apikey_scopes_gen.go index 7bad39ccc25..73606fb710e 100644 --- a/codersdk/apikey_scopes_gen.go +++ b/codersdk/apikey_scopes_gen.go @@ -210,6 +210,11 @@ const ( APIKeyScopeWorkspaceAgentResourceMonitorCreate APIKeyScope = "workspace_agent_resource_monitor:create" APIKeyScopeWorkspaceAgentResourceMonitorRead APIKeyScope = "workspace_agent_resource_monitor:read" APIKeyScopeWorkspaceAgentResourceMonitorUpdate APIKeyScope = "workspace_agent_resource_monitor:update" + APIKeyScopeWorkspaceBuildOrchestrationAll 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" APIKeyScopeWorkspaceDormantAll APIKeyScope = "workspace_dormant:*" APIKeyScopeWorkspaceDormantApplicationConnect APIKeyScope = "workspace_dormant:application_connect" APIKeyScopeWorkspaceDormantCreate APIKeyScope = "workspace_dormant:create" diff --git a/codersdk/rbacresources_gen.go b/codersdk/rbacresources_gen.go index 11b64881826..00f3534a7a2 100644 --- a/codersdk/rbacresources_gen.go +++ b/codersdk/rbacresources_gen.go @@ -50,6 +50,7 @@ const ( ResourceWorkspace RBACResource = "workspace" ResourceWorkspaceAgentDevcontainers RBACResource = "workspace_agent_devcontainers" ResourceWorkspaceAgentResourceMonitor RBACResource = "workspace_agent_resource_monitor" + ResourceWorkspaceBuildOrchestration RBACResource = "workspace_build_orchestration" ResourceWorkspaceDormant RBACResource = "workspace_dormant" ResourceWorkspaceProxy RBACResource = "workspace_proxy" ) @@ -126,6 +127,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{ ResourceWorkspace: {ActionApplicationConnect, ActionCreate, ActionCreateAgent, ActionDelete, ActionDeleteAgent, ActionRead, ActionShare, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate, ActionUpdateAgent}, ResourceWorkspaceAgentDevcontainers: {ActionCreate}, ResourceWorkspaceAgentResourceMonitor: {ActionCreate, ActionRead, ActionUpdate}, + ResourceWorkspaceBuildOrchestration: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceWorkspaceDormant: {ActionApplicationConnect, ActionCreate, ActionCreateAgent, ActionDelete, ActionDeleteAgent, ActionRead, ActionShare, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate, ActionUpdateAgent}, ResourceWorkspaceProxy: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, } diff --git a/docs/reference/api/members.md b/docs/reference/api/members.md index 1556ced5578..620bde4aeed 100644 --- a/docs/reference/api/members.md +++ b/docs/reference/api/members.md @@ -193,10 +193,10 @@ Status Code **200** #### Enumerated Values -| Property | Value(s) | -|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | +| Property | Value(s) | +|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -326,10 +326,10 @@ Status Code **200** #### Enumerated Values -| Property | Value(s) | -|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | +| Property | Value(s) | +|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -459,10 +459,10 @@ Status Code **200** #### Enumerated Values -| Property | Value(s) | -|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | +| Property | Value(s) | +|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -554,10 +554,10 @@ Status Code **200** #### Enumerated Values -| Property | Value(s) | -|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | +| Property | Value(s) | +|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -960,9 +960,9 @@ Status Code **200** #### Enumerated Values -| Property | Value(s) | -|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | +| Property | Value(s) | +|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 912d53f8cef..0f92f624c16 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -1495,9 +1495,9 @@ None #### Enumerated Values -| Value(s) | -|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `ai_model_price:*`, `ai_model_price:read`, `ai_model_price:update`, `ai_provider:*`, `ai_provider:create`, `ai_provider:delete`, `ai_provider:read`, `ai_provider:update`, `ai_seat:*`, `ai_seat:create`, `ai_seat:read`, `aibridge_interception:*`, `aibridge_interception:create`, `aibridge_interception:read`, `aibridge_interception:update`, `all`, `api_key:*`, `api_key:create`, `api_key:delete`, `api_key:read`, `api_key:update`, `application_connect`, `assign_org_role:*`, `assign_org_role:assign`, `assign_org_role:create`, `assign_org_role:delete`, `assign_org_role:read`, `assign_org_role:unassign`, `assign_org_role:update`, `assign_role:*`, `assign_role:assign`, `assign_role:read`, `assign_role:unassign`, `audit_log:*`, `audit_log:create`, `audit_log:read`, `boundary_usage:*`, `boundary_usage:delete`, `boundary_usage:read`, `boundary_usage:update`, `chat:*`, `chat:create`, `chat:delete`, `chat:read`, `chat:share`, `chat:update`, `coder:all`, `coder:apikeys.manage_self`, `coder:application_connect`, `coder:templates.author`, `coder:templates.build`, `coder:workspaces.access`, `coder:workspaces.create`, `coder:workspaces.delete`, `coder:workspaces.operate`, `connection_log:*`, `connection_log:read`, `connection_log:update`, `crypto_key:*`, `crypto_key:create`, `crypto_key:delete`, `crypto_key:read`, `crypto_key:update`, `debug_info:*`, `debug_info:read`, `deployment_config:*`, `deployment_config:read`, `deployment_config:update`, `deployment_stats:*`, `deployment_stats:read`, `file:*`, `file:create`, `file:read`, `group:*`, `group:create`, `group:delete`, `group:read`, `group:update`, `group_member:*`, `group_member:read`, `idpsync_settings:*`, `idpsync_settings:read`, `idpsync_settings:update`, `inbox_notification:*`, `inbox_notification:create`, `inbox_notification:read`, `inbox_notification:update`, `license:*`, `license:create`, `license:delete`, `license:read`, `notification_message:*`, `notification_message:create`, `notification_message:delete`, `notification_message:read`, `notification_message:update`, `notification_preference:*`, `notification_preference:read`, `notification_preference:update`, `notification_template:*`, `notification_template:read`, `notification_template:update`, `oauth2_app:*`, `oauth2_app:create`, `oauth2_app:delete`, `oauth2_app:read`, `oauth2_app:update`, `oauth2_app_code_token:*`, `oauth2_app_code_token:create`, `oauth2_app_code_token:delete`, `oauth2_app_code_token:read`, `oauth2_app_secret:*`, `oauth2_app_secret:create`, `oauth2_app_secret:delete`, `oauth2_app_secret:read`, `oauth2_app_secret:update`, `organization:*`, `organization:create`, `organization:delete`, `organization:read`, `organization:update`, `organization_member:*`, `organization_member:create`, `organization_member:delete`, `organization_member:read`, `organization_member:update`, `prebuilt_workspace:*`, `prebuilt_workspace:delete`, `prebuilt_workspace:update`, `provisioner_daemon:*`, `provisioner_daemon:create`, `provisioner_daemon:delete`, `provisioner_daemon:read`, `provisioner_daemon:update`, `provisioner_jobs:*`, `provisioner_jobs:create`, `provisioner_jobs:read`, `provisioner_jobs:update`, `replicas:*`, `replicas:read`, `system:*`, `system:create`, `system:delete`, `system:read`, `system:update`, `tailnet_coordinator:*`, `tailnet_coordinator:create`, `tailnet_coordinator:delete`, `tailnet_coordinator:read`, `tailnet_coordinator:update`, `task:*`, `task:create`, `task:delete`, `task:read`, `task:update`, `template:*`, `template:create`, `template:delete`, `template:read`, `template:update`, `template:use`, `template:view_insights`, `usage_event:*`, `usage_event:create`, `usage_event:read`, `usage_event:update`, `user:*`, `user:create`, `user:delete`, `user:read`, `user:read_personal`, `user:update`, `user:update_personal`, `user_secret:*`, `user_secret:create`, `user_secret:delete`, `user_secret:read`, `user_secret:update`, `user_skill:*`, `user_skill:create`, `user_skill:delete`, `user_skill:read`, `user_skill:update`, `webpush_subscription:*`, `webpush_subscription:create`, `webpush_subscription:delete`, `webpush_subscription:read`, `workspace:*`, `workspace:application_connect`, `workspace:create`, `workspace:create_agent`, `workspace:delete`, `workspace:delete_agent`, `workspace:read`, `workspace:share`, `workspace:ssh`, `workspace:start`, `workspace:stop`, `workspace:update`, `workspace:update_agent`, `workspace_agent_devcontainers:*`, `workspace_agent_devcontainers:create`, `workspace_agent_resource_monitor:*`, `workspace_agent_resource_monitor:create`, `workspace_agent_resource_monitor:read`, `workspace_agent_resource_monitor:update`, `workspace_dormant:*`, `workspace_dormant:application_connect`, `workspace_dormant:create`, `workspace_dormant:create_agent`, `workspace_dormant:delete`, `workspace_dormant:delete_agent`, `workspace_dormant:read`, `workspace_dormant:share`, `workspace_dormant:ssh`, `workspace_dormant:start`, `workspace_dormant:stop`, `workspace_dormant:update`, `workspace_dormant:update_agent`, `workspace_proxy:*`, `workspace_proxy:create`, `workspace_proxy:delete`, `workspace_proxy:read`, `workspace_proxy:update` | +| Value(s) | +|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ai_model_price:*`, `ai_model_price:read`, `ai_model_price:update`, `ai_provider:*`, `ai_provider:create`, `ai_provider:delete`, `ai_provider:read`, `ai_provider:update`, `ai_seat:*`, `ai_seat:create`, `ai_seat:read`, `aibridge_interception:*`, `aibridge_interception:create`, `aibridge_interception:read`, `aibridge_interception:update`, `all`, `api_key:*`, `api_key:create`, `api_key:delete`, `api_key:read`, `api_key:update`, `application_connect`, `assign_org_role:*`, `assign_org_role:assign`, `assign_org_role:create`, `assign_org_role:delete`, `assign_org_role:read`, `assign_org_role:unassign`, `assign_org_role:update`, `assign_role:*`, `assign_role:assign`, `assign_role:read`, `assign_role:unassign`, `audit_log:*`, `audit_log:create`, `audit_log:read`, `boundary_usage:*`, `boundary_usage:delete`, `boundary_usage:read`, `boundary_usage:update`, `chat:*`, `chat:create`, `chat:delete`, `chat:read`, `chat:share`, `chat:update`, `coder:all`, `coder:apikeys.manage_self`, `coder:application_connect`, `coder:templates.author`, `coder:templates.build`, `coder:workspaces.access`, `coder:workspaces.create`, `coder:workspaces.delete`, `coder:workspaces.operate`, `connection_log:*`, `connection_log:read`, `connection_log:update`, `crypto_key:*`, `crypto_key:create`, `crypto_key:delete`, `crypto_key:read`, `crypto_key:update`, `debug_info:*`, `debug_info:read`, `deployment_config:*`, `deployment_config:read`, `deployment_config:update`, `deployment_stats:*`, `deployment_stats:read`, `file:*`, `file:create`, `file:read`, `group:*`, `group:create`, `group:delete`, `group:read`, `group:update`, `group_member:*`, `group_member:read`, `idpsync_settings:*`, `idpsync_settings:read`, `idpsync_settings:update`, `inbox_notification:*`, `inbox_notification:create`, `inbox_notification:read`, `inbox_notification:update`, `license:*`, `license:create`, `license:delete`, `license:read`, `notification_message:*`, `notification_message:create`, `notification_message:delete`, `notification_message:read`, `notification_message:update`, `notification_preference:*`, `notification_preference:read`, `notification_preference:update`, `notification_template:*`, `notification_template:read`, `notification_template:update`, `oauth2_app:*`, `oauth2_app:create`, `oauth2_app:delete`, `oauth2_app:read`, `oauth2_app:update`, `oauth2_app_code_token:*`, `oauth2_app_code_token:create`, `oauth2_app_code_token:delete`, `oauth2_app_code_token:read`, `oauth2_app_secret:*`, `oauth2_app_secret:create`, `oauth2_app_secret:delete`, `oauth2_app_secret:read`, `oauth2_app_secret:update`, `organization:*`, `organization:create`, `organization:delete`, `organization:read`, `organization:update`, `organization_member:*`, `organization_member:create`, `organization_member:delete`, `organization_member:read`, `organization_member:update`, `prebuilt_workspace:*`, `prebuilt_workspace:delete`, `prebuilt_workspace:update`, `provisioner_daemon:*`, `provisioner_daemon:create`, `provisioner_daemon:delete`, `provisioner_daemon:read`, `provisioner_daemon:update`, `provisioner_jobs:*`, `provisioner_jobs:create`, `provisioner_jobs:read`, `provisioner_jobs:update`, `replicas:*`, `replicas:read`, `system:*`, `system:create`, `system:delete`, `system:read`, `system:update`, `tailnet_coordinator:*`, `tailnet_coordinator:create`, `tailnet_coordinator:delete`, `tailnet_coordinator:read`, `tailnet_coordinator:update`, `task:*`, `task:create`, `task:delete`, `task:read`, `task:update`, `template:*`, `template:create`, `template:delete`, `template:read`, `template:update`, `template:use`, `template:view_insights`, `usage_event:*`, `usage_event:create`, `usage_event:read`, `usage_event:update`, `user:*`, `user:create`, `user:delete`, `user:read`, `user:read_personal`, `user:update`, `user:update_personal`, `user_secret:*`, `user_secret:create`, `user_secret:delete`, `user_secret:read`, `user_secret:update`, `user_skill:*`, `user_skill:create`, `user_skill:delete`, `user_skill:read`, `user_skill:update`, `webpush_subscription:*`, `webpush_subscription:create`, `webpush_subscription:delete`, `webpush_subscription:read`, `workspace:*`, `workspace:application_connect`, `workspace:create`, `workspace:create_agent`, `workspace:delete`, `workspace:delete_agent`, `workspace:read`, `workspace:share`, `workspace:ssh`, `workspace:start`, `workspace:stop`, `workspace:update`, `workspace:update_agent`, `workspace_agent_devcontainers:*`, `workspace_agent_devcontainers:create`, `workspace_agent_resource_monitor:*`, `workspace_agent_resource_monitor:create`, `workspace_agent_resource_monitor:read`, `workspace_agent_resource_monitor:update`, `workspace_build_orchestration:*`, `workspace_build_orchestration:create`, `workspace_build_orchestration:delete`, `workspace_build_orchestration:read`, `workspace_build_orchestration:update`, `workspace_dormant:*`, `workspace_dormant:application_connect`, `workspace_dormant:create`, `workspace_dormant:create_agent`, `workspace_dormant:delete`, `workspace_dormant:delete_agent`, `workspace_dormant:read`, `workspace_dormant:share`, `workspace_dormant:ssh`, `workspace_dormant:start`, `workspace_dormant:stop`, `workspace_dormant:update`, `workspace_dormant:update_agent`, `workspace_proxy:*`, `workspace_proxy:create`, `workspace_proxy:delete`, `workspace_proxy:read`, `workspace_proxy:update` | ## codersdk.AddLicenseRequest @@ -10866,9 +10866,9 @@ Only certain features set these fields: - FeatureManagedAgentLimit| #### Enumerated Values -| Value(s) | -|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | +| Value(s) | +|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | ## codersdk.RateLimitConfig diff --git a/docs/reference/api/users.md b/docs/reference/api/users.md index 376a415031e..7a55206094c 100644 --- a/docs/reference/api/users.md +++ b/docs/reference/api/users.md @@ -865,11 +865,11 @@ Status Code **200** #### Enumerated Values -| Property | Value(s) | -|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | -| `login_type` | `github`, `oidc`, `password`, `token` | -| `scope` | `all`, `application_connect` | +| Property | Value(s) | +|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | +| `login_type` | `github`, `oidc`, `password`, `token` | +| `scope` | `all`, `application_connect` | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/site/src/api/rbacresourcesGenerated.ts b/site/src/api/rbacresourcesGenerated.ts index 23bd95350cf..939a13829ba 100644 --- a/site/src/api/rbacresourcesGenerated.ts +++ b/site/src/api/rbacresourcesGenerated.ts @@ -248,6 +248,12 @@ export const RBACResourceActions: Partial< read: "read workspace agent resource monitor", update: "update workspace agent resource monitor", }, + workspace_build_orchestration: { + create: "create a workspace build orchestration", + delete: "delete a workspace build orchestration", + read: "read a workspace build orchestration", + update: "update a workspace build orchestration", + }, workspace_dormant: { application_connect: "connect to workspace apps via browser", create: "create a new workspace", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 2c0df043ff4..3813d3e2427 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -713,6 +713,11 @@ export type APIKeyScope = | "workspace_agent_resource_monitor:update" | "workspace:*" | "workspace:application_connect" + | "workspace_build_orchestration:*" + | "workspace_build_orchestration:create" + | "workspace_build_orchestration:delete" + | "workspace_build_orchestration:read" + | "workspace_build_orchestration:update" | "workspace:create" | "workspace:create_agent" | "workspace:delete" @@ -939,6 +944,11 @@ export const APIKeyScopes: APIKeyScope[] = [ "workspace_agent_resource_monitor:update", "workspace:*", "workspace:application_connect", + "workspace_build_orchestration:*", + "workspace_build_orchestration:create", + "workspace_build_orchestration:delete", + "workspace_build_orchestration:read", + "workspace_build_orchestration:update", "workspace:create", "workspace:create_agent", "workspace:delete", @@ -6905,6 +6915,7 @@ export type RBACResource = | "workspace" | "workspace_agent_devcontainers" | "workspace_agent_resource_monitor" + | "workspace_build_orchestration" | "workspace_dormant" | "workspace_proxy"; @@ -6955,6 +6966,7 @@ export const RBACResources: RBACResource[] = [ "workspace", "workspace_agent_devcontainers", "workspace_agent_resource_monitor", + "workspace_build_orchestration", "workspace_dormant", "workspace_proxy", ]; From b3fc22f27ef68081791a3de311b64c409e23bc28 Mon Sep 17 00:00:00 2001 From: George Katsitadze Date: Tue, 16 Jun 2026 14:39:51 -0700 Subject: [PATCH 08/13] add a racy test of pending gate under concurrent contention --- coderd/database/querier_test.go | 129 ++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index fd7e0138815..4337a9cac04 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -8524,6 +8524,135 @@ func TestUpdateWorkspaceBuildOrchestrationRetryByIDMaxAttempts(t *testing.T) { 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() From 6f49a4d4e35028dee5f821a4f29fb990f80e1328 Mon Sep 17 00:00:00 2001 From: George Katsitadze Date: Wed, 17 Jun 2026 13:03:50 -0700 Subject: [PATCH 09/13] fix ci: add orchestration api key scopes to db and non-duplicate test subjects --- coderd/database/dump.sql | 7 ++++++- ...0526_workspace_build_orchestrations.down.sql | 3 +++ ...000526_workspace_build_orchestrations.up.sql | 7 +++++++ coderd/database/models.go | 17 ++++++++++++++++- coderd/rbac/roles_test.go | 2 +- 5 files changed, 33 insertions(+), 3 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 9acb2ef6089..681aadaf3bf 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -257,7 +257,12 @@ CREATE TYPE api_key_scope AS ENUM ( 'ai_gateway_key:*', 'ai_gateway_key:create', 'ai_gateway_key:delete', - 'ai_gateway_key:read' + 'ai_gateway_key:read', + '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 ( diff --git a/coderd/database/migrations/000526_workspace_build_orchestrations.down.sql b/coderd/database/migrations/000526_workspace_build_orchestrations.down.sql index b84b4fae00d..f2b1d1dc133 100644 --- a/coderd/database/migrations/000526_workspace_build_orchestrations.down.sql +++ b/coderd/database/migrations/000526_workspace_build_orchestrations.down.sql @@ -1,3 +1,6 @@ +-- 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 diff --git a/coderd/database/migrations/000526_workspace_build_orchestrations.up.sql b/coderd/database/migrations/000526_workspace_build_orchestrations.up.sql index 40aa64ba94f..1b87be552d6 100644 --- a/coderd/database/migrations/000526_workspace_build_orchestrations.up.sql +++ b/coderd/database/migrations/000526_workspace_build_orchestrations.up.sql @@ -94,3 +94,10 @@ COMMENT ON COLUMN workspace_build_orchestrations.attempt_count IS 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'; diff --git a/coderd/database/models.go b/coderd/database/models.go index 9b53a09643a..1d23b651a77 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -386,6 +386,11 @@ const ( ApiKeyScopeAIGatewayKeyCreate APIKeyScope = "ai_gateway_key:create" ApiKeyScopeAIGatewayKeyDelete APIKeyScope = "ai_gateway_key:delete" ApiKeyScopeAIGatewayKeyRead APIKeyScope = "ai_gateway_key:read" + 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 { @@ -654,7 +659,12 @@ func (e APIKeyScope) Valid() bool { ApiKeyScopeAIGatewayKey, ApiKeyScopeAIGatewayKeyCreate, ApiKeyScopeAIGatewayKeyDelete, - ApiKeyScopeAIGatewayKeyRead: + ApiKeyScopeAIGatewayKeyRead, + ApiKeyScopeWorkspaceBuildOrchestration, + ApiKeyScopeWorkspaceBuildOrchestrationCreate, + ApiKeyScopeWorkspaceBuildOrchestrationDelete, + ApiKeyScopeWorkspaceBuildOrchestrationRead, + ApiKeyScopeWorkspaceBuildOrchestrationUpdate: return true } return false @@ -892,6 +902,11 @@ func AllAPIKeyScopeValues() []APIKeyScope { ApiKeyScopeAIGatewayKeyCreate, ApiKeyScopeAIGatewayKeyDelete, ApiKeyScopeAIGatewayKeyRead, + ApiKeyScopeWorkspaceBuildOrchestration, + ApiKeyScopeWorkspaceBuildOrchestrationCreate, + ApiKeyScopeWorkspaceBuildOrchestrationDelete, + ApiKeyScopeWorkspaceBuildOrchestrationRead, + ApiKeyScopeWorkspaceBuildOrchestrationUpdate, } } diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index 320dfa386c5..e95f0e4f940 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -993,7 +993,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspaceBuildOrchestration.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, orgAdmin, templateAdmin, orgTemplateAdmin, userAdmin, orgUserAdmin, orgAuditor}, + false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { From 6b6fcd3faf7bdca09cbd002a510c4cf1061c7638 Mon Sep 17 00:00:00 2001 From: George K Date: Mon, 6 Jul 2026 13:48:14 -0700 Subject: [PATCH 10/13] feat: add on_success workspace build request handling (#25758) --- coderd/apidoc/docs.go | 44 + coderd/apidoc/swagger.json | 40 + coderd/coderd.go | 18 +- coderd/database/dbauthz/dbauthz.go | 16 +- coderd/database/dbauthz/dbauthz_test.go | 8 + coderd/database/dbmetrics/querymetrics.go | 8 + coderd/database/dbmock/dbmock.go | 15 + coderd/database/dbpurge/dbpurge.go | 16 + coderd/database/dbpurge/dbpurge_test.go | 147 +++ .../workspacebuildorchestrations.go | 32 + coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 31 + .../queries/workspacebuildorchestrations.sql | 17 + coderd/pproflabel/pproflabel.go | 3 + .../provisionerdserver/provisionerdserver.go | 16 + coderd/workspacebuilds.go | 127 ++- coderd/workspacebuilds_on_success_test.go | 840 ++++++++++++++++++ coderd/workspaces.go | 15 +- coderd/wsbuildorchestrator/doc.go | 4 + .../wsbuildorchestrator.go | 567 ++++++++++++ .../wsbuildorchestrator_internal_test.go | 427 +++++++++ coderd/wspubsub/wspubsub.go | 30 + codersdk/workspaces.go | 28 + docs/reference/api/builds.md | 11 + docs/reference/api/schemas.md | 65 +- site/src/api/typesGenerated.ts | 41 + 26 files changed, 2536 insertions(+), 31 deletions(-) create mode 100644 coderd/database/dbtestutil/workspacebuildorchestrations.go create mode 100644 coderd/workspacebuilds_on_success_test.go create mode 100644 coderd/wsbuildorchestrator/doc.go create mode 100644 coderd/wsbuildorchestrator/wsbuildorchestrator.go create mode 100644 coderd/wsbuildorchestrator/wsbuildorchestrator_internal_test.go diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index a727d71a5de..534f808cb83 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -18449,6 +18449,42 @@ const docTemplate = `{ } } }, + "codersdk.CreateWorkspaceBuildOnSuccessRequest": { + "type": "object", + "required": [ + "transition" + ], + "properties": { + "rich_parameter_values": { + "description": "RichParameterValues are applied to the child build. Parameters\nnot listed here fall back to their values from the previous\nbuild, matching normal build behavior.", + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceBuildParameter" + } + }, + "template_version_id": { + "description": "TemplateVersionID pins the child build to a specific template\nversion. Pinning requires permission to update the template,\nsince the active version may change before the child build\nruns. When empty, the child build uses the template's active\nversion at the time it runs.", + "type": "string", + "format": "uuid" + }, + "template_version_preset_id": { + "description": "TemplateVersionPresetID selects a preset for the child build.\nIt requires TemplateVersionID to also be set.", + "type": "string", + "format": "uuid" + }, + "transition": { + "description": "Transition must be \"start\". The parent build's transition must\nbe \"stop\".", + "enum": [ + "start" + ], + "allOf": [ + { + "$ref": "#/definitions/codersdk.WorkspaceTransition" + } + ] + } + } + }, "codersdk.CreateWorkspaceBuildReason": { "type": "string", "enum": [ @@ -18490,6 +18526,14 @@ const docTemplate = `{ } ] }, + "on_success": { + "description": "OnSuccess queues a follow-up workspace build after this build succeeds.\nIt currently supports restarting a workspace by starting it after a\nsuccessful stop build.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.CreateWorkspaceBuildOnSuccessRequest" + } + ] + }, "orphan": { "description": "Orphan may be set for the Destroy transition.", "type": "boolean" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 20bbcae3227..603029585f6 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -16682,6 +16682,38 @@ } } }, + "codersdk.CreateWorkspaceBuildOnSuccessRequest": { + "type": "object", + "required": ["transition"], + "properties": { + "rich_parameter_values": { + "description": "RichParameterValues are applied to the child build. Parameters\nnot listed here fall back to their values from the previous\nbuild, matching normal build behavior.", + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceBuildParameter" + } + }, + "template_version_id": { + "description": "TemplateVersionID pins the child build to a specific template\nversion. Pinning requires permission to update the template,\nsince the active version may change before the child build\nruns. When empty, the child build uses the template's active\nversion at the time it runs.", + "type": "string", + "format": "uuid" + }, + "template_version_preset_id": { + "description": "TemplateVersionPresetID selects a preset for the child build.\nIt requires TemplateVersionID to also be set.", + "type": "string", + "format": "uuid" + }, + "transition": { + "description": "Transition must be \"start\". The parent build's transition must\nbe \"stop\".", + "enum": ["start"], + "allOf": [ + { + "$ref": "#/definitions/codersdk.WorkspaceTransition" + } + ] + } + } + }, "codersdk.CreateWorkspaceBuildReason": { "type": "string", "enum": [ @@ -16719,6 +16751,14 @@ } ] }, + "on_success": { + "description": "OnSuccess queues a follow-up workspace build after this build succeeds.\nIt currently supports restarting a workspace by starting it after a\nsuccessful stop build.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.CreateWorkspaceBuildOnSuccessRequest" + } + ] + }, "orphan": { "description": "Orphan may be set for the Destroy transition.", "type": "boolean" diff --git a/coderd/coderd.go b/coderd/coderd.go index 601669d3210..cba3fc00342 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -96,6 +96,7 @@ import ( "github.com/coder/coder/v2/coderd/workspaceconnwatcher" "github.com/coder/coder/v2/coderd/workspacestats" "github.com/coder/coder/v2/coderd/wsbuilder" + "github.com/coder/coder/v2/coderd/wsbuildorchestrator" "github.com/coder/coder/v2/coderd/x/chatd" "github.com/coder/coder/v2/coderd/x/chatd/chatprovider" "github.com/coder/coder/v2/coderd/x/chatd/mcpclient" @@ -950,6 +951,19 @@ func New(options *Options) *API { api.workspaceAgentConnWatcher = workspaceconnwatcher.New(api.ctx, options.Logger, options.Pubsub, options.Database) + api.workspaceBuildOrchestrator = wsbuildorchestrator.New(wsbuildorchestrator.Options{ + Logger: options.Logger, + Database: options.Database, + Pubsub: options.Pubsub, + FileCache: api.FileCache, + BuildUsageChecker: api.BuildUsageChecker, + DeploymentValues: options.DeploymentValues, + Experiments: api.Experiments, + BuilderMetrics: options.WorkspaceBuilderMetrics, + Clock: api.Clock, + }) + api.workspaceBuildOrchestrator.Start(api.ctx) + apiKeyMiddleware := httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{ DB: options.Database, ActivateDormantUser: ActivateDormantUser(options.Logger, &api.Auditor, options.Database), @@ -2293,7 +2307,8 @@ type API struct { // profiler is process-global, so concurrent collections would fail. ProfileCollecting atomic.Bool - workspaceAgentConnWatcher *workspaceconnwatcher.Watcher + workspaceAgentConnWatcher *workspaceconnwatcher.Watcher + workspaceBuildOrchestrator *wsbuildorchestrator.Orchestrator } // Close waits for all WebSocket connections to drain before returning. @@ -2358,6 +2373,7 @@ func (api *API) Close() error { _ = api.AppEncryptionKeyCache.Close() _ = api.UpdatesProvider.Close() api.workspaceAgentConnWatcher.Close() + api.workspaceBuildOrchestrator.Close() if current := api.PrebuildsReconciler.Load(); current != nil { ctx, giveUp := context.WithTimeoutCause(context.Background(), time.Second*30, xerrors.New("gave up waiting for reconciler to stop before shutdown")) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 46c5e41addc..bd5ae625852 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -671,10 +671,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. @@ -2389,6 +2390,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{ diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 8f76c9bc2fd..a71ab78532f 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4156,6 +4156,14 @@ func (s *MethodTestSuite) TestWorkspace() { 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{ diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 3b294898f9b..89f1d82c2b6 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -818,6 +818,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) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 6ca5ce63839..47afc753d37 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -1381,6 +1381,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() diff --git a/coderd/database/dbpurge/dbpurge.go b/coderd/database/dbpurge/dbpurge.go index b50bfe3ae8b..51e034417e0 100644 --- a/coderd/database/dbpurge/dbpurge.go +++ b/coderd/database/dbpurge/dbpurge.go @@ -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)) diff --git a/coderd/database/dbpurge/dbpurge_test.go b/coderd/database/dbpurge/dbpurge_test.go index 18d7866cac5..6efe954360a 100644 --- a/coderd/database/dbpurge/dbpurge_test.go +++ b/coderd/database/dbpurge/dbpurge_test.go @@ -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() diff --git a/coderd/database/dbtestutil/workspacebuildorchestrations.go b/coderd/database/dbtestutil/workspacebuildorchestrations.go new file mode 100644 index 00000000000..4dce0a92799 --- /dev/null +++ b/coderd/database/dbtestutil/workspacebuildorchestrations.go @@ -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 +} diff --git a/coderd/database/querier.go b/coderd/database/querier.go index d5259b3820c..3f48a4697a9 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -222,6 +222,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 diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index b83febb163e..bbf8ed756c0 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -35299,6 +35299,37 @@ 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 diff --git a/coderd/database/queries/workspacebuildorchestrations.sql b/coderd/database/queries/workspacebuildorchestrations.sql index f256d89a7ad..fe8279a20e5 100644 --- a/coderd/database/queries/workspacebuildorchestrations.sql +++ b/coderd/database/queries/workspacebuildorchestrations.sql @@ -114,3 +114,20 @@ 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; diff --git a/coderd/pproflabel/pproflabel.go b/coderd/pproflabel/pproflabel.go index f686c1c4288..5204a0681df 100644 --- a/coderd/pproflabel/pproflabel.go +++ b/coderd/pproflabel/pproflabel.go @@ -35,6 +35,9 @@ const ( // ServiceTallymanPublisher publishes usage events to coder/tallyman. ServiceTallymanPublisher = "tallyman-publisher" ServiceUsageEventCron = "usage-event-cron" + // ServiceWorkspaceBuildOrchestrator fulfills workspace build + // orchestrations once their parent build reaches a terminal state. + ServiceWorkspaceBuildOrchestrator = "workspace-build-orchestrator" RequestTypeTag = "coder_request_type" ) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index d233cb41dd9..cb65747ed76 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1285,6 +1285,13 @@ func (s *server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*proto. s.notifyWorkspaceBuildFailed(ctx, workspace, build) + // Wake the orchestrator before the workspace event publish + // below, which returns on error, so a failed UI event cannot + // skip the wake. + if err := wspubsub.PublishWorkspaceBuildOrchestrationWake(ctx, s.Pubsub); err != nil { + s.Logger.Warn(ctx, "failed to publish workspace build orchestration wake", slog.Error(err)) + } + msg, err := json.Marshal(wspubsub.WorkspaceEvent{ Kind: wspubsub.WorkspaceEventKindStateChange, WorkspaceID: workspace.ID, @@ -2532,6 +2539,15 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro } } + // Wake the orchestrator before the workspace event publish below, + // which returns on error, so a failed UI event cannot skip the + // wake. + if err := wspubsub.PublishWorkspaceBuildOrchestrationWake(ctx, s.Pubsub); err != nil { + s.Logger.Warn(ctx, "failed to publish workspace build orchestration wake", + slog.Error(err), + ) + } + msg, err := json.Marshal(wspubsub.WorkspaceEvent{ Kind: wspubsub.WorkspaceEventKindStateChange, WorkspaceID: workspace.ID, diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 8ccb6417b8e..9678defee55 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -375,6 +375,30 @@ func (api *API) postWorkspaceBuildsInternal( codersdk.WorkspaceBuild, error, ) { + if err := validateCreateWorkspaceBuildOnSuccess(createBuild); err != nil { + return codersdk.WorkspaceBuild{}, err + } + + var childParameterValuesJSON json.RawMessage + if createBuild.OnSuccess != nil { + childParameterValues := createBuild.OnSuccess.RichParameterValues + if childParameterValues == nil { + childParameterValues = []codersdk.WorkspaceBuildParameter{} + } + + var err error + childParameterValuesJSON, err = json.Marshal(childParameterValues) + if err != nil { + return codersdk.WorkspaceBuild{}, httperror.NewResponseError( + http.StatusInternalServerError, + codersdk.Response{ + Message: "Internal error preparing follow-up workspace build parameters", + Detail: err.Error(), + }, + ) + } + } + transition := database.WorkspaceTransition(createBuild.Transition) builder := wsbuilder.New(workspace, transition, *api.BuildUsageChecker.Load()). Initiator(apiKey.UserID). @@ -481,7 +505,62 @@ func (api *API) postWorkspaceBuildsInternal( }, workspaceBuildBaggage, ) - return err + if err != nil { + return err + } + + if createBuild.OnSuccess != nil { + onSuccessReq := createBuild.OnSuccess + // Reuse the parent build's timestamps so the orchestration row and + // the parent build agree on created_at. + _, err = tx.InsertWorkspaceBuildOrchestration(ctx, database.InsertWorkspaceBuildOrchestrationParams{ + ID: uuid.New(), + CreatedAt: workspaceBuild.CreatedAt, + UpdatedAt: workspaceBuild.UpdatedAt, + ParentBuildID: workspaceBuild.ID, + ChildTransition: database.WorkspaceTransition(onSuccessReq.Transition), + ChildTemplateVersionID: uuid.NullUUID{ + UUID: onSuccessReq.TemplateVersionID, + Valid: onSuccessReq.TemplateVersionID != uuid.Nil, + }, + ChildTemplateVersionPresetID: uuid.NullUUID{ + UUID: onSuccessReq.TemplateVersionPresetID, + Valid: onSuccessReq.TemplateVersionPresetID != uuid.Nil, + }, + ChildRichParameterValues: childParameterValuesJSON, + ChildLogLevel: string(createBuild.LogLevel), + ChildReason: database.NullBuildReason{ + BuildReason: database.BuildReason(createBuild.Reason), + Valid: createBuild.Reason != "", + }, + }) + if err != nil { + if dbauthz.IsNotAuthorizedError(err) { + detail := "Queuing the follow-up workspace build requires permission to start the workspace." + if onSuccessReq.TemplateVersionID != uuid.Nil { + detail = "Pinning a template version on the follow-up build requires template update permission. Omit template_version_id to use the active version." + } + return httperror.NewResponseError(http.StatusForbidden, codersdk.Response{ + Message: "Unauthorized to queue follow-up workspace build.", + Detail: detail, + }) + } + api.Logger.Error(ctx, "failed to queue follow-up workspace build", + slog.F("workspace_id", workspace.ID), + slog.F("workspace_build_id", workspaceBuild.ID), + slog.Error(err), + ) + return httperror.NewResponseError( + http.StatusInternalServerError, + codersdk.Response{ + Message: "Internal error queueing follow-up workspace build", + Detail: err.Error(), + }, + ) + } + } + + return nil }, nil) if err != nil { return codersdk.WorkspaceBuild{}, err @@ -584,6 +663,48 @@ func (api *API) postWorkspaceBuildsInternal( return apiBuild, nil } +// validateCreateWorkspaceBuildOnSuccess enforces the subset of build options +// that currently has well-defined stop-then-start semantics. +func validateCreateWorkspaceBuildOnSuccess(createBuild codersdk.CreateWorkspaceBuildRequest) error { + onSuccess := createBuild.OnSuccess + if onSuccess == nil { + return nil + } + + if createBuild.Transition != codersdk.WorkspaceTransitionStop { + return httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{ + Message: "OnSuccess is only permitted when stopping a workspace.", + }) + } + if onSuccess.Transition != codersdk.WorkspaceTransitionStart { + return httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{ + Message: "OnSuccess transition must be start.", + }) + } + if createBuild.DryRun { + return httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{ + Message: "OnSuccess cannot be set alongside DryRun.", + }) + } + if createBuild.Orphan { + return httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{ + Message: "OnSuccess cannot be set alongside Orphan.", + }) + } + if len(createBuild.ProvisionerState) > 0 { + return httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{ + Message: "OnSuccess cannot be set alongside ProvisionerState.", + }) + } + if onSuccess.TemplateVersionPresetID != uuid.Nil && onSuccess.TemplateVersionID == uuid.Nil { + return httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{ + Message: "OnSuccess TemplateVersionPresetID requires TemplateVersionID.", + }) + } + + return nil +} + func (api *API) notifyWorkspaceUpdated( ctx context.Context, initiatorID uuid.UUID, @@ -765,6 +886,10 @@ func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Reques Kind: wspubsub.WorkspaceEventKindStateChange, WorkspaceID: workspace.ID, }) + err = wspubsub.PublishWorkspaceBuildOrchestrationWake(ctx, api.Pubsub) + if err != nil { + api.Logger.Warn(ctx, "failed to publish workspace build orchestration wake", slog.Error(err)) + } // Publish workspace build update to the all builds channel if the experiment is enabled. if api.Experiments.Enabled(codersdk.ExperimentWorkspaceBuildUpdates) { diff --git a/coderd/workspacebuilds_on_success_test.go b/coderd/workspacebuilds_on_success_test.go new file mode 100644 index 00000000000..a49ef56147e --- /dev/null +++ b/coderd/workspacebuilds_on_success_test.go @@ -0,0 +1,840 @@ +package coderd_test + +import ( + "encoding/json" + "net/http" + "strconv" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/provisioner/echo" + "github.com/coder/coder/v2/provisionersdk/proto" + "github.com/coder/coder/v2/testutil" +) + +func TestPostWorkspaceBuildsOnSuccessRestart(t *testing.T) { + t.Parallel() + + const paramName = "foo" + + // GIVEN: a running workspace with an existing rich parameter value. + deploymentValues := coderdtest.DeploymentValues(t) + deploymentValues.EnableTerraformDebugMode = true + db, ps, sqlDB := dbtestutil.NewDBWithSQLDB(t) + client, _, _ := coderdtest.NewWithAPI(t, &coderdtest.Options{ + Database: db, + Pubsub: ps, + IncludeProvisionerDaemon: true, + DeploymentValues: deploymentValues, + }) + first := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, + echoResponsesWithRichParameter(paramName, echoResponseOptions{ + blockStopApply: false, + }), + ) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, template.ID, func(request *codersdk.CreateWorkspaceRequest) { + request.RichParameterValues = []codersdk.WorkspaceBuildParameter{ + {Name: paramName, Value: "bar"}, + } + }) + initialBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + require.Equal(t, codersdk.WorkspaceStatusRunning, initialBuild.Status) + + // WHEN: a stop build is created with an on_success start build. + ctx := testutil.Context(t, testutil.WaitLong) + user, err := client.User(ctx, codersdk.Me) + require.NoError(t, err) + + stopBuild, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStop, + Reason: codersdk.CreateWorkspaceBuildReasonCLI, + LogLevel: codersdk.ProvisionerLogLevelDebug, + OnSuccess: &codersdk.CreateWorkspaceBuildOnSuccessRequest{ + Transition: codersdk.WorkspaceTransitionStart, + TemplateVersionID: template.ActiveVersionID, + RichParameterValues: []codersdk.WorkspaceBuildParameter{ + {Name: paramName, Value: "baz"}, + }, + }, + }) + require.NoError(t, err) + require.Equal(t, codersdk.WorkspaceTransitionStop, stopBuild.Transition) + require.Equal(t, codersdk.BuildReasonCLI, stopBuild.Reason) + + // THEN: the server persists the child start build intent. + orchestration, err := dbtestutil.GetWorkspaceBuildOrchestrationByParentBuildID(ctx, sqlDB, stopBuild.ID) + require.NoError(t, err) + require.Equal(t, "pending", orchestration.Status) + require.Equal(t, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransition(orchestration.ChildTransition)) + require.True(t, orchestration.ChildTemplateVersionID.Valid) + require.Equal(t, template.ActiveVersionID, orchestration.ChildTemplateVersionID.UUID) + require.False(t, orchestration.ChildTemplateVersionPresetID.Valid) + require.Equal(t, string(codersdk.ProvisionerLogLevelDebug), orchestration.ChildLogLevel) + require.True(t, orchestration.ChildReason.Valid) + require.Equal(t, codersdk.BuildReasonCLI, codersdk.BuildReason(orchestration.ChildReason.BuildReason)) + + var childRichParameterValues []codersdk.WorkspaceBuildParameter + require.NoError(t, json.Unmarshal(orchestration.ChildRichParameterValues, &childRichParameterValues)) + require.ElementsMatch(t, []codersdk.WorkspaceBuildParameter{ + {Name: paramName, Value: "baz"}, + }, childRichParameterValues) + + // THEN: the returned parent stop build completes successfully. + stopBuild = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, stopBuild.ID) + require.Equal(t, codersdk.ProvisionerJobSucceeded, stopBuild.Job.Status) + require.Equal(t, codersdk.WorkspaceStatusStopped, stopBuild.Status) + + // THEN: the server creates and completes the child start build. + var childBuild codersdk.WorkspaceBuild + require.Eventually(t, func() bool { + childBuild, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber( + ctx, + user.Username, + workspace.Name, + strconv.FormatInt(int64(stopBuild.BuildNumber+1), 10), + ) + return err == nil && + childBuild.Transition == codersdk.WorkspaceTransitionStart + }, testutil.WaitMedium, testutil.IntervalFast) + + childBuild = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, childBuild.ID) + require.Equal(t, codersdk.ProvisionerJobSucceeded, childBuild.Job.Status) + require.Equal(t, codersdk.WorkspaceStatusRunning, childBuild.Status) + require.Equal(t, codersdk.BuildReasonCLI, childBuild.Reason) + require.Equal(t, template.ActiveVersionID, childBuild.TemplateVersionID) + + // THEN: the child build uses the on_success parameter values. + params, err := client.WorkspaceBuildParameters(ctx, childBuild.ID) + require.NoError(t, err) + require.ElementsMatch(t, []codersdk.WorkspaceBuildParameter{ + {Name: paramName, Value: "baz"}, + }, params) +} + +func TestPostWorkspaceBuildsOnSuccessTemplateVersionPreset(t *testing.T) { + t.Parallel() + + // GIVEN: a running workspace and a preset on its active template + // version. + db, ps, sqlDB := dbtestutil.NewDBWithSQLDB(t) + client, _, _ := coderdtest.NewWithAPI(t, &coderdtest.Options{ + Database: db, + Pubsub: ps, + IncludeProvisionerDaemon: true, + }) + first := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) + preset := dbgen.Preset(t, db, database.InsertPresetParams{ + Name: "on-success-preset", + TemplateVersionID: version.ID, + }) + workspace := coderdtest.CreateWorkspace(t, client, template.ID) + initialBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + require.Equal(t, codersdk.WorkspaceStatusRunning, initialBuild.Status) + + // WHEN: a stop build is created with an on_success start build + // that requests the preset. + ctx := testutil.Context(t, testutil.WaitLong) + user, err := client.User(ctx, codersdk.Me) + require.NoError(t, err) + + stopBuild, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStop, + OnSuccess: &codersdk.CreateWorkspaceBuildOnSuccessRequest{ + Transition: codersdk.WorkspaceTransitionStart, + TemplateVersionID: template.ActiveVersionID, + TemplateVersionPresetID: preset.ID, + }, + }) + require.NoError(t, err) + + // THEN: the server persists the child preset intent. + orchestration, err := dbtestutil.GetWorkspaceBuildOrchestrationByParentBuildID(ctx, sqlDB, stopBuild.ID) + require.NoError(t, err) + require.True(t, orchestration.ChildTemplateVersionPresetID.Valid) + require.Equal(t, preset.ID, orchestration.ChildTemplateVersionPresetID.UUID) + + stopBuild = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, stopBuild.ID) + require.Equal(t, codersdk.ProvisionerJobSucceeded, stopBuild.Job.Status) + require.Equal(t, codersdk.WorkspaceStatusStopped, stopBuild.Status) + + // THEN: the child start build uses the preset. + var childBuild codersdk.WorkspaceBuild + require.Eventually(t, func() bool { + childBuild, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber( + ctx, + user.Username, + workspace.Name, + strconv.FormatInt(int64(stopBuild.BuildNumber+1), 10), + ) + return err == nil && + childBuild.Transition == codersdk.WorkspaceTransitionStart + }, testutil.WaitShort, testutil.IntervalFast) + + childBuild = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, childBuild.ID) + require.Equal(t, codersdk.ProvisionerJobSucceeded, childBuild.Job.Status) + require.Equal(t, codersdk.WorkspaceStatusRunning, childBuild.Status) + require.NotNil(t, childBuild.TemplateVersionPresetID) + require.Equal(t, preset.ID, *childBuild.TemplateVersionPresetID) +} + +func TestPostWorkspaceBuildsOnSuccessUnpinnedChildUsesActiveTemplateVersion(t *testing.T) { + t.Parallel() + + // GIVEN: a running workspace and a second completed template + // version that is not active yet. + db, ps, sqlDB := dbtestutil.NewDBWithSQLDB(t) + client, provisionerCloser, api := coderdtest.NewWithAPI(t, &coderdtest.Options{ + Database: db, + Pubsub: ps, + IncludeProvisionerDaemon: true, + }) + first := coderdtest.CreateFirstUser(t, client) + userClient, user := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) + + version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, userClient, template.ID) + initialBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID) + require.Equal(t, codersdk.WorkspaceStatusRunning, initialBuild.Status) + + newVersion := coderdtest.UpdateTemplateVersion(t, client, first.OrganizationID, nil, template.ID) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, newVersion.ID) + + // WHEN: a non-template-admin queues an unpinned on_success child + // build. + + // Stop the provisioner so the parent build cannot complete before + // the test updates the active template version. + require.NoError(t, provisionerCloser.Close()) + ctx := testutil.Context(t, testutil.WaitLong) + stopBuild, err := userClient.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStop, + OnSuccess: &codersdk.CreateWorkspaceBuildOnSuccessRequest{ + Transition: codersdk.WorkspaceTransitionStart, + }, + }) + require.NoError(t, err) + + // THEN: the child build remains unpinned in the orchestration row. + orchestration, err := dbtestutil.GetWorkspaceBuildOrchestrationByParentBuildID(ctx, sqlDB, stopBuild.ID) + require.NoError(t, err) + require.False(t, orchestration.ChildTemplateVersionID.Valid, "child build should remain unpinned") + + // WHEN: the active version changes before the parent succeeds. + coderdtest.UpdateActiveTemplateVersion(t, client, template.ID, newVersion.ID) + coderdtest.NewProvisionerDaemon(t, api) + + stopBuild = coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, stopBuild.ID) + require.Equal(t, codersdk.ProvisionerJobSucceeded, stopBuild.Job.Status) + require.Equal(t, codersdk.WorkspaceStatusStopped, stopBuild.Status) + + // THEN: the child build uses the active version when the + // orchestrator creates it. + var childBuild codersdk.WorkspaceBuild + require.Eventually(t, func() bool { + childBuild, err = userClient.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber( + ctx, + user.Username, + workspace.Name, + strconv.FormatInt(int64(stopBuild.BuildNumber+1), 10), + ) + return err == nil && + childBuild.Transition == codersdk.WorkspaceTransitionStart + }, testutil.WaitMedium, testutil.IntervalFast) + + childBuild = coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, childBuild.ID) + require.Equal(t, codersdk.ProvisionerJobSucceeded, childBuild.Job.Status) + require.Equal(t, codersdk.WorkspaceStatusRunning, childBuild.Status) + require.Equal(t, newVersion.ID, childBuild.TemplateVersionID) +} + +func TestPostWorkspaceBuildsOnSuccessUnpinnedChildNoParams(t *testing.T) { + t.Parallel() + + // GIVEN: a running workspace owned by a non-template-admin. + db, ps, sqlDB := dbtestutil.NewDBWithSQLDB(t) + client, _, _ := coderdtest.NewWithAPI(t, &coderdtest.Options{ + Database: db, + Pubsub: ps, + IncludeProvisionerDaemon: true, + }) + first := coderdtest.CreateFirstUser(t, client) + userClient, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) + + version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, userClient, template.ID) + initialBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID) + require.Equal(t, codersdk.WorkspaceStatusRunning, initialBuild.Status) + + // WHEN: the non-template-admin queues a stop build with an unpinned + // on_success start build that supplies no parameters, reason, or log level. + ctx := testutil.Context(t, testutil.WaitLong) + stopBuild, err := userClient.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStop, + OnSuccess: &codersdk.CreateWorkspaceBuildOnSuccessRequest{ + Transition: codersdk.WorkspaceTransitionStart, + }, + }) + // THEN: the request is permitted without template-update privileges, + // because no durable template version pin is requested. + require.NoError(t, err) + require.Equal(t, codersdk.WorkspaceTransitionStop, stopBuild.Transition) + + // THEN: the persisted child build intent leaves the optional fields unset. + orchestration, err := dbtestutil.GetWorkspaceBuildOrchestrationByParentBuildID(ctx, sqlDB, stopBuild.ID) + require.NoError(t, err) + require.Equal(t, "pending", orchestration.Status) + require.Equal(t, workspace.ID, orchestration.WorkspaceID) + require.Equal(t, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransition(orchestration.ChildTransition)) + require.False(t, orchestration.ChildTemplateVersionID.Valid) + require.False(t, orchestration.ChildTemplateVersionPresetID.Valid) + require.False(t, orchestration.ChildReason.Valid) + require.Empty(t, orchestration.ChildLogLevel) + + // THEN: nil parameters are coerced to an empty JSON array, not null, to + // satisfy the database CHECK constraint. + require.JSONEq(t, "[]", string(orchestration.ChildRichParameterValues)) + var childRichParameterValues []codersdk.WorkspaceBuildParameter + require.NoError(t, json.Unmarshal(orchestration.ChildRichParameterValues, &childRichParameterValues)) + require.Empty(t, childRichParameterValues) +} + +func TestPostWorkspaceBuildsOnSuccessPinnedChildVersionRequiresTemplateUpdate(t *testing.T) { + t.Parallel() + + // GIVEN: a running workspace owned by a non-template-admin. + client, _ := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + first := coderdtest.CreateFirstUser(t, client) + userClient, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) + + version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, userClient, template.ID) + initialBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID) + require.Equal(t, codersdk.WorkspaceStatusRunning, initialBuild.Status) + + // WHEN: the non-template-admin tries to queue a stop build with a + // pinned on_success child version. + ctx := testutil.Context(t, testutil.WaitLong) + _, err := userClient.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStop, + OnSuccess: &codersdk.CreateWorkspaceBuildOnSuccessRequest{ + Transition: codersdk.WorkspaceTransitionStart, + TemplateVersionID: version.ID, + }, + }) + require.Error(t, err) + + // THEN: the API rejects the durable child version pin and explains the + // missing template update permission. + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusForbidden, apiErr.StatusCode()) + require.Contains(t, apiErr.Response.Detail, "template update permission") + + // THEN: no new workspace build is created. + builds, err := userClient.WorkspaceBuilds(ctx, codersdk.WorkspaceBuildsRequest{WorkspaceID: workspace.ID}) + require.NoError(t, err) + require.Len(t, builds, 1) + require.Equal(t, initialBuild.ID, builds[0].ID) +} + +func TestPostWorkspaceBuildsOnSuccessValidation(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + request codersdk.CreateWorkspaceBuildRequest + wantMessage string + }{ + { + name: "ParentMustBeStop", + request: codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStart, + OnSuccess: &codersdk.CreateWorkspaceBuildOnSuccessRequest{ + Transition: codersdk.WorkspaceTransitionStart, + }, + }, + wantMessage: "OnSuccess is only permitted when stopping a workspace.", + }, + { + // The oneof=start struct tag on OnSuccess.Transition rejects this + // during httpapi.Read, before the explicit check is reached. + name: "ChildMustBeStart", + request: codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStop, + OnSuccess: &codersdk.CreateWorkspaceBuildOnSuccessRequest{ + Transition: codersdk.WorkspaceTransitionStop, + }, + }, + wantMessage: "Validation failed.", + }, + { + name: "ParentDryRunRejected", + request: codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStop, + DryRun: true, + OnSuccess: &codersdk.CreateWorkspaceBuildOnSuccessRequest{ + Transition: codersdk.WorkspaceTransitionStart, + }, + }, + wantMessage: "OnSuccess cannot be set alongside DryRun.", + }, + { + name: "ParentOrphanRejected", + request: codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStop, + Orphan: true, + OnSuccess: &codersdk.CreateWorkspaceBuildOnSuccessRequest{ + Transition: codersdk.WorkspaceTransitionStart, + }, + }, + wantMessage: "OnSuccess cannot be set alongside Orphan.", + }, + { + name: "ParentProvisionerStateRejected", + request: codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStop, + ProvisionerState: []byte("state"), + OnSuccess: &codersdk.CreateWorkspaceBuildOnSuccessRequest{ + Transition: codersdk.WorkspaceTransitionStart, + }, + }, + wantMessage: "OnSuccess cannot be set alongside ProvisionerState.", + }, + { + name: "ChildPresetWithoutVersionRejected", + request: codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStop, + OnSuccess: &codersdk.CreateWorkspaceBuildOnSuccessRequest{ + Transition: codersdk.WorkspaceTransitionStart, + TemplateVersionPresetID: uuid.New(), + }, + }, + wantMessage: "OnSuccess TemplateVersionPresetID requires TemplateVersionID.", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // GIVEN: a running workspace. + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + first := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, template.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + + // WHEN: an invalid on_success request is posted. + _, err := client.CreateWorkspaceBuild(testutil.Context(t, testutil.WaitLong), workspace.ID, tt.request) + require.Error(t, err) + + // THEN: the API rejects the request before creating a build. + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + require.Contains(t, apiErr.Message, tt.wantMessage) + }) + } +} + +// Canceling an already-running job resolves the orchestration as +// "failed", not "canceled". This hits the same orchestrator branch as +// TestPostWorkspaceBuildsOnSuccessParentFailed below; despite that +// overlap, the test pins this non-obvious end-to-end behavior. +func TestPostWorkspaceBuildsOnSuccessParentCanceledMidFlight(t *testing.T) { + t.Parallel() + + // GIVEN: a running workspace whose stop apply will block. + db, ps, sqlDB := dbtestutil.NewDBWithSQLDB(t) + client, _, _ := coderdtest.NewWithAPI(t, &coderdtest.Options{ + Database: db, + Pubsub: ps, + IncludeProvisionerDaemon: true, + }) + first := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, + echoResponsesWithRichParameter("foo", echoResponseOptions{ + blockStopApply: true, + }), + ) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, template.ID) + initialBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + require.Equal(t, codersdk.WorkspaceStatusRunning, initialBuild.Status) + + // WHEN: a stop build is created with an on_success start build. + ctx := testutil.Context(t, testutil.WaitLong) + user, err := client.User(ctx, codersdk.Me) + require.NoError(t, err) + + stopBuild, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStop, + OnSuccess: &codersdk.CreateWorkspaceBuildOnSuccessRequest{ + Transition: codersdk.WorkspaceTransitionStart, + TemplateVersionID: template.ActiveVersionID, + }, + }) + require.NoError(t, err) + require.Equal(t, codersdk.WorkspaceTransitionStop, stopBuild.Transition) + require.Equal(t, codersdk.BuildReasonInitiator, stopBuild.Reason) + + // WHEN: the parent stop build starts running and is canceled. + require.Eventually(t, func() bool { + var err error + stopBuild, err = client.WorkspaceBuild(ctx, stopBuild.ID) + return err == nil && + stopBuild.Job.Status == codersdk.ProvisionerJobRunning + }, testutil.WaitShort, testutil.IntervalFast) + + require.NoError(t, client.CancelWorkspaceBuild(ctx, stopBuild.ID, codersdk.CancelWorkspaceBuildParams{})) + require.Eventually(t, func() bool { + var err error + stopBuild, err = client.WorkspaceBuild(ctx, stopBuild.ID) + if err != nil { + return false + } + return stopBuild.Job.Status == codersdk.ProvisionerJobFailed && + stopBuild.Job.Error == "canceled" + }, testutil.WaitShort, testutil.IntervalFast) + + // THEN: the server resolves the orchestration without creating the + // child start build. + require.Eventually(t, func() bool { + orchestration, err := dbtestutil.GetWorkspaceBuildOrchestrationByParentBuildID(ctx, sqlDB, stopBuild.ID) + return err == nil && + orchestration.Status == "failed" && + !orchestration.ChildBuildID.Valid && + orchestration.Error.Valid && + orchestration.Error.String == "parent workspace build failed: canceled" + }, testutil.WaitShort, testutil.IntervalFast) + + _, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber( + ctx, + user.Username, + workspace.Name, + strconv.FormatInt(int64(stopBuild.BuildNumber+1), 10), + ) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) +} + +func TestPostWorkspaceBuildsOnSuccessParentFailed(t *testing.T) { + t.Parallel() + + // GIVEN: a running workspace whose stop apply will fail. + db, ps, sqlDB := dbtestutil.NewDBWithSQLDB(t) + client, _, _ := coderdtest.NewWithAPI(t, &coderdtest.Options{ + Database: db, + Pubsub: ps, + IncludeProvisionerDaemon: true, + }) + first := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, + echoResponsesWithRichParameter("foo", echoResponseOptions{ + failStopApply: true, + }), + ) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, template.ID) + initialBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + require.Equal(t, codersdk.WorkspaceStatusRunning, initialBuild.Status) + + // WHEN: a stop build is created with an on_success start build. + ctx := testutil.Context(t, testutil.WaitLong) + user, err := client.User(ctx, codersdk.Me) + require.NoError(t, err) + + stopBuild, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStop, + OnSuccess: &codersdk.CreateWorkspaceBuildOnSuccessRequest{ + Transition: codersdk.WorkspaceTransitionStart, + TemplateVersionID: template.ActiveVersionID, + }, + }) + require.NoError(t, err) + + // WHEN: the parent stop build fails. + stopBuild = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, stopBuild.ID) + require.Equal(t, codersdk.ProvisionerJobFailed, stopBuild.Job.Status) + + // THEN: the server resolves the orchestration without creating the + // child start build. + require.Eventually(t, func() bool { + orchestration, err := dbtestutil.GetWorkspaceBuildOrchestrationByParentBuildID(ctx, sqlDB, stopBuild.ID) + return err == nil && + orchestration.Status == "failed" && + !orchestration.ChildBuildID.Valid && + orchestration.Error.Valid && + orchestration.Error.String == "parent workspace build failed: failed!" + }, testutil.WaitShort, testutil.IntervalFast) + + _, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber( + ctx, + user.Username, + workspace.Name, + strconv.FormatInt(int64(stopBuild.BuildNumber+1), 10), + ) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) +} + +func TestPostWorkspaceBuildsOnSuccessNonRetryableChildBuildFailure(t *testing.T) { + t.Parallel() + + // GIVEN: a running workspace with a rich parameter value that + // satisfies the template regex validation. + db, ps, sqlDB := dbtestutil.NewDBWithSQLDB(t) + client, _, _ := coderdtest.NewWithAPI(t, &coderdtest.Options{ + Database: db, + Pubsub: ps, + IncludeProvisionerDaemon: true, + }) + first := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, + echoResponsesWithRichParameter("foo", echoResponseOptions{ + validationRegex: "^good$", + }), + ) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, template.ID, func(request *codersdk.CreateWorkspaceRequest) { + request.RichParameterValues = []codersdk.WorkspaceBuildParameter{ + {Name: "foo", Value: "good"}, + } + }) + initialBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + require.Equal(t, codersdk.WorkspaceStatusRunning, initialBuild.Status) + + // WHEN: a stop build is created with an on_success child build + // that has an invalid rich parameter value. + ctx := testutil.Context(t, testutil.WaitLong) + user, err := client.User(ctx, codersdk.Me) + require.NoError(t, err) + + stopBuild, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStop, + OnSuccess: &codersdk.CreateWorkspaceBuildOnSuccessRequest{ + Transition: codersdk.WorkspaceTransitionStart, + TemplateVersionID: template.ActiveVersionID, + // The invalid value triggers a non-retryable child build + // creation error when the orchestrator processes the row. + RichParameterValues: []codersdk.WorkspaceBuildParameter{ + {Name: "foo", Value: "bad"}, + }, + }, + }) + require.NoError(t, err) + + stopBuild = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, stopBuild.ID) + require.Equal(t, codersdk.ProvisionerJobSucceeded, stopBuild.Job.Status) + require.Equal(t, codersdk.WorkspaceStatusStopped, stopBuild.Status) + + // THEN: the server marks the orchestration as failed without + // retrying or creating the child start build. + var orchestration database.WorkspaceBuildOrchestration + require.Eventually(t, func() bool { + orchestration, err = dbtestutil.GetWorkspaceBuildOrchestrationByParentBuildID(ctx, sqlDB, stopBuild.ID) + return err == nil && + orchestration.Status == "failed" && + !orchestration.ChildBuildID.Valid && + orchestration.AttemptCount == 0 && + !orchestration.NextRetryAfter.Valid && + orchestration.Error.Valid + }, testutil.WaitShort, testutil.IntervalFast) + require.Contains(t, orchestration.Error.String, "Unable to validate parameters") + + _, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber( + ctx, + user.Username, + workspace.Name, + strconv.FormatInt(int64(stopBuild.BuildNumber+1), 10), + ) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) +} + +func TestPostWorkspaceBuildsOnSuccessRetryableChildBuildFailureDoesNotBlockLaterRestart(t *testing.T) { + t.Parallel() + + // GIVEN: two provisioners, one holding a template import job + // open to make the child build fail retryably while the other + // processes workspace builds. + db, ps, sqlDB := dbtestutil.NewDBWithSQLDB(t) + client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{ + Database: db, + Pubsub: ps, + IncludeProvisionerDaemon: true, + }) + coderdtest.NewProvisionerDaemon(t, api) + + first := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) + + workspace := coderdtest.CreateWorkspace(t, client, template.ID) + startBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + require.Equal(t, codersdk.WorkspaceStatusRunning, startBuild.Status) + + blockedVersion := coderdtest.UpdateTemplateVersion(t, client, first.OrganizationID, + // Without a PlanComplete response, the echo provisioner will + // keep the template version import job running. + &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: []*proto.Response{{ + Type: &proto.Response_Log{ + Log: &proto.Log{}, + }, + }}, + }, template.ID, + ) + coderdtest.AwaitTemplateVersionJobRunning(t, client, blockedVersion.ID) + + // WHEN: a stop build is created with an on_success child build + // request that references a template version whose import job is + // still running, causing child build creation to be retried later. + ctx := testutil.Context(t, testutil.WaitLong) + badStopBuild, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStop, + OnSuccess: &codersdk.CreateWorkspaceBuildOnSuccessRequest{ + Transition: codersdk.WorkspaceTransitionStart, + TemplateVersionID: blockedVersion.ID, + }, + }) + require.NoError(t, err) + + badStopBuild = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, badStopBuild.ID) + require.Equal(t, codersdk.ProvisionerJobSucceeded, badStopBuild.Job.Status) + require.Equal(t, codersdk.WorkspaceStatusStopped, badStopBuild.Status) + + // THEN: the orchestrator records a delayed retry after child + // build creation fails because the requested template version + // is still importing. + var badOrchestration database.WorkspaceBuildOrchestration + require.Eventually(t, func() bool { + badOrchestration, err = dbtestutil.GetWorkspaceBuildOrchestrationByParentBuildID(ctx, sqlDB, badStopBuild.ID) + return err == nil && + badOrchestration.Status == "pending" && + badOrchestration.AttemptCount == 1 && + badOrchestration.NextRetryAfter.Valid + }, testutil.WaitShort, testutil.IntervalFast) + require.False(t, badOrchestration.ChildBuildID.Valid) + require.True(t, badOrchestration.Error.Valid) + require.Contains(t, badOrchestration.Error.String, "template version is running") + + // WHEN: a later restart uses a valid child build request. + goodWorkspace := coderdtest.CreateWorkspace(t, client, template.ID) + goodStartBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, goodWorkspace.LatestBuild.ID) + require.Equal(t, codersdk.WorkspaceStatusRunning, goodStartBuild.Status) + + user, err := client.User(ctx, codersdk.Me) + require.NoError(t, err) + + goodStopBuild, err := client.CreateWorkspaceBuild(ctx, goodWorkspace.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStop, + OnSuccess: &codersdk.CreateWorkspaceBuildOnSuccessRequest{ + Transition: codersdk.WorkspaceTransitionStart, + TemplateVersionID: template.ActiveVersionID, + }, + }) + require.NoError(t, err) + + goodStopBuild = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, goodStopBuild.ID) + require.Equal(t, codersdk.ProvisionerJobSucceeded, goodStopBuild.Job.Status) + require.Equal(t, codersdk.WorkspaceStatusStopped, goodStopBuild.Status) + + // THEN: the delayed retry row does not block the later orchestration. + var goodChildBuild codersdk.WorkspaceBuild + require.Eventually(t, func() bool { + goodChildBuild, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber( + ctx, + user.Username, + goodWorkspace.Name, + strconv.FormatInt(int64(goodStopBuild.BuildNumber+1), 10), + ) + return err == nil && + goodChildBuild.Transition == codersdk.WorkspaceTransitionStart + }, testutil.WaitMedium, testutil.IntervalFast) + + goodChildBuild = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, goodChildBuild.ID) + require.Equal(t, codersdk.ProvisionerJobSucceeded, goodChildBuild.Job.Status) + require.Equal(t, codersdk.WorkspaceStatusRunning, goodChildBuild.Status) +} + +type echoResponseOptions struct { + blockStopApply bool + failStopApply bool + validationRegex string +} + +func echoResponsesWithRichParameter(paramName string, options echoResponseOptions) *echo.Responses { + validationError := "" + if options.validationRegex != "" { + validationError = "invalid parameter value" + } + + responses := &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionInit: echo.InitComplete, + ProvisionGraph: []*proto.Response{{ + Type: &proto.Response_Graph{ + Graph: &proto.GraphComplete{ + Parameters: []*proto.RichParameter{{ + Name: paramName, + Type: "string", + DefaultValue: "bar", + Mutable: true, + FormType: proto.ParameterFormType_INPUT, + ValidationRegex: options.validationRegex, + ValidationError: validationError, + }}, + }, + }, + }}, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ApplyComplete, + } + if options.blockStopApply { + responses.ProvisionApplyMap = map[proto.WorkspaceTransition][]*proto.Response{ + proto.WorkspaceTransition_START: echo.ApplyComplete, + proto.WorkspaceTransition_STOP: {{ + Type: &proto.Response_Log{ + Log: &proto.Log{}, + }, + }}, + } + } + if options.failStopApply { + responses.ProvisionApplyMap = map[proto.WorkspaceTransition][]*proto.Response{ + proto.WorkspaceTransition_START: echo.ApplyComplete, + proto.WorkspaceTransition_STOP: echo.ApplyFailed, + } + } + return responses +} diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 8d2a3887303..d53989d2baf 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -2943,20 +2943,7 @@ func validWorkspaceSchedule(s *string) (sql.NullString, error) { } func (api *API) publishWorkspaceUpdate(ctx context.Context, ownerID uuid.UUID, event wspubsub.WorkspaceEvent) { - err := event.Validate() - if err != nil { - api.Logger.Warn(ctx, "invalid workspace update event", - slog.F("workspace_id", event.WorkspaceID), - slog.F("event_kind", event.Kind), slog.Error(err)) - return - } - msg, err := json.Marshal(event) - if err != nil { - api.Logger.Warn(ctx, "failed to marshal workspace update", - slog.F("workspace_id", event.WorkspaceID), slog.Error(err)) - return - } - err = api.Pubsub.Publish(wspubsub.WorkspaceEventChannel(ownerID), msg) + err := wspubsub.PublishWorkspaceEvent(ctx, api.Pubsub, ownerID, event) if err != nil { api.Logger.Warn(ctx, "failed to publish workspace update", slog.F("workspace_id", event.WorkspaceID), slog.Error(err)) diff --git a/coderd/wsbuildorchestrator/doc.go b/coderd/wsbuildorchestrator/doc.go new file mode 100644 index 00000000000..5c0c79e63ef --- /dev/null +++ b/coderd/wsbuildorchestrator/doc.go @@ -0,0 +1,4 @@ +// Package wsbuildorchestrator runs the background worker that +// fulfills workspace build orchestrations once their parent build +// reaches a terminal state. +package wsbuildorchestrator diff --git a/coderd/wsbuildorchestrator/wsbuildorchestrator.go b/coderd/wsbuildorchestrator/wsbuildorchestrator.go new file mode 100644 index 00000000000..58a6fa1234f --- /dev/null +++ b/coderd/wsbuildorchestrator/wsbuildorchestrator.go @@ -0,0 +1,567 @@ +package wsbuildorchestrator + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "fmt" + "net/http" + "sync" + "sync/atomic" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/google/uuid" + "golang.org/x/xerrors" + + "cdr.dev/slog/v3" + "github.com/coder/coder/v2/coderd/audit" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/database/provisionerjobs" + "github.com/coder/coder/v2/coderd/database/pubsub" + "github.com/coder/coder/v2/coderd/files" + "github.com/coder/coder/v2/coderd/pproflabel" + "github.com/coder/coder/v2/coderd/wsbuilder" + "github.com/coder/coder/v2/coderd/wspubsub" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/quartz" +) + +const ( + subscribeMaxBackoff = 10 * time.Second + // Pubsub should wake the worker promptly, while occasional + // polling prevents missed wakes from leaving rows pending + // indefinitely. + backupPollInterval = 30 * time.Second + maxAttempts = 3 + retryDelay = 30 * time.Second +) + +// Orchestrator fulfills workspace build orchestrations after their +// parent builds reach a terminal state. +type Orchestrator struct { + logger slog.Logger + db database.Store + pubsub pubsub.Pubsub + fileCache *files.Cache + buildUsageChecker *atomic.Pointer[wsbuilder.UsageChecker] + deploymentValues *codersdk.DeploymentValues + experiments codersdk.Experiments + builderMetrics *wsbuilder.Metrics + clock quartz.Clock + + wakeCh chan struct{} + + // startOnce ensures the background goroutines are launched at most + // once, even if Start is called more than once. + startOnce sync.Once + // cancel cancels the context on all running jobs. If the ctx + // passed into `Start` is canceled, the jobs will also stop. + cancel context.CancelFunc + // wg ensures all job goroutines have exited before Close returns. + wg sync.WaitGroup +} + +type Options struct { + Logger slog.Logger + Database database.Store + Pubsub pubsub.Pubsub + FileCache *files.Cache + BuildUsageChecker *atomic.Pointer[wsbuilder.UsageChecker] + DeploymentValues *codersdk.DeploymentValues + Experiments codersdk.Experiments + BuilderMetrics *wsbuilder.Metrics + Clock quartz.Clock +} + +// New constructs an Orchestrator. Call Start to begin processing. +func New(opts Options) *Orchestrator { + clock := opts.Clock + if clock == nil { + clock = quartz.NewReal() + } + return &Orchestrator{ + logger: opts.Logger.Named("workspace_build_orchestrator"), + db: opts.Database, + pubsub: opts.Pubsub, + fileCache: opts.FileCache, + buildUsageChecker: opts.BuildUsageChecker, + deploymentValues: opts.DeploymentValues, + experiments: opts.Experiments, + builderMetrics: opts.BuilderMetrics, + clock: clock, + // Keep one pending wake signal while the worker is between + // runs. One is enough because each run drains all ready + // orchestration rows. + wakeCh: make(chan struct{}, 1), + } +} + +// Start launches the orchestrator's background goroutines. It is safe +// to call more than once; only the first call has any effect. Call +// Close to stop the goroutines and wait for their exit. +func (o *Orchestrator) Start(ctx context.Context) { + o.startOnce.Do(func() { + ctx, o.cancel = context.WithCancel(ctx) + o.wg.Add(2) + pproflabel.Go(ctx, pproflabel.Service(pproflabel.ServiceWorkspaceBuildOrchestrator, "goroutine", "subscribe"), func(ctx context.Context) { + defer o.wg.Done() + o.subscribe(ctx) + }) + pproflabel.Go(ctx, pproflabel.Service(pproflabel.ServiceWorkspaceBuildOrchestrator, "goroutine", "run"), func(ctx context.Context) { + defer o.wg.Done() + o.run(ctx) + }) + }) +} + +// Close stops the orchestrator and waits for its goroutines to exit. +func (o *Orchestrator) Close() { + if o.cancel != nil { + o.cancel() + } + o.wg.Wait() +} + +func (o *Orchestrator) subscribe(ctx context.Context) { + eb := backoff.NewExponentialBackOff() + eb.MaxElapsedTime = 0 + eb.MaxInterval = subscribeMaxBackoff + bkoff := backoff.WithContext(eb, ctx) + + var cancelSubscribe func() + err := backoff.Retry(func() error { + cancelFn, err := o.pubsub.SubscribeWithErr( + wspubsub.WorkspaceBuildOrchestrationWakeChannel, + o.listen, + ) + if err != nil { + o.logger.Warn(ctx, "failed to subscribe to wake channel", slog.Error(err)) + return err + } + cancelSubscribe = cancelFn + return nil + }, bkoff) + if err != nil { + if ctx.Err() == nil { + o.logger.Error(ctx, "code bug: retry failed before context canceled", slog.Error(err)) + } + return + } + defer cancelSubscribe() + o.logger.Debug(ctx, "subscribed to wake channel") + + // Reconcile rows that may have become ready while the worker was + // not subscribed. + o.wake() + + <-ctx.Done() +} + +func (o *Orchestrator) listen(ctx context.Context, _ []byte, err error) { + if xerrors.Is(err, pubsub.ErrDroppedMessages) { + o.logger.Warn(ctx, "pubsub may have dropped wake signals") + o.wake() + return + } + if err != nil { + o.logger.Warn(ctx, "unhandled pubsub error", slog.Error(err)) + return + } + o.wake() +} + +func (o *Orchestrator) wake() { + select { + case o.wakeCh <- struct{}{}: + default: + } +} + +func (o *Orchestrator) run(ctx context.Context) { + ticker := o.clock.NewTicker(backupPollInterval) + defer ticker.Stop() + + for { + // wakeCh can win the select below even when ctx is canceled, + // so re-check here. Once canceled, do not begin another + // processing round. + if ctx.Err() != nil { + return + } + + err := o.processAll(ctx) + if err != nil && ctx.Err() == nil { + o.logger.Error(ctx, "failed to process orchestrations", slog.Error(err)) + } + + select { + case <-o.wakeCh: + case <-ticker.C: + case <-ctx.Done(): + return + } + } +} + +// processAll processes all pending orchestration rows whose parent +// builds have reached a terminal state. +func (o *Orchestrator) processAll(ctx context.Context) error { + for { + found, err := o.processNext(ctx) + if err != nil { + return err + } + if !found { + return nil + } + } +} + +func (o *Orchestrator) processNext(ctx context.Context) (bool, error) { + //nolint:gocritic // Inserting the orchestration row required + // authorization for the parent and child transitions. The worker + // uses system authority to fulfill that durable intent after the + // parent build completes. + sysCtx := dbauthz.AsSystemRestricted(ctx) + + var ( + found bool + workspace database.Workspace + childJob *database.ProvisionerJob + orchestrationID uuid.UUID + childBuildErr error + ) + + err := o.db.InTx(func(tx database.Store) error { + orchestration, err := tx.GetNextPendingWorkspaceBuildOrchestrationForUpdate(sysCtx) + if xerrors.Is(err, sql.ErrNoRows) { + return nil + } + if err != nil { + return xerrors.Errorf("get next pending workspace build orchestration: %w", err) + } + + found = true + orchestrationID = orchestration.ID + + // markFailed resolves the locked orchestration as failed with + // a message, so a row that cannot make progress does not keep + // blocking later ones. + markFailed := func(msg string) error { + _, err := tx.UpdateWorkspaceBuildOrchestrationFailedByID(sysCtx, database.UpdateWorkspaceBuildOrchestrationFailedByIDParams{ + Error: sql.NullString{String: msg, Valid: true}, + UpdatedAt: dbtime.Now(), + ID: orchestration.ID, + }) + if err != nil { + return xerrors.Errorf("mark workspace build orchestration as failed: %w", err) + } + return nil + } + + // parentBuild and parentJob are guaranteed to exist by + // foreign keys on the locked orchestration row, so an error + // here is unexpected and likely transient. Return it to + // retry, rather than resolving the orchestration as failed. + parentBuild, err := tx.GetWorkspaceBuildByID(sysCtx, orchestration.ParentBuildID) + if err != nil { + return xerrors.Errorf("get parent workspace build: %w", err) + } + + parentJob, err := tx.GetProvisionerJobByID(sysCtx, parentBuild.JobID) + if err != nil { + return xerrors.Errorf("get parent provisioner job: %w", err) + } + + // Resolve terminal parent outcomes that do not create a child + // build. Successful parents continue below. + switch parentJob.JobStatus { + case database.ProvisionerJobStatusSucceeded: + case database.ProvisionerJobStatusCanceled: + _, err = tx.UpdateWorkspaceBuildOrchestrationCanceledByID(sysCtx, database.UpdateWorkspaceBuildOrchestrationCanceledByIDParams{ + ID: orchestration.ID, + UpdatedAt: dbtime.Now(), + }) + if err != nil { + return xerrors.Errorf("mark workspace build orchestration as canceled: %w", err) + } + return nil + case database.ProvisionerJobStatusFailed: + parentFailure := "parent workspace build failed" + if parentJob.Error.Valid && parentJob.Error.String != "" { + parentFailure = fmt.Sprintf("parent workspace build failed: %s", parentJob.Error.String) + } + return markFailed(parentFailure) + default: + // This should be unreachable because the row-locking query + // only selects terminal parent jobs. Mark the row as failed + // because retrying would block later orchestrations. + return markFailed(fmt.Sprintf("unexpected parent job status %q", parentJob.JobStatus)) + } + + childBuildRequest, err := childBuildRequestFromOrchestration(orchestration) + if err != nil { + // Mark the row failed to avoid retrying work that cannot + // make progress. + return markFailed(err.Error()) + } + + workspace, err = tx.GetWorkspaceByID(sysCtx, parentBuild.WorkspaceID) + if err != nil { + return xerrors.Errorf("get workspace: %w", err) + } + + // GetWorkspaceByID returns soft-deleted rows. + if workspace.Deleted { + return markFailed("workspace was deleted") + } + + // A dormant workspace must be woken before it can start. + // Starting it while still dormant would leave it running but + // still subject to deleting_at, which could auto-delete it. + if workspace.DormantAt.Valid { + return markFailed("workspace is dormant") + } + + childBuild, provisionerJob, err := o.createBuild(sysCtx, tx, workspace, parentBuild.InitiatorID, childBuildRequest) + if err != nil { + // Carry the builder error out of the transaction; the + // fail-vs-retry decision runs after the rollback. + childBuildErr = err + return xerrors.Errorf("create child workspace build: %w", err) + } + childJob = provisionerJob + + _, err = tx.UpdateWorkspaceBuildOrchestrationCompletedByID(sysCtx, database.UpdateWorkspaceBuildOrchestrationCompletedByIDParams{ + ChildBuildID: uuid.NullUUID{ + UUID: childBuild.ID, + Valid: true, + }, + UpdatedAt: dbtime.Now(), + ID: orchestration.ID, + }) + if err != nil { + return xerrors.Errorf("complete workspace build orchestration: %w", err) + } + + return nil + }, nil) + if err != nil { + if !found { + // A persistent error here blocks the whole queue, but + // that is systemic, not a poison row. Surface for retry. + return false, err + } + + if ctx.Err() != nil { + // On shutdown, don't resolve or log it as unexpected + // error below. + return false, err + } + + // A row was locked but processing failed. Resolve so it does + // not stay pending and block newer orchestrations. + errMsg := err.Error() + failNow := false + if childBuildErr != nil { + // The child build error carries an HTTP status we can + // classify into retryable vs permanent. + errMsg = childBuildErrorMessage(childBuildErr) + failNow = childBuildErrorShouldFailOrchestration(childBuildErr) + } else { + o.logger.Error(ctx, "unexpected error processing orchestration", + slog.F("workspace_build_orchestration_id", orchestrationID), + slog.Error(err)) + } + + var markErr error + if failNow { + // Mark the orchestration failed so one bad row does not + // block later orchestrations. + _, markErr = o.db.UpdateWorkspaceBuildOrchestrationFailedByID(sysCtx, database.UpdateWorkspaceBuildOrchestrationFailedByIDParams{ + Error: sql.NullString{ + String: errMsg, + Valid: true, + }, + UpdatedAt: dbtime.Now(), + ID: orchestrationID, + }) + } else { + // Back off and retry, eventually failing after maxAttempts + // so a persistently failing row stops blocking the queue. + now := dbtime.Now() + _, markErr = o.db.UpdateWorkspaceBuildOrchestrationRetryByID(sysCtx, database.UpdateWorkspaceBuildOrchestrationRetryByIDParams{ + Error: sql.NullString{ + String: errMsg, + Valid: true, + }, + NextRetryAfter: now.Add(retryDelay), + UpdatedAt: now, + ID: orchestrationID, + MaxAttemptCount: maxAttempts, + }) + } + + if markErr != nil { + if xerrors.Is(markErr, sql.ErrNoRows) { + // This update runs after the transaction has ended, so + // another worker may have resolved the orchestration + // first. Treat that race as success because the row no + // longer needs processing. + return found, nil + } + // Preserve the original error because the orchestration row + // could not be updated with it. + return false, errors.Join( + err, + xerrors.Errorf("resolve workspace build orchestration: %w", markErr), + ) + } + + return found, nil + } + + // These post-commit notifications are best-effort. The child + // build and provisioner job are already persisted, so missing + // pubsub does not corrupt state. It can delay workers or + // subscribers until another wake or refresh. + if childJob != nil { + if err := provisionerjobs.PostJob(o.pubsub, *childJob); err != nil { + o.logger.Error(ctx, "failed to post child provisioner job to pubsub", + slog.F("workspace_build_orchestration_id", orchestrationID), + slog.F("workspace_id", workspace.ID), + slog.Error(err), + ) + } + + err := wspubsub.PublishWorkspaceEvent(ctx, o.pubsub, workspace.OwnerID, wspubsub.WorkspaceEvent{ + Kind: wspubsub.WorkspaceEventKindStateChange, + WorkspaceID: workspace.ID, + }) + if err != nil { + o.logger.Warn(ctx, "failed to publish workspace update", + slog.F("workspace_build_orchestration_id", orchestrationID), + slog.F("workspace_id", workspace.ID), slog.Error(err)) + } + } + + return found, nil +} + +func childBuildRequestFromOrchestration(orchestration database.WorkspaceBuildOrchestration) (codersdk.CreateWorkspaceBuildRequest, error) { + var childParameterValues []codersdk.WorkspaceBuildParameter + if len(orchestration.ChildRichParameterValues) > 0 { + err := json.Unmarshal(orchestration.ChildRichParameterValues, &childParameterValues) + if err != nil { + return codersdk.CreateWorkspaceBuildRequest{}, xerrors.Errorf("unmarshal child rich parameter values: %w", err) + } + } + if childParameterValues == nil { + childParameterValues = []codersdk.WorkspaceBuildParameter{} + } + + request := codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransition(orchestration.ChildTransition), + RichParameterValues: childParameterValues, + LogLevel: codersdk.ProvisionerLogLevel(orchestration.ChildLogLevel), + } + + if orchestration.ChildTemplateVersionID.Valid { + request.TemplateVersionID = orchestration.ChildTemplateVersionID.UUID + } + if orchestration.ChildTemplateVersionPresetID.Valid { + request.TemplateVersionPresetID = orchestration.ChildTemplateVersionPresetID.UUID + } + if orchestration.ChildReason.Valid { + request.Reason = codersdk.CreateWorkspaceBuildReason(orchestration.ChildReason.BuildReason) + } + + return request, nil +} + +func (o *Orchestrator) createBuild( + ctx context.Context, + tx database.Store, + workspace database.Workspace, + initiatorID uuid.UUID, + request codersdk.CreateWorkspaceBuildRequest, +) (*database.WorkspaceBuild, *database.ProvisionerJob, error) { + transition := database.WorkspaceTransition(request.Transition) + builder := wsbuilder.New(workspace, transition, *o.buildUsageChecker.Load()). + Initiator(initiatorID). + RichParameterValues(request.RichParameterValues). + LogLevel(string(request.LogLevel)). + DeploymentValues(o.deploymentValues). + Experiments(o.experiments). + TemplateVersionPresetID(request.TemplateVersionPresetID). + BuildMetrics(o.builderMetrics) + + if request.TemplateVersionID != uuid.Nil { + builder = builder.VersionID(request.TemplateVersionID) + } else if transition == database.WorkspaceTransitionStart { + builder = builder.ActiveVersion() + } + if request.Reason != "" { + builder = builder.Reason(database.BuildReason(request.Reason)) + } + + workspaceBuild, provisionerJob, _, err := builder.Build(ctx, tx, o.fileCache, + // nil authorization function skips the builder's RBAC and + // config checks. The parent and child transitions were + // authorized when the orchestration row was inserted, and the + // child reuses the parent build's already-validated log + // level. + nil, + // The child build is created by a background worker, so there + // is no request IP to attach. Its initiator is still set from + // the parent build. + audit.WorkspaceBuildBaggage{}, + ) + if err != nil { + return nil, nil, err + } + + return workspaceBuild, provisionerJob, nil +} + +// childBuildErrorShouldFailOrchestration reports whether a child build +// error should be persisted as a failed orchestration instead of retried. +func childBuildErrorShouldFailOrchestration(err error) bool { + buildErr, ok := errors.AsType[wsbuilder.BuildError](err) + if !ok { + return false + } + + switch buildErr.Status { + case http.StatusBadRequest, http.StatusForbidden, http.StatusNotFound: + // These statuses indicate invalid stored build input or a + // permission/resource state that retrying the same request + // will not fix. + return true + default: + return false + } +} + +// childBuildErrorMessage returns the error text to be stored on the +// orchestration row. Build errors can expose cleaner response +// messages than Error(), which may contain only the wrapped cause. +func childBuildErrorMessage(err error) string { + buildErr, ok := errors.AsType[wsbuilder.BuildError](err) + if !ok { + return err.Error() + } + + _, response := buildErr.Response() + if response.Detail != "" && response.Detail != response.Message { + return fmt.Sprintf("%s: %s", response.Message, response.Detail) + } + if response.Message != "" { + return response.Message + } + return buildErr.Error() +} diff --git a/coderd/wsbuildorchestrator/wsbuildorchestrator_internal_test.go b/coderd/wsbuildorchestrator/wsbuildorchestrator_internal_test.go new file mode 100644 index 00000000000..d8fe1445bf3 --- /dev/null +++ b/coderd/wsbuildorchestrator/wsbuildorchestrator_internal_test.go @@ -0,0 +1,427 @@ +package wsbuildorchestrator + +import ( + "context" + "database/sql" + "encoding/json" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "go.uber.org/goleak" + "golang.org/x/xerrors" + + "cdr.dev/slog/v3/sloggers/slogtest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/database/pubsub" + "github.com/coder/coder/v2/coderd/wspubsub" + "github.com/coder/coder/v2/testutil" + "github.com/coder/quartz" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m, testutil.GoleakOptions...) +} + +func newTestOrchestrator(t *testing.T, db database.Store, ps pubsub.Pubsub) *Orchestrator { + t.Helper() + + return New(Options{ + Logger: testutil.Logger(t), + Database: db, + Pubsub: ps, + }) +} + +func TestWorkspaceBuildOrchestratorSubscribeQueuesWakeOnPubsub(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + ps := pubsub.NewInMemory() + o := newTestOrchestrator(t, nil, ps) + + go o.subscribe(ctx) + + // subscribe sends an initial wake after registration. Drain it so + // the publish below tests pubsub delivery without racing setup. + testutil.RequireReceive(ctx, t, o.wakeCh) + + err := wspubsub.PublishWorkspaceBuildOrchestrationWake(ctx, ps) + require.NoError(t, err) + + testutil.RequireReceive(ctx, t, o.wakeCh) +} + +func TestWorkspaceBuildOrchestratorRunProcessesOnWakeAndPoll(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + // trigger causes the run loop to process another pass, either + // via a wake signal or by advancing past the backup poll. + trigger func(ctx context.Context, o *Orchestrator, mClock *quartz.Mock) + }{ + { + name: "Wake", + trigger: func(_ context.Context, o *Orchestrator, _ *quartz.Mock) { + o.wake() + }, + }, + { + name: "BackupPoll", + trigger: func(ctx context.Context, _ *Orchestrator, mClock *quartz.Mock) { + mClock.Advance(backupPollInterval).MustWait(ctx) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + mClock := quartz.NewMock(t) + store := &runStore{ + calls: make(chan struct{}), + } + o := New(Options{ + Logger: testutil.Logger(t), + Database: store, + Clock: mClock, + }) + + go o.run(ctx) + + // Drain the initial pass. run() creates the backup poll + // ticker and then processes (sends) once before + // waiting. So, receiving here also ensures the ticker + // exists before we advance the clock. + testutil.RequireReceive(ctx, t, store.calls) + + tc.trigger(ctx, o, mClock) + // Now this pass can only come from the trigger. + testutil.RequireReceive(ctx, t, store.calls) + }) + } +} + +type runStore struct { + database.Store + calls chan struct{} +} + +func (s *runStore) InTx(fn func(database.Store) error, _ *database.TxOptions) error { + return fn(s) +} + +func (s *runStore) GetNextPendingWorkspaceBuildOrchestrationForUpdate(ctx context.Context) ( + database.WorkspaceBuildOrchestration, error, +) { + select { + case s.calls <- struct{}{}: + case <-ctx.Done(): + return database.WorkspaceBuildOrchestration{}, ctx.Err() + } + return database.WorkspaceBuildOrchestration{}, sql.ErrNoRows +} + +// Note: it overwrites parentJob's OrganizationID and Type. +func seedPendingOrchestration( + ctx context.Context, + t *testing.T, + db database.Store, + workspaceDeleted bool, + parentJob database.ProvisionerJob, +) (database.ProvisionerJob, database.WorkspaceBuild) { + t.Helper() + + 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, + Deleted: workspaceDeleted, + }) + + parentJob.OrganizationID = org.ID + parentJob.Type = database.ProvisionerJobTypeWorkspaceBuild + job := dbgen.ProvisionerJob(t, db, nil, parentJob) + parentBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + WorkspaceID: workspace.ID, + TemplateVersionID: version.ID, + JobID: job.ID, + Transition: database.WorkspaceTransitionStop, + Reason: database.BuildReasonInitiator, + }) + + now := dbtime.Now() + _, 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) + + return job, parentBuild +} + +// succeededJob returns a provisioner job in the succeeded state. +func succeededJob() database.ProvisionerJob { + now := dbtime.Now() + return database.ProvisionerJob{ + StartedAt: sql.NullTime{Time: now, Valid: true}, + CompletedAt: sql.NullTime{Time: now, Valid: true}, + } +} + +// A succeeded parent whose workspace can no longer be started +// (deleted or dormant) must fail the orchestration without creating a +// child build. +func TestWorkspaceBuildOrchestratorFailsForUnstartableWorkspace(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + // seed builds a pending orchestration whose workspace cannot + // start, and returns its parent build. + seed func(ctx context.Context, t *testing.T, db database.Store) database.WorkspaceBuild + wantError string + }{ + { + name: "Deleted", + seed: func(ctx context.Context, t *testing.T, db database.Store) database.WorkspaceBuild { + job, build := seedPendingOrchestration(ctx, t, db, true, succeededJob()) + require.Equal(t, database.ProvisionerJobStatusSucceeded, job.JobStatus) + return build + }, + wantError: "workspace was deleted", + }, + { + name: "Dormant", + seed: func(ctx context.Context, t *testing.T, db database.Store) database.WorkspaceBuild { + job, build := seedPendingOrchestration(ctx, t, db, false, succeededJob()) + require.Equal(t, database.ProvisionerJobStatusSucceeded, job.JobStatus) + _, err := db.UpdateWorkspaceDormantDeletingAt(ctx, database.UpdateWorkspaceDormantDeletingAtParams{ + ID: build.WorkspaceID, + DormantAt: sql.NullTime{Time: dbtime.Now(), Valid: true}, + }) + require.NoError(t, err) + return build + }, + wantError: "workspace is dormant", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // GIVEN: a pending orchestration whose workspace cannot start. + ctx := testutil.Context(t, testutil.WaitShort) + db, _, rawDB := dbtestutil.NewDBWithSQLDB(t) + parentBuild := tc.seed(ctx, t, db) + + o := newTestOrchestrator(t, db, nil) + + // WHEN: the orchestrator processes the row. + found, err := o.processNext(ctx) + require.NoError(t, err) + require.True(t, found) + + // THEN: the orchestration resolves as failed without creating + // a child build. + orchestration, err := dbtestutil.GetWorkspaceBuildOrchestrationByParentBuildID(ctx, rawDB, parentBuild.ID) + require.NoError(t, err) + require.Equal(t, "failed", orchestration.Status) + require.False(t, orchestration.ChildBuildID.Valid) + require.True(t, orchestration.Error.Valid) + require.Equal(t, tc.wantError, orchestration.Error.String) + }) + } +} + +// If a pending parent build is canceled before any provisioner +// acquires it, the orchestration resolves as canceled, with no error +// and without creating a child build. +func TestWorkspaceBuildOrchestratorCancelsForCanceledParent(t *testing.T) { + t.Parallel() + + // GIVEN: a workspace whose parent stop build was canceled + // (without a provisioner acquiring it) and a pending + // orchestration to start it. + ctx := testutil.Context(t, testutil.WaitShort) + db, _, rawDB := dbtestutil.NewDBWithSQLDB(t) + + now := dbtime.Now() + parentJob, parentBuild := seedPendingOrchestration(ctx, t, db, false, database.ProvisionerJob{ + CanceledAt: sql.NullTime{Time: now, Valid: true}, + CompletedAt: sql.NullTime{Time: now, Valid: true}, + }) + require.Equal(t, database.ProvisionerJobStatusCanceled, parentJob.JobStatus) + + o := newTestOrchestrator(t, db, nil) + + // WHEN: the orchestrator processes the row. + found, err := o.processNext(ctx) + require.NoError(t, err) + require.True(t, found) + + // THEN: the orchestration resolves as canceled, with no error and + // without creating a child build. + orchestration, err := dbtestutil.GetWorkspaceBuildOrchestrationByParentBuildID(ctx, rawDB, parentBuild.ID) + require.NoError(t, err) + require.Equal(t, "canceled", orchestration.Status) + require.False(t, orchestration.ChildBuildID.Valid) + require.False(t, orchestration.Error.Valid) +} + +// emptyStore reports no pending orchestrations so the run loop stays +// idle. +type emptyStore struct { + database.Store +} + +func (s emptyStore) InTx(fn func(database.Store) error, _ *database.TxOptions) error { + return fn(s) +} + +func (emptyStore) GetNextPendingWorkspaceBuildOrchestrationForUpdate(context.Context) ( + database.WorkspaceBuildOrchestration, error, +) { + return database.WorkspaceBuildOrchestration{}, sql.ErrNoRows +} + +func TestWorkspaceBuildOrchestratorCloseStopsGoroutines(t *testing.T) { + t.Parallel() + + o := New(Options{ + Logger: testutil.Logger(t), + Database: emptyStore{}, + Pubsub: pubsub.NewInMemory(), + }) + o.Start(context.Background()) + + // Close blocks on wg.Wait, so it returns only once both + // background goroutines have exited. A timeout here means a + // goroutine never exited after cancellation, leaving Close + // blocked. + closed := make(chan struct{}) + go func() { + o.Close() + close(closed) + }() + + ctx := testutil.Context(t, testutil.WaitShort) + select { + case <-ctx.Done(): + t.Fatal("Close did not stop background goroutines") + case <-closed: + } +} + +// lookupErrorStore returns a pending orchestration, then fails the +// parent provisioner job lookup. This exercises the non-child-build +// error path in processNext and captures the resulting retry update. +type lookupErrorStore struct { + database.Store + orchestrationID uuid.UUID + jobErr error + + retryCalled bool + retryParams database.UpdateWorkspaceBuildOrchestrationRetryByIDParams +} + +func (s *lookupErrorStore) InTx(fn func(database.Store) error, _ *database.TxOptions) error { + return fn(s) +} + +func (s *lookupErrorStore) GetNextPendingWorkspaceBuildOrchestrationForUpdate(context.Context) ( + database.WorkspaceBuildOrchestration, error, +) { + return database.WorkspaceBuildOrchestration{ + ID: s.orchestrationID, + ParentBuildID: uuid.New(), + }, nil +} + +func (*lookupErrorStore) GetWorkspaceBuildByID(_ context.Context, id uuid.UUID) ( + database.WorkspaceBuild, error, +) { + return database.WorkspaceBuild{ID: id, JobID: uuid.New()}, nil +} + +func (s *lookupErrorStore) GetProvisionerJobByID(context.Context, uuid.UUID) ( + database.ProvisionerJob, error, +) { + return database.ProvisionerJob{}, s.jobErr +} + +func (s *lookupErrorStore) UpdateWorkspaceBuildOrchestrationRetryByID( + _ context.Context, + arg database.UpdateWorkspaceBuildOrchestrationRetryByIDParams, +) (database.WorkspaceBuildOrchestration, error) { + s.retryCalled = true + s.retryParams = arg + return database.WorkspaceBuildOrchestration{}, nil +} + +// TestWorkspaceBuildOrchestratorRetriesUnexpectedError verifies that +// an unexpected error while processing a row makes processNext +// request a bounded retry rather than surfacing the error, which +// would leave the row pending and block newer orchestrations. +func TestWorkspaceBuildOrchestratorRetriesUnexpectedError(t *testing.T) { + t.Parallel() + + // GIVEN: a store that returns a pending orchestration, then fails + // the parent provisioner job lookup with an unexpected + // (non-child-build) error. + store := &lookupErrorStore{ + orchestrationID: uuid.New(), + jobErr: xerrors.New("boom"), + } + o := New(Options{ + // The unexpected-error path logs at error level by design, so + // tolerate it here instead of failing via slogtest. + Logger: slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}), + Database: store, + Pubsub: pubsub.NewInMemory(), + }) + + ctx := testutil.Context(t, testutil.WaitShort) + + // WHEN: the orchestrator processes the row. + found, err := o.processNext(ctx) + + // THEN: processNext requests a bounded retry (next_retry_after + // and maxAttempts passed) and returns without error, instead of + // surfacing the error, which would leave the row pending. + require.NoError(t, err) + require.True(t, found) + require.True(t, store.retryCalled) + require.Equal(t, store.orchestrationID, store.retryParams.ID) + require.Equal(t, int32(maxAttempts), store.retryParams.MaxAttemptCount) + require.False(t, store.retryParams.NextRetryAfter.IsZero()) + require.True(t, store.retryParams.Error.Valid) + require.Contains(t, store.retryParams.Error.String, "boom") +} diff --git a/coderd/wspubsub/wspubsub.go b/coderd/wspubsub/wspubsub.go index c648022e1da..c2bdda859aa 100644 --- a/coderd/wspubsub/wspubsub.go +++ b/coderd/wspubsub/wspubsub.go @@ -17,6 +17,10 @@ import ( // creating N separate subscriptions. const AllWorkspaceEventChannel = "workspace_updates:all" +// WorkspaceBuildOrchestrationWakeChannel wakes the internal worker that +// processes pending workspace build orchestration rows. +const WorkspaceBuildOrchestrationWakeChannel = "workspace_build_orchestrations:wake" + // HandleWorkspaceBuildUpdate wraps a callback to parse WorkspaceBuildUpdate // messages from the pubsub. func HandleWorkspaceBuildUpdate(cb func(ctx context.Context, payload codersdk.WorkspaceBuildUpdate, err error)) func(ctx context.Context, message []byte, err error) { @@ -48,12 +52,38 @@ func PublishWorkspaceBuildUpdate(_ context.Context, ps pubsub.Pubsub, update cod return nil } +// PublishWorkspaceBuildOrchestrationWake wakes coderd instances that can +// process pending workspace build orchestration rows. Call this after any +// workspace build reaches a terminal state: succeeded, failed, or canceled. +func PublishWorkspaceBuildOrchestrationWake(_ context.Context, ps pubsub.Pubsub) error { + if err := ps.Publish(WorkspaceBuildOrchestrationWakeChannel, []byte("{}")); err != nil { + return xerrors.Errorf("publish workspace build orchestration wake: %w", err) + } + return nil +} + // WorkspaceEventChannel can be used to subscribe to events for // workspaces owned by the provided user ID. func WorkspaceEventChannel(ownerID uuid.UUID) string { return fmt.Sprintf("workspace_owner:%s", ownerID) } +// PublishWorkspaceEvent validates and publishes a workspace event to +// the owner's event channel. +func PublishWorkspaceEvent(_ context.Context, ps pubsub.Pubsub, ownerID uuid.UUID, event WorkspaceEvent) error { + if err := event.Validate(); err != nil { + return xerrors.Errorf("validate workspace event: %w", err) + } + msg, err := json.Marshal(event) + if err != nil { + return xerrors.Errorf("marshal workspace event: %w", err) + } + if err := ps.Publish(WorkspaceEventChannel(ownerID), msg); err != nil { + return xerrors.Errorf("publish workspace event: %w", err) + } + return nil +} + func HandleWorkspaceEvent(cb func(ctx context.Context, payload WorkspaceEvent, err error)) func(ctx context.Context, message []byte, err error) { return func(ctx context.Context, message []byte, err error) { if err != nil { diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index b520f27e4f8..7c6f1ea89f2 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -136,6 +136,34 @@ type CreateWorkspaceBuildRequest struct { TemplateVersionPresetID uuid.UUID `json:"template_version_preset_id,omitempty" format:"uuid"` // Reason sets the reason for the workspace build. Reason CreateWorkspaceBuildReason `json:"reason,omitempty" validate:"omitempty,oneof=dashboard cli ssh_connection vscode_connection jetbrains_connection task_manual_pause"` + // OnSuccess queues a follow-up workspace build after this build succeeds. + // It currently supports restarting a workspace by starting it after a + // successful stop build. + OnSuccess *CreateWorkspaceBuildOnSuccessRequest `json:"on_success,omitempty"` +} + +// CreateWorkspaceBuildOnSuccessRequest queues a follow-up build that +// runs after the parent build succeeds. It currently supports +// restarting a workspace: the parent build must be a "stop" and this +// child build a "start". The child build inherits LogLevel and Reason +// from the parent CreateWorkspaceBuildRequest. +type CreateWorkspaceBuildOnSuccessRequest struct { + // TemplateVersionID pins the child build to a specific template + // version. Pinning requires permission to update the template, + // since the active version may change before the child build + // runs. When empty, the child build uses the template's active + // version at the time it runs. + TemplateVersionID uuid.UUID `json:"template_version_id,omitempty" format:"uuid"` + // Transition must be "start". The parent build's transition must + // be "stop". + Transition WorkspaceTransition `json:"transition" validate:"oneof=start,required"` + // RichParameterValues are applied to the child build. Parameters + // not listed here fall back to their values from the previous + // build, matching normal build behavior. + RichParameterValues []WorkspaceBuildParameter `json:"rich_parameter_values,omitempty"` + // TemplateVersionPresetID selects a preset for the child build. + // It requires TemplateVersionID to also be set. + TemplateVersionPresetID uuid.UUID `json:"template_version_preset_id,omitempty" format:"uuid"` } type WorkspaceOptions struct { diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index 00db92184dd..5f56524b445 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -1767,6 +1767,17 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ { "dry_run": true, "log_level": "debug", + "on_success": { + "rich_parameter_values": [ + { + "name": "string", + "value": "string" + } + ], + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", + "transition": "start" + }, "orphan": true, "reason": "dashboard", "rich_parameter_values": [ diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index f0102c48c5f..53b1ef18421 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4979,6 +4979,37 @@ This is required on creation to enable a user-flow of validating a template work |-----------|--------|----------|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `content` | string | false | | Content must be SKILL.md-format Markdown with YAML frontmatter. The frontmatter must include name, may include description, and must be followed by a non-empty body. | +## codersdk.CreateWorkspaceBuildOnSuccessRequest + +```json +{ + "rich_parameter_values": [ + { + "name": "string", + "value": "string" + } + ], + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", + "transition": "start" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|------------------------------|-------------------------------------------------------------------------------|----------|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `rich_parameter_values` | array of [codersdk.WorkspaceBuildParameter](#codersdkworkspacebuildparameter) | false | | Rich parameter values are applied to the child build. Parameters not listed here fall back to their values from the previous build, matching normal build behavior. | +| `template_version_id` | string | false | | Template version ID pins the child build to a specific template version. Pinning requires permission to update the template, since the active version may change before the child build runs. When empty, the child build uses the template's active version at the time it runs. | +| `template_version_preset_id` | string | false | | Template version preset ID selects a preset for the child build. It requires TemplateVersionID to also be set. | +| `transition` | [codersdk.WorkspaceTransition](#codersdkworkspacetransition) | true | | Transition must be "start". The parent build's transition must be "stop". | + +#### Enumerated Values + +| Property | Value(s) | +|--------------|----------| +| `transition` | `start` | + ## codersdk.CreateWorkspaceBuildReason ```json @@ -4999,6 +5030,17 @@ This is required on creation to enable a user-flow of validating a template work { "dry_run": true, "log_level": "debug", + "on_success": { + "rich_parameter_values": [ + { + "name": "string", + "value": "string" + } + ], + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", + "transition": "start" + }, "orphan": true, "reason": "dashboard", "rich_parameter_values": [ @@ -5018,17 +5060,18 @@ This is required on creation to enable a user-flow of validating a template work ### Properties -| Name | Type | Required | Restrictions | Description | -|------------------------------|-------------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `dry_run` | boolean | false | | | -| `log_level` | [codersdk.ProvisionerLogLevel](#codersdkprovisionerloglevel) | false | | Log level changes the default logging verbosity of a provider ("info" if empty). | -| `orphan` | boolean | false | | Orphan may be set for the Destroy transition. | -| `reason` | [codersdk.CreateWorkspaceBuildReason](#codersdkcreateworkspacebuildreason) | false | | Reason sets the reason for the workspace build. | -| `rich_parameter_values` | array of [codersdk.WorkspaceBuildParameter](#codersdkworkspacebuildparameter) | false | | Rich parameter values are optional. It will write params to the 'workspace' scope. This will overwrite any existing parameters with the same name. This will not delete old params not included in this list. | -| `state` | array of integer | false | | | -| `template_version_id` | string | false | | | -| `template_version_preset_id` | string | false | | Template version preset ID is the ID of the template version preset to use for the build. | -| `transition` | [codersdk.WorkspaceTransition](#codersdkworkspacetransition) | true | | | +| Name | Type | Required | Restrictions | Description | +|------------------------------|------------------------------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `dry_run` | boolean | false | | | +| `log_level` | [codersdk.ProvisionerLogLevel](#codersdkprovisionerloglevel) | false | | Log level changes the default logging verbosity of a provider ("info" if empty). | +| `on_success` | [codersdk.CreateWorkspaceBuildOnSuccessRequest](#codersdkcreateworkspacebuildonsuccessrequest) | false | | On success queues a follow-up workspace build after this build succeeds. It currently supports restarting a workspace by starting it after a successful stop build. | +| `orphan` | boolean | false | | Orphan may be set for the Destroy transition. | +| `reason` | [codersdk.CreateWorkspaceBuildReason](#codersdkcreateworkspacebuildreason) | false | | Reason sets the reason for the workspace build. | +| `rich_parameter_values` | array of [codersdk.WorkspaceBuildParameter](#codersdkworkspacebuildparameter) | false | | Rich parameter values are optional. It will write params to the 'workspace' scope. This will overwrite any existing parameters with the same name. This will not delete old params not included in this list. | +| `state` | array of integer | false | | | +| `template_version_id` | string | false | | | +| `template_version_preset_id` | string | false | | Template version preset ID is the ID of the template version preset to use for the build. | +| `transition` | [codersdk.WorkspaceTransition](#codersdkworkspacetransition) | true | | | #### Enumerated Values diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index a7a9c3ad26d..a290bf8d7f1 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -3732,6 +3732,41 @@ export interface CreateUserSkillRequest { readonly content: string; } +// From codersdk/workspaces.go +/** + * CreateWorkspaceBuildOnSuccessRequest queues a follow-up build that + * runs after the parent build succeeds. It currently supports + * restarting a workspace: the parent build must be a "stop" and this + * child build a "start". The child build inherits LogLevel and Reason + * from the parent CreateWorkspaceBuildRequest. + */ +export interface CreateWorkspaceBuildOnSuccessRequest { + /** + * TemplateVersionID pins the child build to a specific template + * version. Pinning requires permission to update the template, + * since the active version may change before the child build + * runs. When empty, the child build uses the template's active + * version at the time it runs. + */ + readonly template_version_id?: string; + /** + * Transition must be "start". The parent build's transition must + * be "stop". + */ + readonly transition: WorkspaceTransition; + /** + * RichParameterValues are applied to the child build. Parameters + * not listed here fall back to their values from the previous + * build, matching normal build behavior. + */ + readonly rich_parameter_values?: readonly WorkspaceBuildParameter[]; + /** + * TemplateVersionPresetID selects a preset for the child build. + * It requires TemplateVersionID to also be set. + */ + readonly template_version_preset_id?: string; +} + // From codersdk/workspaces.go export type CreateWorkspaceBuildReason = | "cli" @@ -3783,6 +3818,12 @@ export interface CreateWorkspaceBuildRequest { * Reason sets the reason for the workspace build. */ readonly reason?: CreateWorkspaceBuildReason; + /** + * OnSuccess queues a follow-up workspace build after this build succeeds. + * It currently supports restarting a workspace by starting it after a + * successful stop build. + */ + readonly on_success?: CreateWorkspaceBuildOnSuccessRequest; } // From codersdk/workspaceproxy.go From c0946fb2fa458245ee4a0ef687923c16f873de35 Mon Sep 17 00:00:00 2001 From: George Katsitadze Date: Mon, 6 Jul 2026 15:02:39 -0700 Subject: [PATCH 11/13] add explicit comment about bypassing authz when fetching authz context --- coderd/database/dbauthz/dbauthz.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index bd5ae625852..aa5942c8796 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -6407,6 +6407,8 @@ func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertW } 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) From 637ce51c89d00624f11c42732190ea9d9bb2a7f8 Mon Sep 17 00:00:00 2001 From: George Katsitadze Date: Mon, 6 Jul 2026 15:18:24 -0700 Subject: [PATCH 12/13] bump migration number --- ...ns.down.sql => 000540_workspace_build_orchestrations.down.sql} | 0 ...ations.up.sql => 000540_workspace_build_orchestrations.up.sql} | 0 ...ations.up.sql => 000540_workspace_build_orchestrations.up.sql} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000526_workspace_build_orchestrations.down.sql => 000540_workspace_build_orchestrations.down.sql} (100%) rename coderd/database/migrations/{000526_workspace_build_orchestrations.up.sql => 000540_workspace_build_orchestrations.up.sql} (100%) rename coderd/database/migrations/testdata/fixtures/{000526_workspace_build_orchestrations.up.sql => 000540_workspace_build_orchestrations.up.sql} (100%) diff --git a/coderd/database/migrations/000526_workspace_build_orchestrations.down.sql b/coderd/database/migrations/000540_workspace_build_orchestrations.down.sql similarity index 100% rename from coderd/database/migrations/000526_workspace_build_orchestrations.down.sql rename to coderd/database/migrations/000540_workspace_build_orchestrations.down.sql diff --git a/coderd/database/migrations/000526_workspace_build_orchestrations.up.sql b/coderd/database/migrations/000540_workspace_build_orchestrations.up.sql similarity index 100% rename from coderd/database/migrations/000526_workspace_build_orchestrations.up.sql rename to coderd/database/migrations/000540_workspace_build_orchestrations.up.sql diff --git a/coderd/database/migrations/testdata/fixtures/000526_workspace_build_orchestrations.up.sql b/coderd/database/migrations/testdata/fixtures/000540_workspace_build_orchestrations.up.sql similarity index 100% rename from coderd/database/migrations/testdata/fixtures/000526_workspace_build_orchestrations.up.sql rename to coderd/database/migrations/testdata/fixtures/000540_workspace_build_orchestrations.up.sql From 395657fb07b926eae9a409865885ff31e811c760 Mon Sep 17 00:00:00 2001 From: George Katsitadze Date: Mon, 6 Jul 2026 17:40:25 -0700 Subject: [PATCH 13/13] fix: give the workspace build orchestrator a real clock The orchestrator was constructed with api.Clock and starts an always-on backup-poll clock.NewTicker on it. In tests that drive the mock clock deterministically (the autobuild lifecycle tests), that shared ticker makes TestWorkspaceAutobuild/NextStartAtIsValid fail date-dependently: NextStartAt fails to recompute past the current tick. The orchestrator is driven by pubsub wakes; the ticker is only a backup poll, so a real clock is correct and keeps its ticker off the mock clock. Empirically this resolves the failure; the wake-driven path is unaffected (it fires in real time). --- coderd/coderd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 45f444cbe9c..3bd31f4460d 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -978,7 +978,7 @@ func New(options *Options) *API { DeploymentValues: options.DeploymentValues, Experiments: api.Experiments, BuilderMetrics: options.WorkspaceBuilderMetrics, - Clock: api.Clock, + Clock: quartz.NewReal(), }) api.workspaceBuildOrchestrator.Start(api.ctx)