diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 60340de45b8b5..eb30358ef918b 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -16488,6 +16488,14 @@ const docTemplate = `{ "operating_system": { "type": "string" }, + "parent_id": { + "format": "uuid", + "allOf": [ + { + "$ref": "#/definitions/uuid.NullUUID" + } + ] + }, "ready_at": { "type": "string", "format": "date-time" @@ -18492,6 +18500,18 @@ const docTemplate = `{ "url.Userinfo": { "type": "object" }, + "uuid.NullUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string" + }, + "valid": { + "description": "Valid is true if UUID is not NULL", + "type": "boolean" + } + } + }, "workspaceapps.AccessMethod": { "type": "string", "enum": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 93454fede9fe1..d21fccd71cf1d 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -15037,6 +15037,14 @@ "operating_system": { "type": "string" }, + "parent_id": { + "format": "uuid", + "allOf": [ + { + "$ref": "#/definitions/uuid.NullUUID" + } + ] + }, "ready_at": { "type": "string", "format": "date-time" @@ -16933,6 +16941,18 @@ "url.Userinfo": { "type": "object" }, + "uuid.NullUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string" + }, + "valid": { + "description": "Valid is true if UUID is not NULL", + "type": "boolean" + } + } + }, "workspaceapps.AccessMethod": { "type": "string", "enum": ["path", "subdomain", "terminal"], diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 80ce962637e52..604060414ba68 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -157,6 +157,7 @@ func WorkspaceAgentPortShare(t testing.TB, db database.Store, orig database.Work func WorkspaceAgent(t testing.TB, db database.Store, orig database.WorkspaceAgent) database.WorkspaceAgent { agt, err := db.InsertWorkspaceAgent(genCtx, database.InsertWorkspaceAgentParams{ ID: takeFirst(orig.ID, uuid.New()), + ParentID: takeFirst(orig.ParentID, uuid.NullUUID{}), CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()), UpdatedAt: takeFirst(orig.UpdatedAt, dbtime.Now()), Name: takeFirst(orig.Name, testutil.GetRandomName(t)), diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 362de7a7a02b7..e92de5d7cac7e 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -9477,6 +9477,7 @@ func (q *FakeQuerier) InsertWorkspaceAgent(_ context.Context, arg database.Inser agent := database.WorkspaceAgent{ ID: arg.ID, + ParentID: arg.ParentID, CreatedAt: arg.CreatedAt, UpdatedAt: arg.UpdatedAt, ResourceID: arg.ResourceID, diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index a56a0fd85d50f..0b1356ede11cc 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -487,9 +487,14 @@ BEGIN ); member_count := ( - SELECT count(*) as count FROM organization_members + SELECT + count(*) AS count + FROM + organization_members + LEFT JOIN users ON users.id = organization_members.user_id WHERE organization_members.organization_id = OLD.id + AND users.deleted = FALSE ); provisioner_keys_count := ( @@ -755,6 +760,32 @@ CREATE TABLE audit_logs ( resource_icon text NOT NULL ); +CREATE TABLE chat_messages ( + id bigint NOT NULL, + chat_id uuid NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + model text NOT NULL, + provider text NOT NULL, + content jsonb NOT NULL +); + +CREATE SEQUENCE chat_messages_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE chat_messages_id_seq OWNED BY chat_messages.id; + +CREATE TABLE chats ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + owner_id uuid NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + title text NOT NULL +); + CREATE TABLE crypto_keys ( feature crypto_key_feature NOT NULL, sequence integer NOT NULL, @@ -1414,9 +1445,13 @@ CREATE TABLE template_version_presets ( CREATE TABLE template_version_terraform_values ( template_version_id uuid NOT NULL, updated_at timestamp with time zone DEFAULT now() NOT NULL, - cached_plan jsonb NOT NULL + cached_plan jsonb NOT NULL, + cached_module_files uuid, + provisionerd_version text DEFAULT ''::text NOT NULL ); +COMMENT ON COLUMN template_version_terraform_values.provisionerd_version IS 'What version of the provisioning engine was used to generate the cached plan and module files.'; + CREATE TABLE template_version_variables ( template_version_id uuid NOT NULL, name text NOT NULL, @@ -1806,6 +1841,7 @@ CREATE TABLE workspace_agents ( display_apps display_app[] DEFAULT '{vscode,vscode_insiders,web_terminal,ssh_helper,port_forwarding_helper}'::display_app[], api_version text DEFAULT ''::text NOT NULL, display_order integer DEFAULT 0 NOT NULL, + parent_id uuid, api_key_scope agent_key_scope_enum DEFAULT 'all'::agent_key_scope_enum NOT NULL, CONSTRAINT max_logs_length CHECK ((logs_length <= 1048576)), CONSTRAINT subsystems_not_none CHECK ((NOT ('none'::workspace_agent_subsystem = ANY (subsystems)))) @@ -2211,6 +2247,8 @@ CREATE VIEW workspaces_expanded AS COMMENT ON VIEW workspaces_expanded IS 'Joins in the display name information such as username, avatar, and organization name.'; +ALTER TABLE ONLY chat_messages ALTER COLUMN id SET DEFAULT nextval('chat_messages_id_seq'::regclass); + ALTER TABLE ONLY licenses ALTER COLUMN id SET DEFAULT nextval('licenses_id_seq'::regclass); ALTER TABLE ONLY provisioner_job_logs ALTER COLUMN id SET DEFAULT nextval('provisioner_job_logs_id_seq'::regclass); @@ -2232,6 +2270,12 @@ ALTER TABLE ONLY api_keys ALTER TABLE ONLY audit_logs ADD CONSTRAINT audit_logs_pkey PRIMARY KEY (id); +ALTER TABLE ONLY chat_messages + ADD CONSTRAINT chat_messages_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY chats + ADD CONSTRAINT chats_pkey PRIMARY KEY (id); + ALTER TABLE ONLY crypto_keys ADD CONSTRAINT crypto_keys_pkey PRIMARY KEY (feature, sequence); @@ -2715,6 +2759,12 @@ CREATE TRIGGER user_status_change_trigger AFTER INSERT OR UPDATE ON users FOR EA ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; +ALTER TABLE ONLY chat_messages + ADD CONSTRAINT chat_messages_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; + +ALTER TABLE ONLY chats + ADD CONSTRAINT chats_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE; + ALTER TABLE ONLY crypto_keys ADD CONSTRAINT crypto_keys_secret_key_id_fkey FOREIGN KEY (secret_key_id) REFERENCES dbcrypt_keys(active_key_digest); @@ -2826,6 +2876,9 @@ ALTER TABLE ONLY template_version_preset_parameters 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; +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); + 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; @@ -2898,6 +2951,9 @@ ALTER TABLE ONLY workspace_agent_logs 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; +ALTER TABLE ONLY workspace_agents + ADD CONSTRAINT workspace_agents_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE; diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index 3f5ce963e6fdb..d6b87ddff5376 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -7,6 +7,8 @@ type ForeignKeyConstraint string // ForeignKeyConstraint enums. const ( 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; + 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; + 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; 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); 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); @@ -44,6 +46,7 @@ const ( 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; 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; @@ -68,6 +71,7 @@ const ( 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); diff --git a/coderd/database/migrations/000318_update_protect_deleting_orgs_to_filter_deleted_users.down.sql b/coderd/database/migrations/000318_update_protect_deleting_orgs_to_filter_deleted_users.down.sql new file mode 100644 index 0000000000000..cacafc029222c --- /dev/null +++ b/coderd/database/migrations/000318_update_protect_deleting_orgs_to_filter_deleted_users.down.sql @@ -0,0 +1,96 @@ +DROP TRIGGER IF EXISTS protect_deleting_organizations ON organizations; + +-- Replace the function with the new implementation +CREATE OR REPLACE FUNCTION protect_deleting_organizations() + RETURNS TRIGGER AS +$$ +DECLARE + workspace_count int; + template_count int; + group_count int; + member_count int; + provisioner_keys_count int; +BEGIN + workspace_count := ( + SELECT count(*) as count FROM workspaces + WHERE + workspaces.organization_id = OLD.id + AND workspaces.deleted = false + ); + + template_count := ( + SELECT count(*) as count FROM templates + WHERE + templates.organization_id = OLD.id + AND templates.deleted = false + ); + + group_count := ( + SELECT count(*) as count FROM groups + WHERE + groups.organization_id = OLD.id + ); + + member_count := ( + SELECT count(*) as count FROM organization_members + WHERE + organization_members.organization_id = OLD.id + ); + + provisioner_keys_count := ( + Select count(*) as count FROM provisioner_keys + WHERE + provisioner_keys.organization_id = OLD.id + ); + + -- Fail the deletion if one of the following: + -- * the organization has 1 or more workspaces + -- * the organization has 1 or more templates + -- * the organization has 1 or more groups other than "Everyone" group + -- * the organization has 1 or more members other than the organization owner + -- * the organization has 1 or more provisioner keys + + -- Only create error message for resources that actually exist + IF (workspace_count + template_count + provisioner_keys_count) > 0 THEN + DECLARE + error_message text := 'cannot delete organization: organization has '; + error_parts text[] := '{}'; + BEGIN + IF workspace_count > 0 THEN + error_parts := array_append(error_parts, workspace_count || ' workspaces'); + END IF; + + IF template_count > 0 THEN + error_parts := array_append(error_parts, template_count || ' templates'); + END IF; + + IF provisioner_keys_count > 0 THEN + error_parts := array_append(error_parts, provisioner_keys_count || ' provisioner keys'); + END IF; + + error_message := error_message || array_to_string(error_parts, ', ') || ' that must be deleted first'; + RAISE EXCEPTION '%', error_message; + END; + END IF; + + IF (group_count) > 1 THEN + RAISE EXCEPTION 'cannot delete organization: organization has % groups that must be deleted first', group_count - 1; + END IF; + + -- Allow 1 member to exist, because you cannot remove yourself. You can + -- remove everyone else. Ideally, we only omit the member that matches + -- the user_id of the caller, however in a trigger, the caller is unknown. + IF (member_count) > 1 THEN + RAISE EXCEPTION 'cannot delete organization: organization has % members that must be deleted first', member_count - 1; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Trigger to protect organizations from being soft deleted with existing resources +CREATE TRIGGER protect_deleting_organizations + BEFORE UPDATE ON organizations + FOR EACH ROW + WHEN (NEW.deleted = true AND OLD.deleted = false) + EXECUTE FUNCTION protect_deleting_organizations(); diff --git a/coderd/database/migrations/000318_update_protect_deleting_orgs_to_filter_deleted_users.up.sql b/coderd/database/migrations/000318_update_protect_deleting_orgs_to_filter_deleted_users.up.sql new file mode 100644 index 0000000000000..8db15223d92f1 --- /dev/null +++ b/coderd/database/migrations/000318_update_protect_deleting_orgs_to_filter_deleted_users.up.sql @@ -0,0 +1,101 @@ +DROP TRIGGER IF EXISTS protect_deleting_organizations ON organizations; + +-- Replace the function with the new implementation +CREATE OR REPLACE FUNCTION protect_deleting_organizations() + RETURNS TRIGGER AS +$$ +DECLARE + workspace_count int; + template_count int; + group_count int; + member_count int; + provisioner_keys_count int; +BEGIN + workspace_count := ( + SELECT count(*) as count FROM workspaces + WHERE + workspaces.organization_id = OLD.id + AND workspaces.deleted = false + ); + + template_count := ( + SELECT count(*) as count FROM templates + WHERE + templates.organization_id = OLD.id + AND templates.deleted = false + ); + + group_count := ( + SELECT count(*) as count FROM groups + WHERE + groups.organization_id = OLD.id + ); + + member_count := ( + SELECT + count(*) AS count + FROM + organization_members + LEFT JOIN users ON users.id = organization_members.user_id + WHERE + organization_members.organization_id = OLD.id + AND users.deleted = FALSE + ); + + provisioner_keys_count := ( + Select count(*) as count FROM provisioner_keys + WHERE + provisioner_keys.organization_id = OLD.id + ); + + -- Fail the deletion if one of the following: + -- * the organization has 1 or more workspaces + -- * the organization has 1 or more templates + -- * the organization has 1 or more groups other than "Everyone" group + -- * the organization has 1 or more members other than the organization owner + -- * the organization has 1 or more provisioner keys + + -- Only create error message for resources that actually exist + IF (workspace_count + template_count + provisioner_keys_count) > 0 THEN + DECLARE + error_message text := 'cannot delete organization: organization has '; + error_parts text[] := '{}'; + BEGIN + IF workspace_count > 0 THEN + error_parts := array_append(error_parts, workspace_count || ' workspaces'); + END IF; + + IF template_count > 0 THEN + error_parts := array_append(error_parts, template_count || ' templates'); + END IF; + + IF provisioner_keys_count > 0 THEN + error_parts := array_append(error_parts, provisioner_keys_count || ' provisioner keys'); + END IF; + + error_message := error_message || array_to_string(error_parts, ', ') || ' that must be deleted first'; + RAISE EXCEPTION '%', error_message; + END; + END IF; + + IF (group_count) > 1 THEN + RAISE EXCEPTION 'cannot delete organization: organization has % groups that must be deleted first', group_count - 1; + END IF; + + -- Allow 1 member to exist, because you cannot remove yourself. You can + -- remove everyone else. Ideally, we only omit the member that matches + -- the user_id of the caller, however in a trigger, the caller is unknown. + IF (member_count) > 1 THEN + RAISE EXCEPTION 'cannot delete organization: organization has % members that must be deleted first', member_count - 1; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Trigger to protect organizations from being soft deleted with existing resources +CREATE TRIGGER protect_deleting_organizations + BEFORE UPDATE ON organizations + FOR EACH ROW + WHEN (NEW.deleted = true AND OLD.deleted = false) + EXECUTE FUNCTION protect_deleting_organizations(); diff --git a/coderd/database/migrations/000319_chat.down.sql b/coderd/database/migrations/000319_chat.down.sql new file mode 100644 index 0000000000000..9bab993f500f5 --- /dev/null +++ b/coderd/database/migrations/000319_chat.down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS chat_messages; + +DROP TABLE IF EXISTS chats; diff --git a/coderd/database/migrations/000319_chat.up.sql b/coderd/database/migrations/000319_chat.up.sql new file mode 100644 index 0000000000000..a53942239c9e2 --- /dev/null +++ b/coderd/database/migrations/000319_chat.up.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS chats ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + title TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS chat_messages ( + -- BIGSERIAL is auto-incrementing so we know the exact order of messages. + id BIGSERIAL PRIMARY KEY, + chat_id UUID NOT NULL REFERENCES chats(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + model TEXT NOT NULL, + provider TEXT NOT NULL, + content JSONB NOT NULL +); diff --git a/coderd/database/migrations/000320_terraform_cached_modules.down.sql b/coderd/database/migrations/000320_terraform_cached_modules.down.sql new file mode 100644 index 0000000000000..6894e43ca9a98 --- /dev/null +++ b/coderd/database/migrations/000320_terraform_cached_modules.down.sql @@ -0,0 +1 @@ +ALTER TABLE template_version_terraform_values DROP COLUMN cached_module_files; diff --git a/coderd/database/migrations/000320_terraform_cached_modules.up.sql b/coderd/database/migrations/000320_terraform_cached_modules.up.sql new file mode 100644 index 0000000000000..17028040de7d1 --- /dev/null +++ b/coderd/database/migrations/000320_terraform_cached_modules.up.sql @@ -0,0 +1 @@ +ALTER TABLE template_version_terraform_values ADD COLUMN cached_module_files uuid references files(id); diff --git a/coderd/database/migrations/000321_add_parent_id_to_workspace_agents.down.sql b/coderd/database/migrations/000321_add_parent_id_to_workspace_agents.down.sql new file mode 100644 index 0000000000000..ab810126ad60e --- /dev/null +++ b/coderd/database/migrations/000321_add_parent_id_to_workspace_agents.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE workspace_agents +DROP COLUMN IF EXISTS parent_id; diff --git a/coderd/database/migrations/000321_add_parent_id_to_workspace_agents.up.sql b/coderd/database/migrations/000321_add_parent_id_to_workspace_agents.up.sql new file mode 100644 index 0000000000000..f2fd7a8c1cd10 --- /dev/null +++ b/coderd/database/migrations/000321_add_parent_id_to_workspace_agents.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE workspace_agents +ADD COLUMN parent_id UUID REFERENCES workspace_agents (id) ON DELETE CASCADE; diff --git a/coderd/database/migrations/000322_rename_test_notification.down.sql b/coderd/database/migrations/000322_rename_test_notification.down.sql new file mode 100644 index 0000000000000..06bfab4370d1d --- /dev/null +++ b/coderd/database/migrations/000322_rename_test_notification.down.sql @@ -0,0 +1,3 @@ +UPDATE notification_templates +SET name = 'Test Notification' +WHERE id = 'c425f63e-716a-4bf4-ae24-78348f706c3f'; diff --git a/coderd/database/migrations/000322_rename_test_notification.up.sql b/coderd/database/migrations/000322_rename_test_notification.up.sql new file mode 100644 index 0000000000000..52b2db5a9353b --- /dev/null +++ b/coderd/database/migrations/000322_rename_test_notification.up.sql @@ -0,0 +1,3 @@ +UPDATE notification_templates +SET name = 'Troubleshooting Notification' +WHERE id = 'c425f63e-716a-4bf4-ae24-78348f706c3f'; diff --git a/coderd/database/migrations/000325_dynamic_parameters_metadata.down.sql b/coderd/database/migrations/000325_dynamic_parameters_metadata.down.sql new file mode 100644 index 0000000000000..991871b5700ab --- /dev/null +++ b/coderd/database/migrations/000325_dynamic_parameters_metadata.down.sql @@ -0,0 +1 @@ +ALTER TABLE template_version_terraform_values DROP COLUMN provisionerd_version; diff --git a/coderd/database/migrations/000325_dynamic_parameters_metadata.up.sql b/coderd/database/migrations/000325_dynamic_parameters_metadata.up.sql new file mode 100644 index 0000000000000..211693b7f3e79 --- /dev/null +++ b/coderd/database/migrations/000325_dynamic_parameters_metadata.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE template_version_terraform_values ADD COLUMN IF NOT EXISTS provisionerd_version TEXT NOT NULL DEFAULT ''; + +COMMENT ON COLUMN template_version_terraform_values.provisionerd_version IS + 'What version of the provisioning engine was used to generate the cached plan and module files.'; diff --git a/coderd/database/migrations/testdata/fixtures/000319_chat.up.sql b/coderd/database/migrations/testdata/fixtures/000319_chat.up.sql new file mode 100644 index 0000000000000..123a62c4eb722 --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000319_chat.up.sql @@ -0,0 +1,6 @@ +INSERT INTO chats (id, owner_id, created_at, updated_at, title) VALUES +('00000000-0000-0000-0000-000000000001', '0ed9befc-4911-4ccf-a8e2-559bf72daa94', '2023-10-01 12:00:00+00', '2023-10-01 12:00:00+00', 'Test Chat 1'); + +INSERT INTO chat_messages (id, chat_id, created_at, model, provider, content) VALUES +(1, '00000000-0000-0000-0000-000000000001', '2023-10-01 12:00:00+00', 'annie-oakley', 'cowboy-coder', '{"role":"user","content":"Hello"}'), +(2, '00000000-0000-0000-0000-000000000001', '2023-10-01 12:01:00+00', 'annie-oakley', 'cowboy-coder', '{"role":"assistant","content":"Howdy pardner! What can I do ya for?"}'); diff --git a/coderd/database/models.go b/coderd/database/models.go index 2dfc2c23ed6e0..3674af6ed6981 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -2628,6 +2628,23 @@ type AuditLog struct { ResourceIcon string `db:"resource_icon" json:"resource_icon"` } +type Chat struct { + ID uuid.UUID `db:"id" json:"id"` + OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Title string `db:"title" json:"title"` +} + +type ChatMessage struct { + ID int64 `db:"id" json:"id"` + ChatID uuid.UUID `db:"chat_id" json:"chat_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + Model string `db:"model" json:"model"` + Provider string `db:"provider" json:"provider"` + Content json.RawMessage `db:"content" json:"content"` +} + type CryptoKey struct { Feature CryptoKeyFeature `db:"feature" json:"feature"` Sequence int32 `db:"sequence" json:"sequence"` @@ -3265,6 +3282,9 @@ type TemplateVersionTerraformValue struct { TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` CachedPlan json.RawMessage `db:"cached_plan" json:"cached_plan"` + CachedModuleFiles uuid.NullUUID `db:"cached_module_files" json:"cached_module_files"` + // What version of the provisioning engine was used to generate the cached plan and module files. + ProvisionerdVersion string `db:"provisionerd_version" json:"provisionerd_version"` } type TemplateVersionVariable struct { @@ -3442,7 +3462,8 @@ type WorkspaceAgent struct { DisplayApps []DisplayApp `db:"display_apps" json:"display_apps"` APIVersion string `db:"api_version" json:"api_version"` // Specifies the order in which to display agents in user interfaces. - DisplayOrder int32 `db:"display_order" json:"display_order"` + DisplayOrder int32 `db:"display_order" json:"display_order"` + ParentID uuid.NullUUID `db:"parent_id" json:"parent_id"` // Defines the scope of the API key associated with the agent. 'all' allows access to everything, 'no_user_data' restricts it to exclude user data. APIKeyScope AgentKeyScopeEnum `db:"api_key_scope" json:"api_key_scope"` } diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 4a2edb4451c34..b2cc20c4894d5 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -3586,6 +3586,43 @@ func TestOrganizationDeleteTrigger(t *testing.T) { require.ErrorContains(t, err, "cannot delete organization") require.ErrorContains(t, err, "has 1 members") }) + + t.Run("UserDeletedButNotRemovedFromOrg", func(t *testing.T) { + t.Parallel() + db, _ := dbtestutil.NewDB(t) + + orgA := dbfake.Organization(t, db).Do() + + userA := dbgen.User(t, db, database.User{}) + userB := dbgen.User(t, db, database.User{}) + userC := dbgen.User(t, db, database.User{}) + + dbgen.OrganizationMember(t, db, database.OrganizationMember{ + OrganizationID: orgA.Org.ID, + UserID: userA.ID, + }) + dbgen.OrganizationMember(t, db, database.OrganizationMember{ + OrganizationID: orgA.Org.ID, + UserID: userB.ID, + }) + dbgen.OrganizationMember(t, db, database.OrganizationMember{ + OrganizationID: orgA.Org.ID, + UserID: userC.ID, + }) + + // Delete one of the users but don't remove them from the org + ctx := testutil.Context(t, testutil.WaitShort) + db.UpdateUserDeletedByID(ctx, userB.ID) + + err := db.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{ + UpdatedAt: dbtime.Now(), + ID: orgA.Org.ID, + }) + require.Error(t, err) + // cannot delete organization: organization has 1 members that must be deleted first + require.ErrorContains(t, err, "cannot delete organization") + require.ErrorContains(t, err, "has 1 members") + }) } type templateVersionWithPreset struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index d0af6d1f8d76a..2d422403dfa23 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5586,11 +5586,45 @@ func (q *sqlQuerier) GetOrganizationByName(ctx context.Context, arg GetOrganizat const getOrganizationResourceCountByID = `-- name: GetOrganizationResourceCountByID :one SELECT - (SELECT COUNT(*) FROM workspaces WHERE workspaces.organization_id = $1 AND workspaces.deleted = false) AS workspace_count, - (SELECT COUNT(*) FROM groups WHERE groups.organization_id = $1) AS group_count, - (SELECT COUNT(*) FROM templates WHERE templates.organization_id = $1 AND templates.deleted = false) AS template_count, - (SELECT COUNT(*) FROM organization_members WHERE organization_members.organization_id = $1) AS member_count, - (SELECT COUNT(*) FROM provisioner_keys WHERE provisioner_keys.organization_id = $1) AS provisioner_key_count + ( + SELECT + count(*) + FROM + workspaces + WHERE + workspaces.organization_id = $1 + AND workspaces.deleted = FALSE) AS workspace_count, + ( + SELECT + count(*) + FROM + GROUPS + WHERE + groups.organization_id = $1) AS group_count, + ( + SELECT + count(*) + FROM + templates + WHERE + templates.organization_id = $1 + AND templates.deleted = FALSE) AS template_count, + ( + SELECT + count(*) + FROM + organization_members + LEFT JOIN users ON organization_members.user_id = users.id + WHERE + organization_members.organization_id = $1 + AND users.deleted = FALSE) AS member_count, +( + SELECT + count(*) + FROM + provisioner_keys + WHERE + provisioner_keys.organization_id = $1) AS provisioner_key_count ` type GetOrganizationResourceCountByIDRow struct { @@ -11471,7 +11505,7 @@ func (q *sqlQuerier) UpdateTemplateVersionExternalAuthProvidersByJobID(ctx conte const getTemplateVersionTerraformValues = `-- name: GetTemplateVersionTerraformValues :one SELECT - template_version_terraform_values.template_version_id, template_version_terraform_values.updated_at, template_version_terraform_values.cached_plan + template_version_terraform_values.template_version_id, template_version_terraform_values.updated_at, template_version_terraform_values.cached_plan, template_version_terraform_values.cached_module_files, template_version_terraform_values.provisionerd_version FROM template_version_terraform_values WHERE @@ -11481,7 +11515,13 @@ WHERE func (q *sqlQuerier) GetTemplateVersionTerraformValues(ctx context.Context, templateVersionID uuid.UUID) (TemplateVersionTerraformValue, error) { row := q.db.QueryRowContext(ctx, getTemplateVersionTerraformValues, templateVersionID) var i TemplateVersionTerraformValue - err := row.Scan(&i.TemplateVersionID, &i.UpdatedAt, &i.CachedPlan) + err := row.Scan( + &i.TemplateVersionID, + &i.UpdatedAt, + &i.CachedPlan, + &i.CachedModuleFiles, + &i.ProvisionerdVersion, + ) return i, err } @@ -13686,7 +13726,7 @@ func (q *sqlQuerier) DeleteOldWorkspaceAgentLogs(ctx context.Context, threshold const getWorkspaceAgentAndLatestBuildByAuthToken = `-- name: GetWorkspaceAgentAndLatestBuildByAuthToken :one 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, - 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.api_key_scope, + 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_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.provisioner_state, 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.initiator_by_avatar_url, workspace_build_with_user.initiator_by_username FROM workspace_agents @@ -13776,6 +13816,7 @@ func (q *sqlQuerier) GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Cont pq.Array(&i.WorkspaceAgent.DisplayApps), &i.WorkspaceAgent.APIVersion, &i.WorkspaceAgent.DisplayOrder, + &i.WorkspaceAgent.ParentID, &i.WorkspaceAgent.APIKeyScope, &i.WorkspaceBuild.ID, &i.WorkspaceBuild.CreatedAt, @@ -13800,7 +13841,7 @@ func (q *sqlQuerier) GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Cont const getWorkspaceAgentByID = `-- name: GetWorkspaceAgentByID :one SELECT - id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, api_key_scope + id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope FROM workspace_agents WHERE @@ -13842,6 +13883,7 @@ func (q *sqlQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (W pq.Array(&i.DisplayApps), &i.APIVersion, &i.DisplayOrder, + &i.ParentID, &i.APIKeyScope, ) return i, err @@ -13849,7 +13891,7 @@ func (q *sqlQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (W const getWorkspaceAgentByInstanceID = `-- name: GetWorkspaceAgentByInstanceID :one SELECT - id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, api_key_scope + id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope FROM workspace_agents WHERE @@ -13893,6 +13935,7 @@ func (q *sqlQuerier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInst pq.Array(&i.DisplayApps), &i.APIVersion, &i.DisplayOrder, + &i.ParentID, &i.APIKeyScope, ) return i, err @@ -14113,7 +14156,7 @@ func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context const getWorkspaceAgentsByResourceIDs = `-- name: GetWorkspaceAgentsByResourceIDs :many SELECT - id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, api_key_scope + id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope FROM workspace_agents WHERE @@ -14161,6 +14204,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids [] pq.Array(&i.DisplayApps), &i.APIVersion, &i.DisplayOrder, + &i.ParentID, &i.APIKeyScope, ); err != nil { return nil, err @@ -14178,7 +14222,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids [] const getWorkspaceAgentsByWorkspaceAndBuildNumber = `-- name: GetWorkspaceAgentsByWorkspaceAndBuildNumber :many SELECT - 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.api_key_scope + 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 FROM workspace_agents JOIN @@ -14236,6 +14280,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Con pq.Array(&i.DisplayApps), &i.APIVersion, &i.DisplayOrder, + &i.ParentID, &i.APIKeyScope, ); err != nil { return nil, err @@ -14252,7 +14297,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Con } const getWorkspaceAgentsCreatedAfter = `-- name: GetWorkspaceAgentsCreatedAfter :many -SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, api_key_scope FROM workspace_agents WHERE created_at > $1 +SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope FROM workspace_agents WHERE created_at > $1 ` func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) { @@ -14296,6 +14341,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, created pq.Array(&i.DisplayApps), &i.APIVersion, &i.DisplayOrder, + &i.ParentID, &i.APIKeyScope, ); err != nil { return nil, err @@ -14313,7 +14359,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, created const getWorkspaceAgentsInLatestBuildByWorkspaceID = `-- name: GetWorkspaceAgentsInLatestBuildByWorkspaceID :many SELECT - 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.api_key_scope + 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 FROM workspace_agents JOIN @@ -14373,6 +14419,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Co pq.Array(&i.DisplayApps), &i.APIVersion, &i.DisplayOrder, + &i.ParentID, &i.APIKeyScope, ); err != nil { return nil, err @@ -14392,6 +14439,7 @@ const insertWorkspaceAgent = `-- name: InsertWorkspaceAgent :one INSERT INTO workspace_agents ( id, + parent_id, created_at, updated_at, name, @@ -14412,11 +14460,12 @@ INSERT INTO api_key_scope ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) RETURNING id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, api_key_scope + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) RETURNING id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope ` type InsertWorkspaceAgentParams struct { ID uuid.UUID `db:"id" json:"id"` + ParentID uuid.NullUUID `db:"parent_id" json:"parent_id"` CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` Name string `db:"name" json:"name"` @@ -14440,6 +14489,7 @@ type InsertWorkspaceAgentParams struct { func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) { row := q.db.QueryRowContext(ctx, insertWorkspaceAgent, arg.ID, + arg.ParentID, arg.CreatedAt, arg.UpdatedAt, arg.Name, @@ -14492,6 +14542,7 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa pq.Array(&i.DisplayApps), &i.APIVersion, &i.DisplayOrder, + &i.ParentID, &i.APIKeyScope, ) return i, err diff --git a/coderd/database/queries/organizations.sql b/coderd/database/queries/organizations.sql index d940fb1ad4dc6..89a4a7bcfcef4 100644 --- a/coderd/database/queries/organizations.sql +++ b/coderd/database/queries/organizations.sql @@ -73,11 +73,46 @@ WHERE -- name: GetOrganizationResourceCountByID :one SELECT - (SELECT COUNT(*) FROM workspaces WHERE workspaces.organization_id = $1 AND workspaces.deleted = false) AS workspace_count, - (SELECT COUNT(*) FROM groups WHERE groups.organization_id = $1) AS group_count, - (SELECT COUNT(*) FROM templates WHERE templates.organization_id = $1 AND templates.deleted = false) AS template_count, - (SELECT COUNT(*) FROM organization_members WHERE organization_members.organization_id = $1) AS member_count, - (SELECT COUNT(*) FROM provisioner_keys WHERE provisioner_keys.organization_id = $1) AS provisioner_key_count; + ( + SELECT + count(*) + FROM + workspaces + WHERE + workspaces.organization_id = $1 + AND workspaces.deleted = FALSE) AS workspace_count, + ( + SELECT + count(*) + FROM + GROUPS + WHERE + groups.organization_id = $1) AS group_count, + ( + SELECT + count(*) + FROM + templates + WHERE + templates.organization_id = $1 + AND templates.deleted = FALSE) AS template_count, + ( + SELECT + count(*) + FROM + organization_members + LEFT JOIN users ON organization_members.user_id = users.id + WHERE + organization_members.organization_id = $1 + AND users.deleted = FALSE) AS member_count, +( + SELECT + count(*) + FROM + provisioner_keys + WHERE + provisioner_keys.organization_id = $1) AS provisioner_key_count; + -- name: InsertOrganization :one INSERT INTO diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql index 3b6f4a3096ce9..5965f0cb16fbf 100644 --- a/coderd/database/queries/workspaceagents.sql +++ b/coderd/database/queries/workspaceagents.sql @@ -31,6 +31,7 @@ SELECT * FROM workspace_agents WHERE created_at > $1; INSERT INTO workspace_agents ( id, + parent_id, created_at, updated_at, name, @@ -51,7 +52,7 @@ INSERT INTO api_key_scope ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) RETURNING *; -- name: UpdateWorkspaceAgentConnectionByID :exec UPDATE diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index 2b91f38c88d42..4c9c8cedcba23 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -9,6 +9,8 @@ const ( UniqueAgentStatsPkey UniqueConstraint = "agent_stats_pkey" // ALTER TABLE ONLY workspace_agent_stats ADD CONSTRAINT agent_stats_pkey PRIMARY KEY (id); UniqueAPIKeysPkey UniqueConstraint = "api_keys_pkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_pkey PRIMARY KEY (id); UniqueAuditLogsPkey UniqueConstraint = "audit_logs_pkey" // ALTER TABLE ONLY audit_logs ADD CONSTRAINT audit_logs_pkey PRIMARY KEY (id); + UniqueChatMessagesPkey UniqueConstraint = "chat_messages_pkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_pkey PRIMARY KEY (id); + UniqueChatsPkey UniqueConstraint = "chats_pkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_pkey PRIMARY KEY (id); UniqueCryptoKeysPkey UniqueConstraint = "crypto_keys_pkey" // ALTER TABLE ONLY crypto_keys ADD CONSTRAINT crypto_keys_pkey PRIMARY KEY (feature, sequence); UniqueCustomRolesUniqueKey UniqueConstraint = "custom_roles_unique_key" // ALTER TABLE ONLY custom_roles ADD CONSTRAINT custom_roles_unique_key UNIQUE (name, organization_id); UniqueDbcryptKeysActiveKeyDigestKey UniqueConstraint = "dbcrypt_keys_active_key_digest_key" // ALTER TABLE ONLY dbcrypt_keys ADD CONSTRAINT dbcrypt_keys_active_key_digest_key UNIQUE (active_key_digest); diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateTestNotification.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateTestNotification.json.golden index 09c18f975d754..b26e3043b4f45 100644 --- a/coderd/notifications/testdata/rendered-templates/webhook/TemplateTestNotification.json.golden +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateTestNotification.json.golden @@ -3,7 +3,7 @@ "msg_id": "00000000-0000-0000-0000-000000000000", "payload": { "_version": "1.2", - "notification_name": "Test Notification", + "notification_name": "Troubleshooting Notification", "notification_template_id": "00000000-0000-0000-0000-000000000000", "user_id": "00000000-0000-0000-0000-000000000000", "user_email": "bobby@coder.com", diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 4232b85644a23..dc45260e921b4 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -2063,6 +2063,7 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. agentID := uuid.New() dbAgent, err := db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{ ID: agentID, + ParentID: uuid.NullUUID{}, CreatedAt: dbtime.Now(), UpdatedAt: dbtime.Now(), ResourceID: resource.ID, diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 1ba86c50baf8a..b6c60781dac35 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -2424,6 +2424,7 @@ func TestInsertWorkspaceResource(t *testing.T) { require.NoError(t, err) require.Len(t, agents, 1) agent := agents[0] + require.Equal(t, uuid.NullUUID{}, agent.ParentID) require.Equal(t, "amd64", agent.Architecture) require.Equal(t, "linux", agent.OperatingSystem) want, err := json.Marshal(map[string]string{ diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index 5c7171f70a627..f58338a209901 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -139,6 +139,7 @@ const ( type WorkspaceAgent struct { ID uuid.UUID `json:"id" format:"uuid"` + ParentID uuid.NullUUID `json:"parent_id" format:"uuid"` CreatedAt time.Time `json:"created_at" format:"date-time"` UpdatedAt time.Time `json:"updated_at" format:"date-time"` FirstConnectedAt *time.Time `json:"first_connected_at,omitempty" format:"date-time"` diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md index 3bf6a09885f7c..626f998f5954d 100644 --- a/docs/admin/security/audit-logs.md +++ b/docs/admin/security/audit-logs.md @@ -29,7 +29,7 @@ We track the following resources: | 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
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
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
user_acltrue
| | TemplateVersion
create, write | |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
external_auth_providersfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
source_example_idfalse
template_idtrue
updated_atfalse
| | User
create, write, delete | |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodefalse
hashed_passwordtrue
idtrue
is_systemtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| -| WorkspaceAgent
connect, disconnect | |
FieldTracked
api_key_scopefalse
api_versionfalse
architecturefalse
auth_instance_idfalse
auth_tokenfalse
connection_timeout_secondsfalse
created_atfalse
directoryfalse
disconnected_atfalse
display_appsfalse
display_orderfalse
environment_variablesfalse
expanded_directoryfalse
first_connected_atfalse
idfalse
instance_metadatafalse
last_connected_atfalse
last_connected_replica_idfalse
lifecycle_statefalse
logs_lengthfalse
logs_overflowedfalse
motd_filefalse
namefalse
operating_systemfalse
ready_atfalse
resource_idfalse
resource_metadatafalse
started_atfalse
subsystemsfalse
troubleshooting_urlfalse
updated_atfalse
versionfalse
| +| WorkspaceAgent
connect, disconnect | |
FieldTracked
api_key_scopefalse
api_versionfalse
architecturefalse
auth_instance_idfalse
auth_tokenfalse
connection_timeout_secondsfalse
created_atfalse
directoryfalse
disconnected_atfalse
display_appsfalse
display_orderfalse
environment_variablesfalse
expanded_directoryfalse
first_connected_atfalse
idfalse
instance_metadatafalse
last_connected_atfalse
last_connected_replica_idfalse
lifecycle_statefalse
logs_lengthfalse
logs_overflowedfalse
motd_filefalse
namefalse
operating_systemfalse
parent_idfalse
ready_atfalse
resource_idfalse
resource_metadatafalse
started_atfalse
subsystemsfalse
troubleshooting_urlfalse
updated_atfalse
versionfalse
| | WorkspaceApp
open, close | |
FieldTracked
agent_idfalse
commandfalse
created_atfalse
display_namefalse
display_orderfalse
externalfalse
healthfalse
healthcheck_intervalfalse
healthcheck_thresholdfalse
healthcheck_urlfalse
hiddenfalse
iconfalse
idfalse
open_infalse
sharing_levelfalse
slugfalse
subdomainfalse
urlfalse
| | WorkspaceBuild
start, stop | |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
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
| diff --git a/docs/reference/api/agents.md b/docs/reference/api/agents.md index 81dd9cf2eb88a..eced88f4f72cc 100644 --- a/docs/reference/api/agents.md +++ b/docs/reference/api/agents.md @@ -609,6 +609,10 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent} \ "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index 1f795c3d7d313..8e88df96c1d29 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -164,6 +164,10 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ @@ -393,6 +397,10 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \ "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ @@ -737,6 +745,10 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/res "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ @@ -859,6 +871,9 @@ Status Code **200** | `»» logs_overflowed` | boolean | false | | | | `»» name` | string | false | | | | `»» operating_system` | string | false | | | +| `»» parent_id` | [uuid.NullUUID](schemas.md#uuidnulluuid) | false | | | +| `»»» uuid` | string | false | | | +| `»»» valid` | boolean | false | | Valid is true if UUID is not NULL | | `»» ready_at` | string(date-time) | false | | | | `»» resource_id` | string(uuid) | false | | | | `»» scripts` | array | false | | | @@ -1092,6 +1107,10 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ @@ -1394,6 +1413,10 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ @@ -1573,6 +1596,9 @@ Status Code **200** | `»»» logs_overflowed` | boolean | false | | | | `»»» name` | string | false | | | | `»»» operating_system` | string | false | | | +| `»»» parent_id` | [uuid.NullUUID](schemas.md#uuidnulluuid) | false | | | +| `»»»» uuid` | string | false | | | +| `»»»» valid` | boolean | false | | Valid is true if UUID is not NULL | | `»»» ready_at` | string(date-time) | false | | | | `»»» resource_id` | string(uuid) | false | | | | `»»» scripts` | array | false | | | @@ -1867,6 +1893,10 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 6caf9e3384546..5ed472a0661c4 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -7779,6 +7779,10 @@ If the schedule is empty, the user will be updated to use the default schedule.| "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ @@ -7983,6 +7987,10 @@ If the schedule is empty, the user will be updated to use the default schedule.| "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ @@ -8039,6 +8047,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `logs_overflowed` | boolean | false | | | | `name` | string | false | | | | `operating_system` | string | false | | | +| `parent_id` | [uuid.NullUUID](#uuidnulluuid) | false | | | | `ready_at` | string | false | | | | `resource_id` | string | false | | | | `scripts` | array of [codersdk.WorkspaceAgentScript](#codersdkworkspaceagentscript) | false | | | @@ -8731,6 +8740,10 @@ If the schedule is empty, the user will be updated to use the default schedule.| "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ @@ -9147,6 +9160,10 @@ If the schedule is empty, the user will be updated to use the default schedule.| "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ @@ -9429,6 +9446,10 @@ If the schedule is empty, the user will be updated to use the default schedule.| "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ @@ -11392,6 +11413,22 @@ RegionIDs in range 900-999 are reserved for end users to run their own DERP node None +## uuid.NullUUID + +```json +{ + "uuid": "string", + "valid": true +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|---------|---------|----------|--------------|-----------------------------------| +| `uuid` | string | false | | | +| `valid` | boolean | false | | Valid is true if UUID is not NULL | + ## workspaceapps.AccessMethod ```json diff --git a/docs/reference/api/templates.md b/docs/reference/api/templates.md index ef136764bf2c5..b1beeb64a7116 100644 --- a/docs/reference/api/templates.md +++ b/docs/reference/api/templates.md @@ -2348,6 +2348,10 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ @@ -2470,6 +2474,9 @@ Status Code **200** | `»» logs_overflowed` | boolean | false | | | | `»» name` | string | false | | | | `»» operating_system` | string | false | | | +| `»» parent_id` | [uuid.NullUUID](schemas.md#uuidnulluuid) | false | | | +| `»»» uuid` | string | false | | | +| `»»» valid` | boolean | false | | Valid is true if UUID is not NULL | | `»» ready_at` | string(date-time) | false | | | | `»» resource_id` | string(uuid) | false | | | | `»» scripts` | array | false | | | @@ -2869,6 +2876,10 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/r "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ @@ -2991,6 +3002,9 @@ Status Code **200** | `»» logs_overflowed` | boolean | false | | | | `»» name` | string | false | | | | `»» operating_system` | string | false | | | +| `»» parent_id` | [uuid.NullUUID](schemas.md#uuidnulluuid) | false | | | +| `»»» uuid` | string | false | | | +| `»»» valid` | boolean | false | | Valid is true if UUID is not NULL | | `»» ready_at` | string(date-time) | false | | | | `»» resource_id` | string(uuid) | false | | | | `»» scripts` | array | false | | | diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 5d09c46a01d30..8e25cd0bd58e6 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -219,6 +219,10 @@ of the template will be used. "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ @@ -496,6 +500,10 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ @@ -799,6 +807,10 @@ of the template will be used. "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ @@ -1062,6 +1074,10 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ @@ -1340,6 +1356,10 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ @@ -1733,6 +1753,10 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \ "logs_overflowed": true, "name": "string", "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, "ready_at": "2019-08-24T14:15:22Z", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "scripts": [ diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index 797abc46b7700..ab24ba8524a64 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -343,6 +343,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ "api_version": ActionIgnore, "display_order": ActionIgnore, "api_key_scope": ActionIgnore, + "parent_id": ActionIgnore, }, &database.WorkspaceApp{}: { "id": ActionIgnore, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 7ec54965c2efb..7b6b60854c5dc 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -3196,6 +3196,7 @@ export interface Workspace { // From codersdk/workspaceagents.go export interface WorkspaceAgent { readonly id: string; + readonly parent_id: string | null; readonly created_at: string; readonly updated_at: string; readonly first_connected_at?: string; diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index 88198bdb7b09a..3afdd2cb15f19 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -101,6 +101,33 @@ export const Running: Story = { }, }; +export const RunningWithChildAgent: Story = { + args: { + ...Running.args, + workspace: { + ...Mocks.MockWorkspace, + latest_build: { + ...Mocks.MockWorkspace.latest_build, + resources: [ + { + ...Mocks.MockWorkspaceResource, + agents: [ + { + ...Mocks.MockWorkspaceAgent, + lifecycle_state: "ready", + }, + { + ...Mocks.MockWorkspaceChildAgent, + lifecycle_state: "ready", + }, + ], + }, + ], + }, + }, + }, +}; + export const RunningWithAppStatuses: Story = { args: { workspace: { diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 9148c71f32d22..3f87ab7a818e5 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -272,22 +272,28 @@ export const Workspace: FC = ({ minWidth: 0 /* Prevent overflow */, }} > - {selectedResource.agents?.map((agent) => ( - - ))} + {selectedResource.agents + // If an agent has a `parent_id`, that means it is + // child of another agent. We do not want these agents + // to be displayed at the top-level on this page. We + // want them to display _as children_ of their parents. + ?.filter((agent) => agent.parent_id === null) + .map((agent) => ( + + ))} {(!selectedResource.agents || selectedResource.agents?.length === 0) && ( diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 8b19905286a22..4e50009d5ba7d 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -944,6 +944,7 @@ export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = { created_at: "", environment_variables: {}, id: "test-workspace-agent", + parent_id: null, name: "a-workspace-agent", operating_system: "linux", resource_id: "", @@ -978,6 +979,47 @@ export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = { ], }; +export const MockWorkspaceChildAgent: TypesGen.WorkspaceAgent = { + apps: [], + architecture: "amd64", + created_at: "", + environment_variables: {}, + id: "test-workspace-child-agent", + parent_id: "test-workspace-agent", + name: "a-workspace-child-agent", + operating_system: "linux", + resource_id: "", + status: "connected", + updated_at: "", + version: MockBuildInfo.version, + api_version: MockBuildInfo.agent_api_version, + latency: { + "Coder Embedded DERP": { + latency_ms: 32.55, + preferred: true, + }, + }, + connection_timeout_seconds: 120, + troubleshooting_url: "https://coder.com/troubleshoot", + lifecycle_state: "starting", + logs_length: 0, + logs_overflowed: false, + log_sources: [MockWorkspaceAgentLogSource], + scripts: [], + startup_script_behavior: "non-blocking", + subsystems: ["envbox", "exectrace"], + health: { + healthy: true, + }, + display_apps: [ + "ssh_helper", + "port_forwarding_helper", + "vscode", + "vscode_insiders", + "web_terminal", + ], +}; + export const MockWorkspaceAppStatus: TypesGen.WorkspaceAppStatus = { id: "test-app-status", created_at: "2022-05-17T17:39:01.382927298Z",