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 |
Field | Tracked |
| active_version_id | true |
activity_bump | true |
allow_user_autostart | true |
allow_user_autostop | true |
allow_user_cancel_workspace_jobs | true |
autostart_block_days_of_week | true |
autostop_requirement_days_of_week | true |
autostop_requirement_weeks | true |
created_at | false |
created_by | true |
created_by_avatar_url | false |
created_by_username | false |
default_ttl | true |
deleted | false |
deprecated | true |
description | true |
display_name | true |
failure_ttl | true |
group_acl | true |
icon | true |
id | true |
max_port_sharing_level | true |
name | true |
organization_display_name | false |
organization_icon | false |
organization_id | false |
organization_name | false |
provisioner | true |
require_active_version | true |
time_til_dormant | true |
time_til_dormant_autodelete | true |
updated_at | false |
user_acl | true |
|
| TemplateVersion
create, write | Field | Tracked |
| archived | true |
created_at | false |
created_by | true |
created_by_avatar_url | false |
created_by_username | false |
external_auth_providers | false |
id | true |
job_id | false |
message | false |
name | true |
organization_id | false |
readme | true |
source_example_id | false |
template_id | true |
updated_at | false |
|
| User
create, write, delete | Field | Tracked |
| avatar_url | false |
created_at | false |
deleted | true |
email | true |
github_com_user_id | false |
hashed_one_time_passcode | false |
hashed_password | true |
id | true |
is_system | true |
last_seen_at | false |
login_type | true |
name | true |
one_time_passcode_expires_at | true |
quiet_hours_schedule | true |
rbac_roles | true |
status | true |
updated_at | false |
username | true |
|
-| WorkspaceAgent
connect, disconnect | Field | Tracked |
| api_key_scope | false |
api_version | false |
architecture | false |
auth_instance_id | false |
auth_token | false |
connection_timeout_seconds | false |
created_at | false |
directory | false |
disconnected_at | false |
display_apps | false |
display_order | false |
environment_variables | false |
expanded_directory | false |
first_connected_at | false |
id | false |
instance_metadata | false |
last_connected_at | false |
last_connected_replica_id | false |
lifecycle_state | false |
logs_length | false |
logs_overflowed | false |
motd_file | false |
name | false |
operating_system | false |
ready_at | false |
resource_id | false |
resource_metadata | false |
started_at | false |
subsystems | false |
troubleshooting_url | false |
updated_at | false |
version | false |
|
+| WorkspaceAgent
connect, disconnect | Field | Tracked |
| api_key_scope | false |
api_version | false |
architecture | false |
auth_instance_id | false |
auth_token | false |
connection_timeout_seconds | false |
created_at | false |
directory | false |
disconnected_at | false |
display_apps | false |
display_order | false |
environment_variables | false |
expanded_directory | false |
first_connected_at | false |
id | false |
instance_metadata | false |
last_connected_at | false |
last_connected_replica_id | false |
lifecycle_state | false |
logs_length | false |
logs_overflowed | false |
motd_file | false |
name | false |
operating_system | false |
parent_id | false |
ready_at | false |
resource_id | false |
resource_metadata | false |
started_at | false |
subsystems | false |
troubleshooting_url | false |
updated_at | false |
version | false |
|
| WorkspaceApp
open, close | Field | Tracked |
| agent_id | false |
command | false |
created_at | false |
display_name | false |
display_order | false |
external | false |
health | false |
healthcheck_interval | false |
healthcheck_threshold | false |
healthcheck_url | false |
hidden | false |
icon | false |
id | false |
open_in | false |
sharing_level | false |
slug | false |
subdomain | false |
url | false |
|
| WorkspaceBuild
start, stop | Field | Tracked |
| build_number | false |
created_at | false |
daily_cost | false |
deadline | false |
id | false |
initiator_by_avatar_url | false |
initiator_by_username | false |
initiator_id | false |
job_id | false |
max_deadline | false |
provisioner_state | false |
reason | false |
template_version_id | true |
template_version_preset_id | false |
transition | false |
updated_at | false |
workspace_id | false |
|
| WorkspaceProxy
| Field | Tracked |
| created_at | true |
deleted | false |
derp_enabled | true |
derp_only | true |
display_name | true |
icon | true |
id | true |
name | true |
region_id | true |
token_hashed_secret | true |
updated_at | false |
url | true |
version | true |
wildcard_hostname | true |
|
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",