From 401aa58eebe422c27aa13ea54d38e81f256d3c53 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Mon, 22 Jun 2026 10:59:43 -0500 Subject: [PATCH] feat: add schema changes for autostop notification (#26417) --- coderd/database/dump.sql | 10 ++- ...8_workspace_autostop_notification.down.sql | 51 ++++++++++++++ ...528_workspace_autostop_notification.up.sql | 68 +++++++++++++++++++ coderd/database/modelqueries.go | 1 + coderd/database/models.go | 44 ++++++------ coderd/database/queries.sql.go | 41 +++++++---- docs/admin/security/audit-logs.md | 4 +- enterprise/audit/table.go | 2 + 8 files changed, 185 insertions(+), 36 deletions(-) create mode 100644 coderd/database/migrations/000528_workspace_autostop_notification.down.sql create mode 100644 coderd/database/migrations/000528_workspace_autostop_notification.up.sql diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 74ccc1890f..4b4a0cd193 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -2972,9 +2972,12 @@ CREATE TABLE workspace_builds ( template_version_preset_id uuid, has_ai_task boolean, has_external_agent boolean, + notified_autostop_deadline timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL, CONSTRAINT workspace_builds_deadline_below_max_deadline CHECK ((((deadline <> '0001-01-01 00:00:00+00'::timestamp with time zone) AND (deadline <= max_deadline)) OR (max_deadline = '0001-01-01 00:00:00+00'::timestamp with time zone))) ); +COMMENT ON COLUMN workspace_builds.notified_autostop_deadline IS 'The autostop deadline value that an autostop reminder notification was last sent for. Used for idempotence: when it equals the build deadline the reminder has already been sent, and it re-arms automatically when the deadline changes.'; + CREATE TABLE workspaces ( id uuid NOT NULL, created_at timestamp with time zone NOT NULL, @@ -3347,7 +3350,8 @@ CREATE TABLE templates ( max_port_sharing_level app_sharing_level DEFAULT 'owner'::app_sharing_level NOT NULL, use_classic_parameter_flow boolean DEFAULT false NOT NULL, cors_behavior cors_behavior DEFAULT 'simple'::cors_behavior NOT NULL, - disable_module_cache boolean DEFAULT false NOT NULL + disable_module_cache boolean DEFAULT false NOT NULL, + time_til_autostop_notify bigint DEFAULT 0 NOT NULL ); COMMENT ON COLUMN templates.default_ttl IS 'The default duration for autostop for workspaces created from this template.'; @@ -3370,6 +3374,8 @@ COMMENT ON COLUMN templates.deprecated IS 'If set to a non empty string, the tem COMMENT ON COLUMN templates.use_classic_parameter_flow IS 'Determines whether to default to the dynamic parameter creation flow for this template or continue using the legacy classic parameter creation flow.This is a template wide setting, the template admin can revert to the classic flow if there are any issues. An escape hatch is required, as workspace creation is a core workflow and cannot break. This column will be removed when the dynamic parameter creation flow is stable.'; +COMMENT ON COLUMN templates.time_til_autostop_notify IS 'How long before the workspace autostop deadline to send a reminder notification, in nanoseconds. 0 disables the notification.'; + CREATE VIEW template_with_names AS SELECT templates.id, templates.created_at, @@ -3402,6 +3408,7 @@ CREATE VIEW template_with_names AS templates.use_classic_parameter_flow, templates.cors_behavior, templates.disable_module_cache, + templates.time_til_autostop_notify, COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url, COALESCE(visible_users.username, ''::text) AS created_by_username, COALESCE(visible_users.name, ''::text) AS created_by_name, @@ -3858,6 +3865,7 @@ CREATE VIEW workspace_build_with_user AS workspace_builds.template_version_preset_id, workspace_builds.has_ai_task, workspace_builds.has_external_agent, + workspace_builds.notified_autostop_deadline, COALESCE(visible_users.avatar_url, ''::text) AS initiator_by_avatar_url, COALESCE(visible_users.username, ''::text) AS initiator_by_username, COALESCE(visible_users.name, ''::text) AS initiator_by_name diff --git a/coderd/database/migrations/000528_workspace_autostop_notification.down.sql b/coderd/database/migrations/000528_workspace_autostop_notification.down.sql new file mode 100644 index 0000000000..96fd3b6fcb --- /dev/null +++ b/coderd/database/migrations/000528_workspace_autostop_notification.down.sql @@ -0,0 +1,51 @@ +DELETE FROM notification_templates WHERE id = '6f6cb984-c167-4fa5-bb87-1058dd642779'; + +DROP VIEW workspace_build_with_user; + +ALTER TABLE workspace_builds DROP COLUMN notified_autostop_deadline; + +CREATE VIEW workspace_build_with_user AS +SELECT + workspace_builds.id, + workspace_builds.created_at, + workspace_builds.updated_at, + workspace_builds.workspace_id, + workspace_builds.template_version_id, + workspace_builds.build_number, + workspace_builds.transition, + workspace_builds.initiator_id, + workspace_builds.job_id, + workspace_builds.deadline, + workspace_builds.reason, + workspace_builds.daily_cost, + workspace_builds.max_deadline, + workspace_builds.template_version_preset_id, + workspace_builds.has_ai_task, + workspace_builds.has_external_agent, + COALESCE(visible_users.avatar_url, ''::text) AS initiator_by_avatar_url, + COALESCE(visible_users.username, ''::text) AS initiator_by_username, + COALESCE(visible_users.name, ''::text) AS initiator_by_name +FROM + workspace_builds +LEFT JOIN + visible_users ON workspace_builds.initiator_id = visible_users.id; + +COMMENT ON VIEW workspace_build_with_user IS 'Joins in the username + avatar url of the initiated by user.'; + +DROP VIEW template_with_names; + +ALTER TABLE templates DROP COLUMN time_til_autostop_notify; + +CREATE VIEW template_with_names AS +SELECT templates.*, + COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url, + COALESCE(visible_users.username, ''::text) AS created_by_username, + COALESCE(visible_users.name, ''::text) AS created_by_name, + COALESCE(organizations.name, ''::text) AS organization_name, + COALESCE(organizations.display_name, ''::text) AS organization_display_name, + COALESCE(organizations.icon, ''::text) AS organization_icon +FROM ((templates + LEFT JOIN visible_users ON ((templates.created_by = visible_users.id))) + LEFT JOIN organizations ON ((templates.organization_id = organizations.id))); + +COMMENT ON VIEW template_with_names IS 'Joins in the display name information such as username, avatar, and organization name.'; diff --git a/coderd/database/migrations/000528_workspace_autostop_notification.up.sql b/coderd/database/migrations/000528_workspace_autostop_notification.up.sql new file mode 100644 index 0000000000..9d7ed1ef55 --- /dev/null +++ b/coderd/database/migrations/000528_workspace_autostop_notification.up.sql @@ -0,0 +1,68 @@ +ALTER TABLE templates ADD COLUMN time_til_autostop_notify bigint DEFAULT 0 NOT NULL; + +COMMENT ON COLUMN templates.time_til_autostop_notify IS 'How long before the workspace autostop deadline to send a reminder notification, in nanoseconds. 0 disables the notification.'; + +DROP VIEW template_with_names; + +CREATE VIEW template_with_names AS +SELECT templates.*, + COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url, + COALESCE(visible_users.username, ''::text) AS created_by_username, + COALESCE(visible_users.name, ''::text) AS created_by_name, + COALESCE(organizations.name, ''::text) AS organization_name, + COALESCE(organizations.display_name, ''::text) AS organization_display_name, + COALESCE(organizations.icon, ''::text) AS organization_icon +FROM ((templates + LEFT JOIN visible_users ON ((templates.created_by = visible_users.id))) + LEFT JOIN organizations ON ((templates.organization_id = organizations.id))); + +COMMENT ON VIEW template_with_names IS 'Joins in the display name information such as username, avatar, and organization name.'; + +ALTER TABLE workspace_builds ADD COLUMN notified_autostop_deadline timestamptz DEFAULT '0001-01-01 00:00:00+00' NOT NULL; + +COMMENT ON COLUMN workspace_builds.notified_autostop_deadline IS 'The autostop deadline value that an autostop reminder notification was last sent for. Used for idempotence: when it equals the build deadline the reminder has already been sent, and it re-arms automatically when the deadline changes.'; + +DROP VIEW workspace_build_with_user; + +CREATE VIEW workspace_build_with_user AS +SELECT + workspace_builds.id, + workspace_builds.created_at, + workspace_builds.updated_at, + workspace_builds.workspace_id, + workspace_builds.template_version_id, + workspace_builds.build_number, + workspace_builds.transition, + workspace_builds.initiator_id, + workspace_builds.job_id, + workspace_builds.deadline, + workspace_builds.reason, + workspace_builds.daily_cost, + workspace_builds.max_deadline, + workspace_builds.template_version_preset_id, + workspace_builds.has_ai_task, + workspace_builds.has_external_agent, + workspace_builds.notified_autostop_deadline, + COALESCE(visible_users.avatar_url, ''::text) AS initiator_by_avatar_url, + COALESCE(visible_users.username, ''::text) AS initiator_by_username, + COALESCE(visible_users.name, ''::text) AS initiator_by_name +FROM + workspace_builds +LEFT JOIN + visible_users ON workspace_builds.initiator_id = visible_users.id; + +COMMENT ON VIEW workspace_build_with_user IS 'Joins in the username + avatar url of the initiated by user.'; + +INSERT INTO notification_templates ( + id, name, title_template, body_template, actions, "group", method, kind, enabled_by_default +) VALUES ( + '6f6cb984-c167-4fa5-bb87-1058dd642779', + 'Workspace Autostop Reminder', + E'Your workspace "{{.Labels.workspace}}" will stop soon', + E'Your workspace **{{.Labels.workspace}}** is scheduled to automatically stop at {{.Labels.deadline}}.\n\nConnect to it or extend the deadline to keep it running.', + '[{"label": "View workspace", "url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}"}]'::jsonb, + 'Workspace Events', + NULL, + 'system'::notification_template_kind, + true +); diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 0810ef21b1..0835527b5d 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -129,6 +129,7 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate &i.UseClassicParameterFlow, &i.CorsBehavior, &i.DisableModuleCache, + &i.TimeTilAutostopNotify, &i.CreatedByAvatarURL, &i.CreatedByUsername, &i.CreatedByName, diff --git a/coderd/database/models.go b/coderd/database/models.go index 1f2965caa4..89ad03a33d 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -5706,6 +5706,7 @@ type Template struct { UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"` CorsBehavior CorsBehavior `db:"cors_behavior" json:"cors_behavior"` DisableModuleCache bool `db:"disable_module_cache" json:"disable_module_cache"` + TimeTilAutostopNotify int64 `db:"time_til_autostop_notify" json:"time_til_autostop_notify"` CreatedByAvatarURL string `db:"created_by_avatar_url" json:"created_by_avatar_url"` CreatedByUsername string `db:"created_by_username" json:"created_by_username"` CreatedByName string `db:"created_by_name" json:"created_by_name"` @@ -5756,6 +5757,8 @@ type TemplateTable struct { UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"` CorsBehavior CorsBehavior `db:"cors_behavior" json:"cors_behavior"` DisableModuleCache bool `db:"disable_module_cache" json:"disable_module_cache"` + // How long before the workspace autostop deadline to send a reminder notification, in nanoseconds. 0 disables the notification. + TimeTilAutostopNotify int64 `db:"time_til_autostop_notify" json:"time_til_autostop_notify"` } // Records aggregated usage statistics for templates/users. All usage is rounded up to the nearest minute. @@ -6413,25 +6416,26 @@ type WorkspaceAppStatus struct { // Joins in the username + avatar url of the initiated by user. type WorkspaceBuild 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"` - WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` - TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` - BuildNumber int32 `db:"build_number" json:"build_number"` - Transition WorkspaceTransition `db:"transition" json:"transition"` - InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"` - JobID uuid.UUID `db:"job_id" json:"job_id"` - Deadline time.Time `db:"deadline" json:"deadline"` - Reason BuildReason `db:"reason" json:"reason"` - DailyCost int32 `db:"daily_cost" json:"daily_cost"` - MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"` - TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"` - HasAITask sql.NullBool `db:"has_ai_task" json:"has_ai_task"` - HasExternalAgent sql.NullBool `db:"has_external_agent" json:"has_external_agent"` - InitiatorByAvatarUrl string `db:"initiator_by_avatar_url" json:"initiator_by_avatar_url"` - InitiatorByUsername string `db:"initiator_by_username" json:"initiator_by_username"` - InitiatorByName string `db:"initiator_by_name" json:"initiator_by_name"` + 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"` + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` + BuildNumber int32 `db:"build_number" json:"build_number"` + Transition WorkspaceTransition `db:"transition" json:"transition"` + InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"` + JobID uuid.UUID `db:"job_id" json:"job_id"` + Deadline time.Time `db:"deadline" json:"deadline"` + Reason BuildReason `db:"reason" json:"reason"` + DailyCost int32 `db:"daily_cost" json:"daily_cost"` + MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"` + TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"` + HasAITask sql.NullBool `db:"has_ai_task" json:"has_ai_task"` + HasExternalAgent sql.NullBool `db:"has_external_agent" json:"has_external_agent"` + NotifiedAutostopDeadline time.Time `db:"notified_autostop_deadline" json:"notified_autostop_deadline"` + InitiatorByAvatarUrl string `db:"initiator_by_avatar_url" json:"initiator_by_avatar_url"` + InitiatorByUsername string `db:"initiator_by_username" json:"initiator_by_username"` + InitiatorByName string `db:"initiator_by_name" json:"initiator_by_name"` } type WorkspaceBuildParameter struct { @@ -6460,6 +6464,8 @@ type WorkspaceBuildTable struct { TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"` HasAITask sql.NullBool `db:"has_ai_task" json:"has_ai_task"` HasExternalAgent sql.NullBool `db:"has_external_agent" json:"has_external_agent"` + // The autostop deadline value that an autostop reminder notification was last sent for. Used for idempotence: when it equals the build deadline the reminder has already been sent, and it re-arms automatically when the deadline changes. + NotifiedAutostopDeadline time.Time `db:"notified_autostop_deadline" json:"notified_autostop_deadline"` } type WorkspaceLatestBuild struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index f8e6589576..fdf4425493 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -26425,7 +26425,7 @@ func (q *sqlQuerier) GetTemplateAverageBuildTime(ctx context.Context, templateID const getTemplateByID = `-- name: GetTemplateByID :one SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon FROM template_with_names WHERE @@ -26469,6 +26469,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat &i.UseClassicParameterFlow, &i.CorsBehavior, &i.DisableModuleCache, + &i.TimeTilAutostopNotify, &i.CreatedByAvatarURL, &i.CreatedByUsername, &i.CreatedByName, @@ -26481,7 +26482,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon FROM template_with_names AS templates WHERE @@ -26533,6 +26534,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G &i.UseClassicParameterFlow, &i.CorsBehavior, &i.DisableModuleCache, + &i.TimeTilAutostopNotify, &i.CreatedByAvatarURL, &i.CreatedByUsername, &i.CreatedByName, @@ -26544,7 +26546,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G } const getTemplates = `-- name: GetTemplates :many -SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon FROM template_with_names AS templates +SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon FROM template_with_names AS templates ORDER BY (name, id) ASC ` @@ -26589,6 +26591,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) { &i.UseClassicParameterFlow, &i.CorsBehavior, &i.DisableModuleCache, + &i.TimeTilAutostopNotify, &i.CreatedByAvatarURL, &i.CreatedByUsername, &i.CreatedByName, @@ -26611,7 +26614,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) { const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many SELECT - t.id, t.created_at, t.updated_at, t.organization_id, t.deleted, t.name, t.provisioner, t.active_version_id, t.description, t.default_ttl, t.created_by, t.icon, t.user_acl, t.group_acl, t.display_name, t.allow_user_cancel_workspace_jobs, t.allow_user_autostart, t.allow_user_autostop, t.failure_ttl, t.time_til_dormant, t.time_til_dormant_autodelete, t.autostop_requirement_days_of_week, t.autostop_requirement_weeks, t.autostart_block_days_of_week, t.require_active_version, t.deprecated, t.activity_bump, t.max_port_sharing_level, t.use_classic_parameter_flow, t.cors_behavior, t.disable_module_cache, t.created_by_avatar_url, t.created_by_username, t.created_by_name, t.organization_name, t.organization_display_name, t.organization_icon + t.id, t.created_at, t.updated_at, t.organization_id, t.deleted, t.name, t.provisioner, t.active_version_id, t.description, t.default_ttl, t.created_by, t.icon, t.user_acl, t.group_acl, t.display_name, t.allow_user_cancel_workspace_jobs, t.allow_user_autostart, t.allow_user_autostop, t.failure_ttl, t.time_til_dormant, t.time_til_dormant_autodelete, t.autostop_requirement_days_of_week, t.autostop_requirement_weeks, t.autostart_block_days_of_week, t.require_active_version, t.deprecated, t.activity_bump, t.max_port_sharing_level, t.use_classic_parameter_flow, t.cors_behavior, t.disable_module_cache, t.time_til_autostop_notify, t.created_by_avatar_url, t.created_by_username, t.created_by_name, t.organization_name, t.organization_display_name, t.organization_icon FROM template_with_names AS t LEFT JOIN @@ -26771,6 +26774,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate &i.UseClassicParameterFlow, &i.CorsBehavior, &i.DisableModuleCache, + &i.TimeTilAutostopNotify, &i.CreatedByAvatarURL, &i.CreatedByUsername, &i.CreatedByName, @@ -32166,7 +32170,7 @@ const getAuthenticatedWorkspaceAgentAndBuildByAuthToken = `-- name: GetAuthentic SELECT workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl, workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted, - workspace_build_with_user.id, workspace_build_with_user.created_at, workspace_build_with_user.updated_at, workspace_build_with_user.workspace_id, workspace_build_with_user.template_version_id, workspace_build_with_user.build_number, workspace_build_with_user.transition, workspace_build_with_user.initiator_id, workspace_build_with_user.job_id, workspace_build_with_user.deadline, workspace_build_with_user.reason, workspace_build_with_user.daily_cost, workspace_build_with_user.max_deadline, workspace_build_with_user.template_version_preset_id, workspace_build_with_user.has_ai_task, workspace_build_with_user.has_external_agent, workspace_build_with_user.initiator_by_avatar_url, workspace_build_with_user.initiator_by_username, workspace_build_with_user.initiator_by_name, + workspace_build_with_user.id, workspace_build_with_user.created_at, workspace_build_with_user.updated_at, workspace_build_with_user.workspace_id, workspace_build_with_user.template_version_id, workspace_build_with_user.build_number, workspace_build_with_user.transition, workspace_build_with_user.initiator_id, workspace_build_with_user.job_id, workspace_build_with_user.deadline, workspace_build_with_user.reason, workspace_build_with_user.daily_cost, workspace_build_with_user.max_deadline, workspace_build_with_user.template_version_preset_id, workspace_build_with_user.has_ai_task, workspace_build_with_user.has_external_agent, workspace_build_with_user.notified_autostop_deadline, workspace_build_with_user.initiator_by_avatar_url, workspace_build_with_user.initiator_by_username, workspace_build_with_user.initiator_by_name, tasks.id AS task_id FROM workspace_agents @@ -32312,6 +32316,7 @@ func (q *sqlQuerier) GetAuthenticatedWorkspaceAgentAndBuildByAuthToken(ctx conte &i.WorkspaceBuild.TemplateVersionPresetID, &i.WorkspaceBuild.HasAITask, &i.WorkspaceBuild.HasExternalAgent, + &i.WorkspaceBuild.NotifiedAutostopDeadline, &i.WorkspaceBuild.InitiatorByAvatarUrl, &i.WorkspaceBuild.InitiatorByUsername, &i.WorkspaceBuild.InitiatorByName, @@ -35449,7 +35454,7 @@ func (q *sqlQuerier) InsertWorkspaceBuildParameters(ctx context.Context, arg Ins } const getActiveWorkspaceBuildsByTemplateID = `-- name: GetActiveWorkspaceBuildsByTemplateID :many -SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.template_version_preset_id, wb.has_ai_task, wb.has_external_agent, wb.initiator_by_avatar_url, wb.initiator_by_username, wb.initiator_by_name +SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.template_version_preset_id, wb.has_ai_task, wb.has_external_agent, wb.notified_autostop_deadline, wb.initiator_by_avatar_url, wb.initiator_by_username, wb.initiator_by_name FROM ( SELECT workspace_id, MAX(build_number) as max_build_number @@ -35505,6 +35510,7 @@ func (q *sqlQuerier) GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, t &i.TemplateVersionPresetID, &i.HasAITask, &i.HasExternalAgent, + &i.NotifiedAutostopDeadline, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, &i.InitiatorByName, @@ -35604,7 +35610,7 @@ func (q *sqlQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, a const getLatestWorkspaceBuildByWorkspaceID = `-- name: GetLatestWorkspaceBuildByWorkspaceID :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user AS workspace_builds WHERE @@ -35635,6 +35641,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, w &i.TemplateVersionPresetID, &i.HasAITask, &i.HasExternalAgent, + &i.NotifiedAutostopDeadline, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, &i.InitiatorByName, @@ -35700,7 +35707,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildWithStatusByWorkspaceID(ctx context. const getLatestWorkspaceBuildsByWorkspaceIDs = `-- name: GetLatestWorkspaceBuildsByWorkspaceIDs :many SELECT DISTINCT ON (workspace_id) - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user AS workspace_builds WHERE @@ -35735,6 +35742,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, &i.TemplateVersionPresetID, &i.HasAITask, &i.HasExternalAgent, + &i.NotifiedAutostopDeadline, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, &i.InitiatorByName, @@ -35754,7 +35762,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, const getWorkspaceBuildByID = `-- name: GetWorkspaceBuildByID :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user AS workspace_builds WHERE @@ -35783,6 +35791,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (W &i.TemplateVersionPresetID, &i.HasAITask, &i.HasExternalAgent, + &i.NotifiedAutostopDeadline, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, &i.InitiatorByName, @@ -35792,7 +35801,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (W const getWorkspaceBuildByJobID = `-- name: GetWorkspaceBuildByJobID :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user AS workspace_builds WHERE @@ -35821,6 +35830,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UU &i.TemplateVersionPresetID, &i.HasAITask, &i.HasExternalAgent, + &i.NotifiedAutostopDeadline, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, &i.InitiatorByName, @@ -35830,7 +35840,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UU const getWorkspaceBuildByWorkspaceIDAndBuildNumber = `-- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user AS workspace_builds WHERE @@ -35863,6 +35873,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Co &i.TemplateVersionPresetID, &i.HasAITask, &i.HasExternalAgent, + &i.NotifiedAutostopDeadline, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, &i.InitiatorByName, @@ -36037,7 +36048,7 @@ func (q *sqlQuerier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, sinc const getWorkspaceBuildsByWorkspaceID = `-- name: GetWorkspaceBuildsByWorkspaceID :many SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user AS workspace_builds WHERE @@ -36109,6 +36120,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg Ge &i.TemplateVersionPresetID, &i.HasAITask, &i.HasExternalAgent, + &i.NotifiedAutostopDeadline, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, &i.InitiatorByName, @@ -36127,7 +36139,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg Ge } const getWorkspaceBuildsCreatedAfter = `-- name: GetWorkspaceBuildsCreatedAfter :many -SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user WHERE created_at > $1 +SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user WHERE created_at > $1 ` func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) { @@ -36156,6 +36168,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, created &i.TemplateVersionPresetID, &i.HasAITask, &i.HasExternalAgent, + &i.NotifiedAutostopDeadline, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, &i.InitiatorByName, @@ -37579,7 +37592,7 @@ LEFT JOIN LATERAL ( ) latest_build ON TRUE LEFT JOIN LATERAL ( SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify FROM templates WHERE diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md index a41fc4c9a5..62b0302dd1 100644 --- a/docs/admin/security/audit-logs.md +++ b/docs/admin/security/audit-logs.md @@ -40,12 +40,12 @@ We track the following resources: | PrebuildsSettings
| |
FieldTracked
idfalse
reconciliation_pausedtrue
| | RoleSyncSettings
| |
FieldTracked
fieldtrue
mappingtrue
| | TaskTable
| |
FieldTracked
created_atfalse
deleted_atfalse
display_nametrue
idtrue
nametrue
organization_idfalse
owner_idtrue
prompttrue
template_parameterstrue
template_version_idtrue
workspace_idtrue
| -| Template
write, delete | |
FieldTracked
active_version_idtrue
activity_bumptrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
cors_behaviortrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_namefalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
disable_module_cachetrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_port_sharing_leveltrue
nametrue
organization_display_namefalse
organization_iconfalse
organization_idfalse
organization_namefalse
provisionertrue
require_active_versiontrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
use_classic_parameter_flowtrue
user_acltrue
| +| Template
write, delete | |
FieldTracked
active_version_idtrue
activity_bumptrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
cors_behaviortrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_namefalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
disable_module_cachetrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_port_sharing_leveltrue
nametrue
organization_display_namefalse
organization_iconfalse
organization_idfalse
organization_namefalse
provisionertrue
require_active_versiontrue
time_til_autostop_notifytrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
use_classic_parameter_flowtrue
user_acltrue
| | TemplateVersion
create, write | |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_namefalse
created_by_usernamefalse
external_auth_providersfalse
has_ai_taskfalse
has_external_agentfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
source_example_idfalse
template_idtrue
updated_atfalse
| | User
create, write, delete | |
FieldTracked
avatar_urlfalse
chat_spend_limit_microstrue
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodefalse
hashed_passwordtrue
idtrue
is_service_accounttrue
is_systemtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| | UserSecret
create, write, delete | |
FieldTracked
created_atfalse
descriptiontrue
env_nametrue
file_pathtrue
idtrue
nametrue
updated_atfalse
user_idtrue
valuetrue
value_key_idfalse
| | UserSkill
create, write, delete | |
FieldTracked
contenttrue
created_atfalse
descriptiontrue
idtrue
nametrue
updated_atfalse
user_idtrue
| -| WorkspaceBuild
start, stop | |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
has_ai_taskfalse
has_external_agentfalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_namefalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
reasonfalse
template_version_idtrue
template_version_preset_idfalse
transitionfalse
updated_atfalse
workspace_idfalse
| +| WorkspaceBuild
start, stop | |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
has_ai_taskfalse
has_external_agentfalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_namefalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
notified_autostop_deadlinefalse
reasonfalse
template_version_idtrue
template_version_preset_idfalse
transitionfalse
updated_atfalse
workspace_idfalse
| | WorkspaceProxy
| |
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
versiontrue
wildcard_hostnametrue
| | WorkspaceTable
| |
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
favoritetrue
group_acltrue
idtrue
last_used_atfalse
nametrue
next_start_attrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
user_acltrue
| diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index d509581b63..f88c50b86e 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -130,6 +130,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ "use_classic_parameter_flow": ActionTrack, "cors_behavior": ActionTrack, "disable_module_cache": ActionTrack, + "time_til_autostop_notify": ActionTrack, }, &database.TemplateVersion{}: { "id": ActionTrack, @@ -213,6 +214,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ "template_version_preset_id": ActionIgnore, // Never changes. "has_ai_task": ActionIgnore, // Never changes. "has_external_agent": ActionIgnore, // Never changes. + "notified_autostop_deadline": ActionIgnore, // Updated by the notification system, not by user action. }, &database.AuditableGroup{}: { "id": ActionTrack,