Files
rdp-proxy/backend/migrations/000010_cluster_node_admin_foundation.up.sql
T
2026-04-28 22:29:50 +03:00

187 lines
7.8 KiB
SQL

CREATE TABLE IF NOT EXISTS clusters (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
region TEXT,
metadata JSONB NOT NULL DEFAULT '{}'::JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT clusters_status_check
CHECK (status IN ('active', 'disabled', 'archived', 'degraded'))
);
INSERT INTO clusters (slug, name, status, region, metadata)
VALUES ('default', 'Default Cluster', 'active', NULL, '{"bootstrap":true}'::jsonb)
ON CONFLICT (slug) DO NOTHING;
CREATE TABLE IF NOT EXISTS cluster_memberships (
cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE,
node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
membership_status TEXT NOT NULL DEFAULT 'active',
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_seen_at TIMESTAMPTZ,
metadata JSONB NOT NULL DEFAULT '{}'::JSONB,
PRIMARY KEY (cluster_id, node_id),
CONSTRAINT cluster_memberships_status_check
CHECK (membership_status IN ('active', 'draining', 'disabled', 'revoked'))
);
INSERT INTO cluster_memberships (cluster_id, node_id, membership_status, joined_at, last_seen_at, metadata)
SELECT c.id, n.id, 'active', COALESCE(n.created_at, NOW()), n.last_seen_at, '{"backfilled":true}'::jsonb
FROM clusters c
CROSS JOIN nodes n
WHERE c.slug = 'default'
ON CONFLICT (cluster_id, node_id) DO NOTHING;
CREATE TABLE IF NOT EXISTS node_identities (
node_id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
public_key TEXT NOT NULL,
certificate_serial TEXT,
certificate_not_before TIMESTAMPTZ,
certificate_not_after TIMESTAMPTZ,
identity_status TEXT NOT NULL DEFAULT 'pending',
rotated_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ,
metadata JSONB NOT NULL DEFAULT '{}'::JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT node_identities_status_check
CHECK (identity_status IN ('pending', 'active', 'rotating', 'revoked'))
);
CREATE TABLE IF NOT EXISTS node_join_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL UNIQUE,
scope JSONB NOT NULL DEFAULT '{}'::JSONB,
expires_at TIMESTAMPTZ NOT NULL,
max_uses INTEGER NOT NULL DEFAULT 1,
used_count INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'active',
created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
revoked_at TIMESTAMPTZ,
CONSTRAINT node_join_tokens_status_check
CHECK (status IN ('active', 'revoked', 'expired')),
CONSTRAINT node_join_tokens_max_uses_check
CHECK (max_uses > 0),
CONSTRAINT node_join_tokens_used_count_check
CHECK (used_count >= 0)
);
CREATE INDEX IF NOT EXISTS idx_node_join_tokens_cluster_status
ON node_join_tokens(cluster_id, status, expires_at);
CREATE TABLE IF NOT EXISTS node_join_requests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE,
join_token_id UUID REFERENCES node_join_tokens(id) ON DELETE SET NULL,
node_name TEXT NOT NULL,
node_fingerprint TEXT NOT NULL,
public_key TEXT NOT NULL,
reported_capabilities JSONB NOT NULL DEFAULT '{}'::JSONB,
reported_facts JSONB NOT NULL DEFAULT '{}'::JSONB,
requested_roles JSONB NOT NULL DEFAULT '[]'::JSONB,
status TEXT NOT NULL DEFAULT 'pending',
reviewed_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
reviewed_at TIMESTAMPTZ,
approved_node_id UUID REFERENCES nodes(id) ON DELETE SET NULL,
rejection_reason TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT node_join_requests_status_check
CHECK (status IN ('pending', 'approved', 'rejected', 'cancelled'))
);
CREATE INDEX IF NOT EXISTS idx_node_join_requests_cluster_status
ON node_join_requests(cluster_id, status, created_at DESC);
CREATE UNIQUE INDEX IF NOT EXISTS idx_node_join_requests_pending_fingerprint
ON node_join_requests(cluster_id, node_fingerprint)
WHERE status = 'pending';
CREATE TABLE IF NOT EXISTS node_role_assignments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE,
node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
role TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
policy JSONB NOT NULL DEFAULT '{}'::JSONB,
assigned_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
revoked_at TIMESTAMPTZ,
CONSTRAINT node_role_assignments_status_check
CHECK (status IN ('active', 'disabled', 'revoked')),
CONSTRAINT node_role_assignments_role_check
CHECK (role IN (
'entry-node',
'relay-node',
'core-mesh',
'rdp-worker',
'vnc-worker',
'vpn-exit',
'vpn-connector',
'file-storage-cache',
'update-cache',
'video-relay'
))
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_node_role_assignments_unique_active
ON node_role_assignments(cluster_id, node_id, role, COALESCE(organization_id, '00000000-0000-0000-0000-000000000000'::uuid))
WHERE status = 'active';
CREATE INDEX IF NOT EXISTS idx_node_role_assignments_cluster
ON node_role_assignments(cluster_id, role, status);
CREATE TABLE IF NOT EXISTS node_heartbeats (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE,
node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
health_status TEXT NOT NULL DEFAULT 'unknown',
reported_version TEXT,
capabilities JSONB NOT NULL DEFAULT '{}'::JSONB,
service_states JSONB NOT NULL DEFAULT '{}'::JSONB,
metadata JSONB NOT NULL DEFAULT '{}'::JSONB,
observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT node_heartbeats_health_status_check
CHECK (health_status IN ('unknown', 'healthy', 'warning', 'critical'))
);
CREATE INDEX IF NOT EXISTS idx_node_heartbeats_cluster_node_observed
ON node_heartbeats(cluster_id, node_id, observed_at DESC);
CREATE TABLE IF NOT EXISTS node_latest_heartbeats (
cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE,
node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
heartbeat_id UUID REFERENCES node_heartbeats(id) ON DELETE SET NULL,
health_status TEXT NOT NULL DEFAULT 'unknown',
reported_version TEXT,
capabilities JSONB NOT NULL DEFAULT '{}'::JSONB,
service_states JSONB NOT NULL DEFAULT '{}'::JSONB,
metadata JSONB NOT NULL DEFAULT '{}'::JSONB,
observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (cluster_id, node_id),
CONSTRAINT node_latest_heartbeats_health_status_check
CHECK (health_status IN ('unknown', 'healthy', 'warning', 'critical'))
);
CREATE TABLE IF NOT EXISTS cluster_audit_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cluster_id UUID REFERENCES clusters(id) ON DELETE SET NULL,
actor_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
event_type TEXT NOT NULL,
target_type TEXT NOT NULL,
target_id TEXT,
payload JSONB NOT NULL DEFAULT '{}'::JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_cluster_audit_events_cluster_created
ON cluster_audit_events(cluster_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_cluster_audit_events_type_created
ON cluster_audit_events(event_type, created_at DESC);