Files
DDI/server/internal/store/migrations/00003_agents.sql
T
beug.k@beugsiud.deandClaude Opus 5.5 8e76c0cf32 feat(store): agents, enrollment tokens, revisions, deployments and leases
Migration 00003 adds the PKI key table, agents, enrollment tokens,
immutable configuration revisions with their files, deployment reports
and DHCP leases, each with database constraints. Repositories and DB
tests for all of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:54:12 +02:00

173 lines
7.7 KiB
PL/PgSQL

-- SPDX-License-Identifier: LicenseRef-DDI-Proprietary
-- Agents (M3, ADR 0021): PKI keys, enrollment tokens, agents, immutable
-- configuration revisions, deployment reports and DHCP leases.
-- +goose Up
-- Key material of the agent PKI: the agent CA (ADR 0008) and the bundle
-- signing key (ADR 0009). Private keys are sealed with the master key; the
-- associated data binds them to their row (ADR 0015). Rows are created once
-- (INSERT ... ON CONFLICT DO NOTHING, so concurrent first starts agree).
CREATE TABLE pki_keys (
purpose text PRIMARY KEY,
certificate bytea,
public_key bytea NOT NULL,
private_key bytea NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT pki_keys_purpose CHECK (purpose IN ('agent_ca', 'bundle_signing')),
CONSTRAINT pki_keys_ca_certificate CHECK (purpose <> 'agent_ca' OR certificate IS NOT NULL)
);
CREATE TYPE agent_role AS ENUM ('dns', 'dhcp');
CREATE TYPE agent_mode AS ENUM ('auto_pull', 'manual');
CREATE TABLE agents (
id uuid PRIMARY KEY,
role agent_role NOT NULL,
hostname text NOT NULL DEFAULT '',
agent_version text NOT NULL DEFAULT '',
service_version text NOT NULL DEFAULT '',
mode agent_mode NOT NULL DEFAULT 'auto_pull',
enrolled_at timestamptz NOT NULL DEFAULT now(),
last_seen_at timestamptz,
remote_address inet,
-- Serial number (lowercase hex) of the only certificate the agent may use.
cert_serial text NOT NULL,
cert_not_after timestamptz NOT NULL,
applied_revision bigint NOT NULL DEFAULT 0,
last_good_revision bigint NOT NULL DEFAULT 0,
health jsonb NOT NULL DEFAULT '{"state": "unknown", "message": "", "checks": []}',
revoked_at timestamptz,
-- Highest lease batch sequence processed (ReportLeases).
lease_sequence bigint NOT NULL DEFAULT 0,
CONSTRAINT agents_cert_serial CHECK (cert_serial ~ '^[0-9a-f]{1,40}$'),
CONSTRAINT agents_revisions CHECK (applied_revision BETWEEN 0 AND 4294967295
AND last_good_revision BETWEEN 0 AND 4294967295),
CONSTRAINT agents_lease_sequence CHECK (lease_sequence >= 0),
CONSTRAINT agents_health_object CHECK (jsonb_typeof(health) = 'object')
);
CREATE UNIQUE INDEX agents_cert_serial_unique ON agents (cert_serial);
CREATE TABLE enrollment_tokens (
id text PRIMARY KEY,
-- SHA-256 of the secret part; the secret has 256 bits of entropy.
secret_hash bytea NOT NULL,
role agent_role NOT NULL,
description text NOT NULL DEFAULT '',
created_by uuid REFERENCES users (id) ON DELETE SET NULL,
created_by_name text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
expires_at timestamptz NOT NULL,
used_at timestamptz,
agent_id uuid REFERENCES agents (id) ON DELETE SET NULL,
CONSTRAINT enrollment_tokens_id_format CHECK (id ~ '^[0-9a-f]{16}$'),
CONSTRAINT enrollment_tokens_secret_hash CHECK (length(secret_hash) = 32),
CONSTRAINT enrollment_tokens_expiry CHECK (expires_at > created_at),
CONSTRAINT enrollment_tokens_agent_used CHECK (agent_id IS NULL OR used_at IS NOT NULL)
);
CREATE INDEX enrollment_tokens_created ON enrollment_tokens (created_at DESC);
-- A revision is immutable; its number is also the SOA serial (uint32).
CREATE TABLE config_revisions (
number bigint PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT now(),
created_by uuid REFERENCES users (id) ON DELETE SET NULL,
created_by_name text NOT NULL,
comment text NOT NULL DEFAULT '',
warnings jsonb NOT NULL DEFAULT '[]',
CONSTRAINT config_revisions_number CHECK (number BETWEEN 1 AND 4294967295),
CONSTRAINT config_revisions_warnings_array CHECK (jsonb_typeof(warnings) = 'array')
);
CREATE TABLE config_revision_files (
revision bigint NOT NULL REFERENCES config_revisions (number) ON DELETE CASCADE,
role agent_role NOT NULL,
-- Position in the generator output (bundles list files in this order).
ordinal integer NOT NULL,
path text NOT NULL,
mode integer NOT NULL,
create_only boolean NOT NULL DEFAULT false,
content bytea NOT NULL,
PRIMARY KEY (revision, role, ordinal),
CONSTRAINT config_revision_files_path_unique UNIQUE (revision, role, path),
CONSTRAINT config_revision_files_ordinal CHECK (ordinal >= 0),
CONSTRAINT config_revision_files_mode CHECK (mode BETWEEN 0 AND 511),
CONSTRAINT config_revision_files_path CHECK (path <> '' AND length(path) <= 255
AND left(path, 1) <> '/' AND path !~ '(^|/)\.')
);
-- Revisions and their files never change once written.
-- +goose StatementBegin
CREATE FUNCTION config_revisions_immutable() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
RAISE EXCEPTION 'configuration revisions are immutable'
USING ERRCODE = 'check_violation', CONSTRAINT = 'config_revisions_immutable';
END;
$$;
-- +goose StatementEnd
CREATE TRIGGER config_revisions_immutable
BEFORE UPDATE ON config_revisions
FOR EACH ROW EXECUTE FUNCTION config_revisions_immutable();
CREATE TRIGGER config_revision_files_immutable
BEFORE UPDATE ON config_revision_files
FOR EACH ROW EXECUTE FUNCTION config_revisions_immutable();
CREATE TYPE deployment_trigger AS ENUM ('pull', 'push');
CREATE TYPE deployment_outcome AS ENUM ('succeeded', 'rejected', 'rolled_back', 'failed');
CREATE TABLE deployments (
id bigserial PRIMARY KEY,
agent_id uuid NOT NULL REFERENCES agents (id) ON DELETE CASCADE,
-- deployment_id of the ApplyRevisionCommand, empty for pulls.
correlation_id text NOT NULL DEFAULT '',
revision bigint NOT NULL,
trigger_kind deployment_trigger NOT NULL,
outcome deployment_outcome NOT NULL,
message text NOT NULL DEFAULT '',
active_revision bigint NOT NULL DEFAULT 0,
started_at timestamptz NOT NULL,
finished_at timestamptz NOT NULL,
reported_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT deployments_revision CHECK (revision BETWEEN 1 AND 4294967295),
CONSTRAINT deployments_active_revision CHECK (active_revision BETWEEN 0 AND 4294967295),
CONSTRAINT deployments_times CHECK (finished_at >= started_at)
);
CREATE INDEX deployments_agent ON deployments (agent_id, id DESC);
CREATE INDEX deployments_revision ON deployments (revision, id DESC);
-- Leases as last reported by each DHCP agent.
CREATE TABLE dhcp_leases (
agent_id uuid NOT NULL REFERENCES agents (id) ON DELETE CASCADE,
ip inet NOT NULL,
mac text NOT NULL DEFAULT '',
hostname text NOT NULL DEFAULT '',
state lease_state NOT NULL,
expires_at timestamptz,
updated_at timestamptz,
subnet_id bigint NOT NULL DEFAULT 0,
client_id text NOT NULL DEFAULT '',
reported_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (agent_id, ip),
CONSTRAINT dhcp_leases_host_ip CHECK (masklen(ip) = CASE family(ip) WHEN 4 THEN 32 ELSE 128 END),
CONSTRAINT dhcp_leases_subnet_id CHECK (subnet_id BETWEEN 0 AND 4294967295)
);
CREATE INDEX dhcp_leases_ip ON dhcp_leases (ip);
CREATE INDEX dhcp_leases_subnet ON dhcp_leases (subnet_id);
-- +goose Down
DROP TABLE dhcp_leases;
DROP TABLE deployments;
DROP TYPE deployment_outcome;
DROP TYPE deployment_trigger;
DROP TRIGGER config_revision_files_immutable ON config_revision_files;
DROP TRIGGER config_revisions_immutable ON config_revisions;
DROP FUNCTION config_revisions_immutable();
DROP TABLE config_revision_files;
DROP TABLE config_revisions;
DROP TABLE enrollment_tokens;
DROP TABLE agents;
DROP TYPE agent_mode;
DROP TYPE agent_role;
DROP TABLE pki_keys;