diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index dcf63c6f66f9e..c4e1c26f1b389 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -141,6 +141,11 @@ CREATE TYPE startup_script_behavior AS ENUM ( 'non-blocking' ); +CREATE TYPE tailnet_status AS ENUM ( + 'ok', + 'lost' +); + CREATE TYPE user_status AS ENUM ( 'active', 'suspended', @@ -292,6 +297,35 @@ BEGIN END; $$; +CREATE FUNCTION tailnet_notify_peer_change() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + IF (OLD IS NOT NULL) THEN + PERFORM pg_notify('tailnet_peer_update', OLD.id::text); + RETURN NULL; + END IF; + IF (NEW IS NOT NULL) THEN + PERFORM pg_notify('tailnet_peer_update', NEW.id::text); + RETURN NULL; + END IF; +END; +$$; + +CREATE FUNCTION tailnet_notify_tunnel_change() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + IF (NEW IS NOT NULL) THEN + PERFORM pg_notify('tailnet_tunnel_update', NEW.src_id || ',' || NEW.dst_id); + RETURN NULL; + ELSIF (OLD IS NOT NULL) THEN + PERFORM pg_notify('tailnet_tunnel_update', OLD.src_id || ',' || OLD.dst_id); + RETURN NULL; + END IF; +END; +$$; + CREATE TABLE api_keys ( id text NOT NULL, hashed_secret bytea NOT NULL, @@ -587,6 +621,21 @@ CREATE TABLE tailnet_coordinators ( COMMENT ON TABLE tailnet_coordinators IS 'We keep this separate from replicas in case we need to break the coordinator out into its own service'; +CREATE TABLE tailnet_peers ( + id uuid NOT NULL, + coordinator_id uuid NOT NULL, + updated_at timestamp with time zone NOT NULL, + node bytea NOT NULL, + status tailnet_status DEFAULT 'ok'::tailnet_status NOT NULL +); + +CREATE TABLE tailnet_tunnels ( + coordinator_id uuid NOT NULL, + src_id uuid NOT NULL, + dst_id uuid NOT NULL, + updated_at timestamp with time zone NOT NULL +); + CREATE TABLE template_version_parameters ( template_version_id uuid NOT NULL, name text NOT NULL, @@ -1248,6 +1297,12 @@ ALTER TABLE ONLY tailnet_clients ALTER TABLE ONLY tailnet_coordinators ADD CONSTRAINT tailnet_coordinators_pkey PRIMARY KEY (id); +ALTER TABLE ONLY tailnet_peers + ADD CONSTRAINT tailnet_peers_pkey PRIMARY KEY (id, coordinator_id); + +ALTER TABLE ONLY tailnet_tunnels + ADD CONSTRAINT tailnet_tunnels_pkey PRIMARY KEY (coordinator_id, src_id, dst_id); + ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_name_key UNIQUE (template_version_id, name); @@ -1351,6 +1406,8 @@ CREATE INDEX idx_tailnet_agents_coordinator ON tailnet_agents USING btree (coord CREATE INDEX idx_tailnet_clients_coordinator ON tailnet_clients USING btree (coordinator_id); +CREATE INDEX idx_tailnet_peers_coordinator ON tailnet_peers USING btree (coordinator_id); + CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false); CREATE UNIQUE INDEX idx_users_username ON users USING btree (username) WHERE (deleted = false); @@ -1391,6 +1448,10 @@ CREATE TRIGGER tailnet_notify_client_subscription_change AFTER INSERT OR DELETE CREATE TRIGGER tailnet_notify_coordinator_heartbeat AFTER INSERT OR UPDATE ON tailnet_coordinators FOR EACH ROW EXECUTE FUNCTION tailnet_notify_coordinator_heartbeat(); +CREATE TRIGGER tailnet_notify_peer_change AFTER INSERT OR DELETE OR UPDATE ON tailnet_peers FOR EACH ROW EXECUTE FUNCTION tailnet_notify_peer_change(); + +CREATE TRIGGER tailnet_notify_tunnel_change AFTER INSERT OR DELETE OR UPDATE ON tailnet_tunnels FOR EACH ROW EXECUTE FUNCTION tailnet_notify_tunnel_change(); + CREATE TRIGGER trigger_insert_apikeys BEFORE INSERT ON api_keys FOR EACH ROW EXECUTE FUNCTION insert_apikey_fail_if_user_deleted(); CREATE TRIGGER trigger_update_users AFTER INSERT OR UPDATE ON users FOR EACH ROW WHEN ((new.deleted = true)) EXECUTE FUNCTION delete_deleted_user_api_keys(); @@ -1440,6 +1501,12 @@ ALTER TABLE ONLY tailnet_client_subscriptions ALTER TABLE ONLY tailnet_clients ADD CONSTRAINT tailnet_clients_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; +ALTER TABLE ONLY tailnet_peers + ADD CONSTRAINT tailnet_peers_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; + +ALTER TABLE ONLY tailnet_tunnels + ADD CONSTRAINT tailnet_tunnels_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; + 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; diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index c2e81fd3bf817..85d5fa9faa130 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -21,6 +21,8 @@ const ( ForeignKeyTailnetAgentsCoordinatorID ForeignKeyConstraint = "tailnet_agents_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_agents ADD CONSTRAINT tailnet_agents_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; ForeignKeyTailnetClientSubscriptionsCoordinatorID ForeignKeyConstraint = "tailnet_client_subscriptions_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_client_subscriptions ADD CONSTRAINT tailnet_client_subscriptions_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; ForeignKeyTailnetClientsCoordinatorID ForeignKeyConstraint = "tailnet_clients_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_clients ADD CONSTRAINT tailnet_clients_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; + ForeignKeyTailnetPeersCoordinatorID ForeignKeyConstraint = "tailnet_peers_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_peers ADD CONSTRAINT tailnet_peers_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; + ForeignKeyTailnetTunnelsCoordinatorID ForeignKeyConstraint = "tailnet_tunnels_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; 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; 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; ForeignKeyTemplateVersionsCreatedBy ForeignKeyConstraint = "template_versions_created_by_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT; diff --git a/coderd/database/migrations/000168_pg_coord_tailnet_v2_api.down.sql b/coderd/database/migrations/000168_pg_coord_tailnet_v2_api.down.sql new file mode 100644 index 0000000000000..084c00e922986 --- /dev/null +++ b/coderd/database/migrations/000168_pg_coord_tailnet_v2_api.down.sql @@ -0,0 +1,14 @@ +BEGIN; + +DROP TRIGGER IF EXISTS tailnet_notify_tunnel_change ON tailnet_tunnels; +DROP FUNCTION IF EXISTS tailnet_notify_tunnel_change; +DROP TABLE IF EXISTS tailnet_tunnels; + +DROP TRIGGER IF EXISTS tailnet_notify_peer_change ON tailnet_peers; +DROP FUNCTION IF EXISTS tailnet_notify_peer_change; +DROP INDEX IF EXISTS idx_tailnet_peers_coordinator; +DROP TABLE IF EXISTS tailnet_peers; + +DROP TYPE IF EXISTS tailnet_status; + +COMMIT; diff --git a/coderd/database/migrations/000168_pg_coord_tailnet_v2_api.up.sql b/coderd/database/migrations/000168_pg_coord_tailnet_v2_api.up.sql new file mode 100644 index 0000000000000..9839c59a3350f --- /dev/null +++ b/coderd/database/migrations/000168_pg_coord_tailnet_v2_api.up.sql @@ -0,0 +1,72 @@ +BEGIN; + +CREATE TYPE tailnet_status AS ENUM ( + 'ok', + 'lost' +); + +CREATE TABLE tailnet_peers ( + id uuid NOT NULL, + coordinator_id uuid NOT NULL, + updated_at timestamp with time zone NOT NULL, + node bytea NOT NULL, + status tailnet_status DEFAULT 'ok'::tailnet_status NOT NULL, + PRIMARY KEY (id, coordinator_id), + FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE +); + +-- For shutting down / GC a coordinator +CREATE INDEX idx_tailnet_peers_coordinator ON tailnet_peers (coordinator_id); + +-- Any time tailnet_peers table changes, send an update with the affected peer ID. +CREATE FUNCTION tailnet_notify_peer_change() RETURNS trigger + LANGUAGE plpgsql +AS $$ +BEGIN + IF (OLD IS NOT NULL) THEN + PERFORM pg_notify('tailnet_peer_update', OLD.id::text); + RETURN NULL; + END IF; + IF (NEW IS NOT NULL) THEN + PERFORM pg_notify('tailnet_peer_update', NEW.id::text); + RETURN NULL; + END IF; +END; +$$; + +CREATE TRIGGER tailnet_notify_peer_change + AFTER INSERT OR UPDATE OR DELETE ON tailnet_peers + FOR EACH ROW +EXECUTE PROCEDURE tailnet_notify_peer_change(); + +CREATE TABLE tailnet_tunnels ( + coordinator_id uuid NOT NULL, + -- we don't keep foreign keys for src_id and dst_id because the coordinator doesn't + -- strictly order creating the peers and creating the tunnels + src_id uuid NOT NULL, + dst_id uuid NOT NULL, + updated_at timestamp with time zone NOT NULL, + PRIMARY KEY (coordinator_id, src_id, dst_id), + FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators (id) ON DELETE CASCADE +); + +CREATE FUNCTION tailnet_notify_tunnel_change() RETURNS trigger + LANGUAGE plpgsql +AS $$ +BEGIN + IF (NEW IS NOT NULL) THEN + PERFORM pg_notify('tailnet_tunnel_update', NEW.src_id || ',' || NEW.dst_id); + RETURN NULL; + ELSIF (OLD IS NOT NULL) THEN + PERFORM pg_notify('tailnet_tunnel_update', OLD.src_id || ',' || OLD.dst_id); + RETURN NULL; + END IF; +END; +$$; + +CREATE TRIGGER tailnet_notify_tunnel_change + AFTER INSERT OR UPDATE OR DELETE ON tailnet_tunnels + FOR EACH ROW +EXECUTE PROCEDURE tailnet_notify_tunnel_change(); + +COMMIT; diff --git a/coderd/database/migrations/testdata/fixtures/000168_pg_coord_tailnet_v2_api.up.sql b/coderd/database/migrations/testdata/fixtures/000168_pg_coord_tailnet_v2_api.up.sql new file mode 100644 index 0000000000000..bc95a3519b565 --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000168_pg_coord_tailnet_v2_api.up.sql @@ -0,0 +1,18 @@ +INSERT INTO tailnet_peers + (id, coordinator_id, updated_at, node, status) +VALUES ( + 'c0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', + '2023-06-15 10:23:54+00', + 'a fake protobuf byte string', + 'ok' +); + +INSERT INTO tailnet_tunnels + (coordinator_id, src_id, dst_id, updated_at) +VALUES ( + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', + 'c0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', + 'b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', + '2023-06-15 10:23:54+00' +); diff --git a/coderd/database/models.go b/coderd/database/models.go index 4d5a0193ccc7c..84147dc28fcc2 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1289,6 +1289,64 @@ func AllStartupScriptBehaviorValues() []StartupScriptBehavior { } } +type TailnetStatus string + +const ( + TailnetStatusOk TailnetStatus = "ok" + TailnetStatusLost TailnetStatus = "lost" +) + +func (e *TailnetStatus) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = TailnetStatus(s) + case string: + *e = TailnetStatus(s) + default: + return fmt.Errorf("unsupported scan type for TailnetStatus: %T", src) + } + return nil +} + +type NullTailnetStatus struct { + TailnetStatus TailnetStatus `json:"tailnet_status"` + Valid bool `json:"valid"` // Valid is true if TailnetStatus is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullTailnetStatus) Scan(value interface{}) error { + if value == nil { + ns.TailnetStatus, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.TailnetStatus.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullTailnetStatus) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.TailnetStatus), nil +} + +func (e TailnetStatus) Valid() bool { + switch e { + case TailnetStatusOk, + TailnetStatusLost: + return true + } + return false +} + +func AllTailnetStatusValues() []TailnetStatus { + return []TailnetStatus{ + TailnetStatusOk, + TailnetStatusLost, + } +} + // Defines the user status: active, dormant, or suspended. type UserStatus string @@ -1865,6 +1923,21 @@ type TailnetCoordinator struct { HeartbeatAt time.Time `db:"heartbeat_at" json:"heartbeat_at"` } +type TailnetPeer struct { + ID uuid.UUID `db:"id" json:"id"` + CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Node []byte `db:"node" json:"node"` + Status TailnetStatus `db:"status" json:"status"` +} + +type TailnetTunnel struct { + CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"` + SrcID uuid.UUID `db:"src_id" json:"src_id"` + DstID uuid.UUID `db:"dst_id" json:"dst_id"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + // Joins in the username + avatar url of the created by user. type Template struct { ID uuid.UUID `db:"id" json:"id"` diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index 0087da609aa2c..da5877ad0f071 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -36,6 +36,8 @@ const ( UniqueTailnetClientSubscriptionsPkey UniqueConstraint = "tailnet_client_subscriptions_pkey" // ALTER TABLE ONLY tailnet_client_subscriptions ADD CONSTRAINT tailnet_client_subscriptions_pkey PRIMARY KEY (client_id, coordinator_id, agent_id); UniqueTailnetClientsPkey UniqueConstraint = "tailnet_clients_pkey" // ALTER TABLE ONLY tailnet_clients ADD CONSTRAINT tailnet_clients_pkey PRIMARY KEY (id, coordinator_id); UniqueTailnetCoordinatorsPkey UniqueConstraint = "tailnet_coordinators_pkey" // ALTER TABLE ONLY tailnet_coordinators ADD CONSTRAINT tailnet_coordinators_pkey PRIMARY KEY (id); + UniqueTailnetPeersPkey UniqueConstraint = "tailnet_peers_pkey" // ALTER TABLE ONLY tailnet_peers ADD CONSTRAINT tailnet_peers_pkey PRIMARY KEY (id, coordinator_id); + UniqueTailnetTunnelsPkey UniqueConstraint = "tailnet_tunnels_pkey" // ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_pkey PRIMARY KEY (coordinator_id, src_id, dst_id); UniqueTemplateVersionParametersTemplateVersionIDNameKey UniqueConstraint = "template_version_parameters_template_version_id_name_key" // ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_name_key UNIQUE (template_version_id, name); UniqueTemplateVersionVariablesTemplateVersionIDNameKey UniqueConstraint = "template_version_variables_template_version_id_name_key" // ALTER TABLE ONLY template_version_variables ADD CONSTRAINT template_version_variables_template_version_id_name_key UNIQUE (template_version_id, name); UniqueTemplateVersionsPkey UniqueConstraint = "template_versions_pkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_pkey PRIMARY KEY (id);