distributable, please
Some checks failed
CI / Rustfmt (push) Successful in 22s
CI / Cargo Audit & Deny (push) Successful in 36s
CI / Security Blocking Checks (push) Successful in 6s
CI / Web Blocking Checks (push) Successful in 53s
CI / Web Advisory Checks (push) Successful in 34s
Publish Images / Resolve Publish Metadata (push) Successful in 1s
CI / Security Advisory Checks (push) Successful in 38s
CI / Clippy (push) Successful in 2m7s
Publish Images / Publish Docker Dist Bundle (push) Failing after 19s
Publish Images / Publish web (amd64) (push) Successful in 49s
Publish Images / Publish web (arm64) (push) Successful in 3m31s
CI / Tests (push) Successful in 8m48s
Publish Images / Build Rust Bundles (amd64) (push) Successful in 12m42s
Publish Images / Build Rust Bundles (arm64) (push) Successful in 12m19s
Publish Images / Publish agent (amd64) (push) Successful in 26s
Publish Images / Publish api (amd64) (push) Successful in 38s
Publish Images / Publish notifier (amd64) (push) Successful in 42s
Publish Images / Publish executor (amd64) (push) Successful in 46s
Publish Images / Publish agent (arm64) (push) Successful in 56s
Publish Images / Publish api (arm64) (push) Successful in 1m52s
Publish Images / Publish executor (arm64) (push) Successful in 2m2s
Publish Images / Publish notifier (arm64) (push) Successful in 2m3s
Publish Images / Publish manifest attune/agent (push) Successful in 6s
Publish Images / Publish manifest attune/api (push) Successful in 11s
Publish Images / Publish manifest attune/executor (push) Successful in 10s
Publish Images / Publish manifest attune/notifier (push) Successful in 8s
Publish Images / Publish manifest attune/web (push) Successful in 8s
Some checks failed
CI / Rustfmt (push) Successful in 22s
CI / Cargo Audit & Deny (push) Successful in 36s
CI / Security Blocking Checks (push) Successful in 6s
CI / Web Blocking Checks (push) Successful in 53s
CI / Web Advisory Checks (push) Successful in 34s
Publish Images / Resolve Publish Metadata (push) Successful in 1s
CI / Security Advisory Checks (push) Successful in 38s
CI / Clippy (push) Successful in 2m7s
Publish Images / Publish Docker Dist Bundle (push) Failing after 19s
Publish Images / Publish web (amd64) (push) Successful in 49s
Publish Images / Publish web (arm64) (push) Successful in 3m31s
CI / Tests (push) Successful in 8m48s
Publish Images / Build Rust Bundles (amd64) (push) Successful in 12m42s
Publish Images / Build Rust Bundles (arm64) (push) Successful in 12m19s
Publish Images / Publish agent (amd64) (push) Successful in 26s
Publish Images / Publish api (amd64) (push) Successful in 38s
Publish Images / Publish notifier (amd64) (push) Successful in 42s
Publish Images / Publish executor (amd64) (push) Successful in 46s
Publish Images / Publish agent (arm64) (push) Successful in 56s
Publish Images / Publish api (arm64) (push) Successful in 1m52s
Publish Images / Publish executor (arm64) (push) Successful in 2m2s
Publish Images / Publish notifier (arm64) (push) Successful in 2m3s
Publish Images / Publish manifest attune/agent (push) Successful in 6s
Publish Images / Publish manifest attune/api (push) Successful in 11s
Publish Images / Publish manifest attune/executor (push) Successful in 10s
Publish Images / Publish manifest attune/notifier (push) Successful in 8s
Publish Images / Publish manifest attune/web (push) Successful in 8s
This commit is contained in:
@@ -0,0 +1,779 @@
|
||||
-- Migration: Supporting Systems
|
||||
-- Description: Creates keys, artifacts, queue_stats, pack_environment, pack_testing,
|
||||
-- and webhook function tables.
|
||||
-- Consolidates former migrations: 000009 (keys_artifacts), 000010 (webhook_system),
|
||||
-- 000011 (pack_environments), and 000012 (pack_testing).
|
||||
-- Version: 20250101000007
|
||||
|
||||
-- ============================================================================
|
||||
-- KEY TABLE
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE key (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
ref TEXT NOT NULL UNIQUE,
|
||||
owner_type owner_type_enum NOT NULL,
|
||||
owner TEXT,
|
||||
owner_identity BIGINT REFERENCES identity(id),
|
||||
owner_pack BIGINT REFERENCES pack(id),
|
||||
owner_pack_ref TEXT,
|
||||
owner_action BIGINT, -- Forward reference to action table
|
||||
owner_action_ref TEXT,
|
||||
owner_sensor BIGINT, -- Forward reference to sensor table
|
||||
owner_sensor_ref TEXT,
|
||||
name TEXT NOT NULL,
|
||||
encrypted BOOLEAN NOT NULL,
|
||||
encryption_key_hash TEXT,
|
||||
value TEXT NOT NULL,
|
||||
created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT key_ref_lowercase CHECK (ref = LOWER(ref)),
|
||||
CONSTRAINT key_ref_format CHECK (ref ~ '^[^.]+(\.[^.]+)*$')
|
||||
);
|
||||
|
||||
-- Unique index on owner_type, owner, name
|
||||
CREATE UNIQUE INDEX idx_key_unique ON key(owner_type, owner, name);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_key_ref ON key(ref);
|
||||
CREATE INDEX idx_key_owner_type ON key(owner_type);
|
||||
CREATE INDEX idx_key_owner_identity ON key(owner_identity);
|
||||
CREATE INDEX idx_key_owner_pack ON key(owner_pack);
|
||||
CREATE INDEX idx_key_owner_action ON key(owner_action);
|
||||
CREATE INDEX idx_key_owner_sensor ON key(owner_sensor);
|
||||
CREATE INDEX idx_key_created ON key(created DESC);
|
||||
CREATE INDEX idx_key_owner_type_owner ON key(owner_type, owner);
|
||||
CREATE INDEX idx_key_owner_identity_name ON key(owner_identity, name);
|
||||
CREATE INDEX idx_key_owner_pack_name ON key(owner_pack, name);
|
||||
|
||||
-- Function to validate and set owner fields
|
||||
CREATE OR REPLACE FUNCTION validate_key_owner()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
owner_count INTEGER := 0;
|
||||
BEGIN
|
||||
-- Count how many owner fields are set
|
||||
IF NEW.owner_identity IS NOT NULL THEN owner_count := owner_count + 1; END IF;
|
||||
IF NEW.owner_pack IS NOT NULL THEN owner_count := owner_count + 1; END IF;
|
||||
IF NEW.owner_action IS NOT NULL THEN owner_count := owner_count + 1; END IF;
|
||||
IF NEW.owner_sensor IS NOT NULL THEN owner_count := owner_count + 1; END IF;
|
||||
|
||||
-- System owner should have no owner fields set
|
||||
IF NEW.owner_type = 'system' THEN
|
||||
IF owner_count > 0 THEN
|
||||
RAISE EXCEPTION 'System owner cannot have specific owner fields set';
|
||||
END IF;
|
||||
NEW.owner := 'system';
|
||||
-- All other types must have exactly one owner field set
|
||||
ELSIF owner_count != 1 THEN
|
||||
RAISE EXCEPTION 'Exactly one owner field must be set for owner_type %', NEW.owner_type;
|
||||
-- Validate owner_type matches the populated field and set owner
|
||||
ELSIF NEW.owner_type = 'identity' THEN
|
||||
IF NEW.owner_identity IS NULL THEN
|
||||
RAISE EXCEPTION 'owner_identity must be set for owner_type identity';
|
||||
END IF;
|
||||
NEW.owner := NEW.owner_identity::TEXT;
|
||||
ELSIF NEW.owner_type = 'pack' THEN
|
||||
IF NEW.owner_pack IS NULL THEN
|
||||
RAISE EXCEPTION 'owner_pack must be set for owner_type pack';
|
||||
END IF;
|
||||
NEW.owner := NEW.owner_pack::TEXT;
|
||||
ELSIF NEW.owner_type = 'action' THEN
|
||||
IF NEW.owner_action IS NULL THEN
|
||||
RAISE EXCEPTION 'owner_action must be set for owner_type action';
|
||||
END IF;
|
||||
NEW.owner := NEW.owner_action::TEXT;
|
||||
ELSIF NEW.owner_type = 'sensor' THEN
|
||||
IF NEW.owner_sensor IS NULL THEN
|
||||
RAISE EXCEPTION 'owner_sensor must be set for owner_type sensor';
|
||||
END IF;
|
||||
NEW.owner := NEW.owner_sensor::TEXT;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Trigger to validate owner fields
|
||||
CREATE TRIGGER validate_key_owner_trigger
|
||||
BEFORE INSERT OR UPDATE ON key
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION validate_key_owner();
|
||||
|
||||
-- Trigger for updated timestamp
|
||||
CREATE TRIGGER update_key_updated
|
||||
BEFORE UPDATE ON key
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_column();
|
||||
|
||||
-- Comments
|
||||
COMMENT ON TABLE key IS 'Keys store configuration values and secrets with ownership scoping';
|
||||
COMMENT ON COLUMN key.ref IS 'Unique key reference (format: [owner.]name)';
|
||||
COMMENT ON COLUMN key.owner_type IS 'Type of owner (system, identity, pack, action, sensor)';
|
||||
COMMENT ON COLUMN key.owner IS 'Owner identifier (auto-populated by trigger)';
|
||||
COMMENT ON COLUMN key.owner_identity IS 'Identity owner (if owner_type=identity)';
|
||||
COMMENT ON COLUMN key.owner_pack IS 'Pack owner (if owner_type=pack)';
|
||||
COMMENT ON COLUMN key.owner_pack_ref IS 'Pack reference for owner_pack';
|
||||
COMMENT ON COLUMN key.owner_action IS 'Action owner (if owner_type=action)';
|
||||
COMMENT ON COLUMN key.owner_sensor IS 'Sensor owner (if owner_type=sensor)';
|
||||
COMMENT ON COLUMN key.name IS 'Key name within owner scope';
|
||||
COMMENT ON COLUMN key.encrypted IS 'Whether the value is encrypted';
|
||||
COMMENT ON COLUMN key.encryption_key_hash IS 'Hash of encryption key used';
|
||||
COMMENT ON COLUMN key.value IS 'The actual value (encrypted if encrypted=true)';
|
||||
|
||||
|
||||
-- Add foreign key constraints for action and sensor references
|
||||
ALTER TABLE key
|
||||
ADD CONSTRAINT key_owner_action_fkey
|
||||
FOREIGN KEY (owner_action) REFERENCES action(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE key
|
||||
ADD CONSTRAINT key_owner_sensor_fkey
|
||||
FOREIGN KEY (owner_sensor) REFERENCES sensor(id) ON DELETE CASCADE;
|
||||
|
||||
-- ============================================================================
|
||||
-- ARTIFACT TABLE
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE artifact (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
ref TEXT NOT NULL,
|
||||
scope owner_type_enum NOT NULL DEFAULT 'system',
|
||||
owner TEXT NOT NULL DEFAULT '',
|
||||
type artifact_type_enum NOT NULL,
|
||||
visibility artifact_visibility_enum NOT NULL DEFAULT 'private',
|
||||
retention_policy artifact_retention_enum NOT NULL DEFAULT 'versions',
|
||||
retention_limit INTEGER NOT NULL DEFAULT 1,
|
||||
created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_artifact_ref ON artifact(ref);
|
||||
CREATE INDEX idx_artifact_scope ON artifact(scope);
|
||||
CREATE INDEX idx_artifact_owner ON artifact(owner);
|
||||
CREATE INDEX idx_artifact_type ON artifact(type);
|
||||
CREATE INDEX idx_artifact_created ON artifact(created DESC);
|
||||
CREATE INDEX idx_artifact_scope_owner ON artifact(scope, owner);
|
||||
CREATE INDEX idx_artifact_type_created ON artifact(type, created DESC);
|
||||
CREATE INDEX idx_artifact_visibility ON artifact(visibility);
|
||||
CREATE INDEX idx_artifact_visibility_scope ON artifact(visibility, scope, owner);
|
||||
|
||||
-- Trigger
|
||||
CREATE TRIGGER update_artifact_updated
|
||||
BEFORE UPDATE ON artifact
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_column();
|
||||
|
||||
-- Comments
|
||||
COMMENT ON TABLE artifact IS 'Artifacts track files, logs, and outputs from executions';
|
||||
COMMENT ON COLUMN artifact.ref IS 'Artifact reference/path';
|
||||
COMMENT ON COLUMN artifact.scope IS 'Owner type (system, identity, pack, action, sensor)';
|
||||
COMMENT ON COLUMN artifact.owner IS 'Owner identifier';
|
||||
COMMENT ON COLUMN artifact.type IS 'Artifact type (file, url, progress, etc.)';
|
||||
COMMENT ON COLUMN artifact.visibility IS 'Visibility level: public (all users) or private (scoped by scope/owner)';
|
||||
COMMENT ON COLUMN artifact.retention_policy IS 'How to retain artifacts (versions, days, hours, minutes)';
|
||||
COMMENT ON COLUMN artifact.retention_limit IS 'Numeric limit for retention policy';
|
||||
|
||||
-- ============================================================================
|
||||
-- QUEUE_STATS TABLE
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE queue_stats (
|
||||
action_id BIGINT PRIMARY KEY REFERENCES action(id) ON DELETE CASCADE,
|
||||
queue_length INTEGER NOT NULL DEFAULT 0,
|
||||
active_count INTEGER NOT NULL DEFAULT 0,
|
||||
max_concurrent INTEGER NOT NULL DEFAULT 1,
|
||||
oldest_enqueued_at TIMESTAMPTZ,
|
||||
total_enqueued BIGINT NOT NULL DEFAULT 0,
|
||||
total_completed BIGINT NOT NULL DEFAULT 0,
|
||||
last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_queue_stats_last_updated ON queue_stats(last_updated);
|
||||
|
||||
-- Comments
|
||||
COMMENT ON TABLE queue_stats IS 'Real-time queue statistics for action execution ordering';
|
||||
COMMENT ON COLUMN queue_stats.action_id IS 'Foreign key to action table';
|
||||
COMMENT ON COLUMN queue_stats.queue_length IS 'Number of executions waiting in queue';
|
||||
COMMENT ON COLUMN queue_stats.active_count IS 'Number of currently running executions';
|
||||
COMMENT ON COLUMN queue_stats.max_concurrent IS 'Maximum concurrent executions allowed';
|
||||
COMMENT ON COLUMN queue_stats.oldest_enqueued_at IS 'Timestamp of oldest queued execution (NULL if queue empty)';
|
||||
COMMENT ON COLUMN queue_stats.total_enqueued IS 'Total executions enqueued since queue creation';
|
||||
COMMENT ON COLUMN queue_stats.total_completed IS 'Total executions completed since queue creation';
|
||||
COMMENT ON COLUMN queue_stats.last_updated IS 'Timestamp of last statistics update';
|
||||
|
||||
-- ============================================================================
|
||||
-- PACK ENVIRONMENT TABLE
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pack_environment (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
pack BIGINT NOT NULL REFERENCES pack(id) ON DELETE CASCADE,
|
||||
pack_ref TEXT NOT NULL,
|
||||
runtime BIGINT NOT NULL REFERENCES runtime(id) ON DELETE CASCADE,
|
||||
runtime_ref TEXT NOT NULL,
|
||||
env_path TEXT NOT NULL,
|
||||
status pack_environment_status_enum NOT NULL DEFAULT 'pending',
|
||||
installed_at TIMESTAMPTZ,
|
||||
last_verified TIMESTAMPTZ,
|
||||
install_log TEXT,
|
||||
install_error TEXT,
|
||||
metadata JSONB DEFAULT '{}'::jsonb,
|
||||
created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(pack, runtime)
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_pack_environment_pack ON pack_environment(pack);
|
||||
CREATE INDEX IF NOT EXISTS idx_pack_environment_runtime ON pack_environment(runtime);
|
||||
CREATE INDEX IF NOT EXISTS idx_pack_environment_status ON pack_environment(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_pack_environment_pack_ref ON pack_environment(pack_ref);
|
||||
CREATE INDEX IF NOT EXISTS idx_pack_environment_runtime_ref ON pack_environment(runtime_ref);
|
||||
CREATE INDEX IF NOT EXISTS idx_pack_environment_pack_runtime ON pack_environment(pack, runtime);
|
||||
|
||||
-- Trigger for updated timestamp
|
||||
CREATE TRIGGER update_pack_environment_updated
|
||||
BEFORE UPDATE ON pack_environment
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_column();
|
||||
|
||||
-- Comments
|
||||
COMMENT ON TABLE pack_environment IS 'Tracks pack-specific runtime environments for dependency isolation';
|
||||
COMMENT ON COLUMN pack_environment.pack IS 'Pack that owns this environment';
|
||||
COMMENT ON COLUMN pack_environment.pack_ref IS 'Pack reference for quick lookup';
|
||||
COMMENT ON COLUMN pack_environment.runtime IS 'Runtime used for this environment';
|
||||
COMMENT ON COLUMN pack_environment.runtime_ref IS 'Runtime reference for quick lookup';
|
||||
COMMENT ON COLUMN pack_environment.env_path IS 'Filesystem path to the environment directory (e.g., /opt/attune/packenvs/mypack/python)';
|
||||
COMMENT ON COLUMN pack_environment.status IS 'Current installation status';
|
||||
COMMENT ON COLUMN pack_environment.installed_at IS 'When the environment was successfully installed';
|
||||
COMMENT ON COLUMN pack_environment.last_verified IS 'Last time the environment was verified as working';
|
||||
COMMENT ON COLUMN pack_environment.install_log IS 'Installation output logs';
|
||||
COMMENT ON COLUMN pack_environment.install_error IS 'Error message if installation failed';
|
||||
COMMENT ON COLUMN pack_environment.metadata IS 'Additional metadata (installed packages, versions, etc.)';
|
||||
|
||||
-- ============================================================================
|
||||
-- PACK ENVIRONMENT: Update existing runtimes with installer metadata
|
||||
-- ============================================================================
|
||||
|
||||
-- Python runtime installers
|
||||
UPDATE runtime
|
||||
SET installers = jsonb_build_object(
|
||||
'base_path_template', '/opt/attune/packenvs/{pack_ref}/{runtime_name_lower}',
|
||||
'installers', jsonb_build_array(
|
||||
jsonb_build_object(
|
||||
'name', 'create_venv',
|
||||
'description', 'Create Python virtual environment',
|
||||
'command', 'python3',
|
||||
'args', jsonb_build_array('-m', 'venv', '{env_path}'),
|
||||
'cwd', '{pack_path}',
|
||||
'env', jsonb_build_object(),
|
||||
'order', 1,
|
||||
'optional', false
|
||||
),
|
||||
jsonb_build_object(
|
||||
'name', 'upgrade_pip',
|
||||
'description', 'Upgrade pip to latest version',
|
||||
'command', '{env_path}/bin/pip',
|
||||
'args', jsonb_build_array('install', '--upgrade', 'pip'),
|
||||
'cwd', '{pack_path}',
|
||||
'env', jsonb_build_object(),
|
||||
'order', 2,
|
||||
'optional', true
|
||||
),
|
||||
jsonb_build_object(
|
||||
'name', 'install_requirements',
|
||||
'description', 'Install pack Python dependencies',
|
||||
'command', '{env_path}/bin/pip',
|
||||
'args', jsonb_build_array('install', '-r', '{pack_path}/requirements.txt'),
|
||||
'cwd', '{pack_path}',
|
||||
'env', jsonb_build_object(),
|
||||
'order', 3,
|
||||
'optional', false,
|
||||
'condition', jsonb_build_object(
|
||||
'file_exists', '{pack_path}/requirements.txt'
|
||||
)
|
||||
)
|
||||
),
|
||||
'executable_templates', jsonb_build_object(
|
||||
'python', '{env_path}/bin/python',
|
||||
'pip', '{env_path}/bin/pip'
|
||||
)
|
||||
)
|
||||
WHERE ref = 'core.python';
|
||||
|
||||
-- Node.js runtime installers
|
||||
UPDATE runtime
|
||||
SET installers = jsonb_build_object(
|
||||
'base_path_template', '/opt/attune/packenvs/{pack_ref}/{runtime_name_lower}',
|
||||
'installers', jsonb_build_array(
|
||||
jsonb_build_object(
|
||||
'name', 'npm_install',
|
||||
'description', 'Install Node.js dependencies',
|
||||
'command', 'npm',
|
||||
'args', jsonb_build_array('install', '--prefix', '{env_path}'),
|
||||
'cwd', '{pack_path}',
|
||||
'env', jsonb_build_object(
|
||||
'NODE_PATH', '{env_path}/node_modules'
|
||||
),
|
||||
'order', 1,
|
||||
'optional', false,
|
||||
'condition', jsonb_build_object(
|
||||
'file_exists', '{pack_path}/package.json'
|
||||
)
|
||||
)
|
||||
),
|
||||
'executable_templates', jsonb_build_object(
|
||||
'node', 'node',
|
||||
'npm', 'npm'
|
||||
),
|
||||
'env_vars', jsonb_build_object(
|
||||
'NODE_PATH', '{env_path}/node_modules'
|
||||
)
|
||||
)
|
||||
WHERE ref = 'core.nodejs';
|
||||
|
||||
-- Shell runtime (no environment needed, uses system shell)
|
||||
UPDATE runtime
|
||||
SET installers = jsonb_build_object(
|
||||
'base_path_template', '/opt/attune/packenvs/{pack_ref}/{runtime_name_lower}',
|
||||
'installers', jsonb_build_array(),
|
||||
'executable_templates', jsonb_build_object(
|
||||
'sh', 'sh',
|
||||
'bash', 'bash'
|
||||
),
|
||||
'requires_environment', false
|
||||
)
|
||||
WHERE ref = 'core.shell';
|
||||
|
||||
-- Native runtime (no environment needed, binaries are standalone)
|
||||
UPDATE runtime
|
||||
SET installers = jsonb_build_object(
|
||||
'base_path_template', '/opt/attune/packenvs/{pack_ref}/{runtime_name_lower}',
|
||||
'installers', jsonb_build_array(),
|
||||
'executable_templates', jsonb_build_object(),
|
||||
'requires_environment', false
|
||||
)
|
||||
WHERE ref = 'core.native';
|
||||
|
||||
-- Built-in sensor runtime (internal, no environment)
|
||||
UPDATE runtime
|
||||
SET installers = jsonb_build_object(
|
||||
'installers', jsonb_build_array(),
|
||||
'requires_environment', false
|
||||
)
|
||||
WHERE ref = 'core.sensor.builtin';
|
||||
|
||||
-- ============================================================================
|
||||
-- PACK ENVIRONMENT: Helper functions
|
||||
-- ============================================================================
|
||||
|
||||
-- Function to get environment path for a pack/runtime combination
|
||||
CREATE OR REPLACE FUNCTION get_pack_environment_path(p_pack_ref TEXT, p_runtime_ref TEXT)
|
||||
RETURNS TEXT AS $$
|
||||
DECLARE
|
||||
v_runtime_name TEXT;
|
||||
v_base_template TEXT;
|
||||
v_result TEXT;
|
||||
BEGIN
|
||||
-- Get runtime name and base path template
|
||||
SELECT
|
||||
LOWER(name),
|
||||
installers->>'base_path_template'
|
||||
INTO v_runtime_name, v_base_template
|
||||
FROM runtime
|
||||
WHERE ref = p_runtime_ref;
|
||||
|
||||
IF v_base_template IS NULL THEN
|
||||
v_base_template := '/opt/attune/packenvs/{pack_ref}/{runtime_name_lower}';
|
||||
END IF;
|
||||
|
||||
-- Replace template variables
|
||||
v_result := v_base_template;
|
||||
v_result := REPLACE(v_result, '{pack_ref}', p_pack_ref);
|
||||
v_result := REPLACE(v_result, '{runtime_ref}', p_runtime_ref);
|
||||
v_result := REPLACE(v_result, '{runtime_name_lower}', v_runtime_name);
|
||||
|
||||
RETURN v_result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql IMMUTABLE;
|
||||
|
||||
COMMENT ON FUNCTION get_pack_environment_path IS 'Calculate the filesystem path for a pack runtime environment';
|
||||
|
||||
-- Function to check if a runtime requires an environment
|
||||
CREATE OR REPLACE FUNCTION runtime_requires_environment(p_runtime_ref TEXT)
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
v_requires BOOLEAN;
|
||||
BEGIN
|
||||
SELECT COALESCE((installers->>'requires_environment')::boolean, true)
|
||||
INTO v_requires
|
||||
FROM runtime
|
||||
WHERE ref = p_runtime_ref;
|
||||
|
||||
RETURN COALESCE(v_requires, false);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
COMMENT ON FUNCTION runtime_requires_environment IS 'Check if a runtime needs a pack-specific environment';
|
||||
|
||||
-- ============================================================================
|
||||
-- PACK ENVIRONMENT: Status view
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE VIEW v_pack_environment_status AS
|
||||
SELECT
|
||||
pe.id,
|
||||
pe.pack,
|
||||
p.ref AS pack_ref,
|
||||
p.label AS pack_name,
|
||||
pe.runtime,
|
||||
r.ref AS runtime_ref,
|
||||
r.name AS runtime_name,
|
||||
pe.env_path,
|
||||
pe.status,
|
||||
pe.installed_at,
|
||||
pe.last_verified,
|
||||
CASE
|
||||
WHEN pe.status = 'ready' AND pe.last_verified < NOW() - INTERVAL '7 days' THEN true
|
||||
ELSE false
|
||||
END AS needs_verification,
|
||||
CASE
|
||||
WHEN pe.status = 'ready' THEN 'healthy'
|
||||
WHEN pe.status = 'failed' THEN 'unhealthy'
|
||||
WHEN pe.status IN ('pending', 'installing') THEN 'provisioning'
|
||||
WHEN pe.status = 'outdated' THEN 'needs_update'
|
||||
ELSE 'unknown'
|
||||
END AS health_status,
|
||||
pe.install_error,
|
||||
pe.created,
|
||||
pe.updated
|
||||
FROM pack_environment pe
|
||||
JOIN pack p ON pe.pack = p.id
|
||||
JOIN runtime r ON pe.runtime = r.id;
|
||||
|
||||
COMMENT ON VIEW v_pack_environment_status IS 'Consolidated view of pack environment status with health indicators';
|
||||
|
||||
-- ============================================================================
|
||||
-- PACK TEST EXECUTION TABLE
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pack_test_execution (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
pack_id BIGINT NOT NULL REFERENCES pack(id) ON DELETE CASCADE,
|
||||
pack_version VARCHAR(50) NOT NULL,
|
||||
execution_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
trigger_reason VARCHAR(50) NOT NULL, -- 'install', 'update', 'manual', 'validation'
|
||||
total_tests INT NOT NULL,
|
||||
passed INT NOT NULL,
|
||||
failed INT NOT NULL,
|
||||
skipped INT NOT NULL,
|
||||
pass_rate DECIMAL(5,4) NOT NULL, -- 0.0000 to 1.0000
|
||||
duration_ms BIGINT NOT NULL,
|
||||
result JSONB NOT NULL, -- Full test result structure
|
||||
created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT valid_test_counts CHECK (total_tests >= 0 AND passed >= 0 AND failed >= 0 AND skipped >= 0),
|
||||
CONSTRAINT valid_pass_rate CHECK (pass_rate >= 0.0 AND pass_rate <= 1.0),
|
||||
CONSTRAINT valid_trigger_reason CHECK (trigger_reason IN ('install', 'update', 'manual', 'validation'))
|
||||
);
|
||||
|
||||
-- Indexes for efficient queries
|
||||
CREATE INDEX idx_pack_test_execution_pack_id ON pack_test_execution(pack_id);
|
||||
CREATE INDEX idx_pack_test_execution_time ON pack_test_execution(execution_time DESC);
|
||||
CREATE INDEX idx_pack_test_execution_pass_rate ON pack_test_execution(pass_rate);
|
||||
CREATE INDEX idx_pack_test_execution_trigger ON pack_test_execution(trigger_reason);
|
||||
|
||||
-- Comments for documentation
|
||||
COMMENT ON TABLE pack_test_execution IS 'Tracks pack test execution results for validation and auditing';
|
||||
COMMENT ON COLUMN pack_test_execution.pack_id IS 'Reference to the pack being tested';
|
||||
COMMENT ON COLUMN pack_test_execution.pack_version IS 'Version of the pack at test time';
|
||||
COMMENT ON COLUMN pack_test_execution.trigger_reason IS 'What triggered the test: install, update, manual, validation';
|
||||
COMMENT ON COLUMN pack_test_execution.pass_rate IS 'Percentage of tests passed (0.0 to 1.0)';
|
||||
COMMENT ON COLUMN pack_test_execution.result IS 'Full JSON structure with detailed test results';
|
||||
|
||||
-- Pack test result summary view (all test executions with pack info)
|
||||
CREATE OR REPLACE VIEW pack_test_summary AS
|
||||
SELECT
|
||||
p.id AS pack_id,
|
||||
p.ref AS pack_ref,
|
||||
p.label AS pack_label,
|
||||
pte.id AS test_execution_id,
|
||||
pte.pack_version,
|
||||
pte.execution_time AS test_time,
|
||||
pte.trigger_reason,
|
||||
pte.total_tests,
|
||||
pte.passed,
|
||||
pte.failed,
|
||||
pte.skipped,
|
||||
pte.pass_rate,
|
||||
pte.duration_ms,
|
||||
ROW_NUMBER() OVER (PARTITION BY p.id ORDER BY pte.execution_time DESC) AS rn
|
||||
FROM pack p
|
||||
LEFT JOIN pack_test_execution pte ON p.id = pte.pack_id
|
||||
WHERE pte.id IS NOT NULL;
|
||||
|
||||
COMMENT ON VIEW pack_test_summary IS 'Summary of all pack test executions with pack details';
|
||||
|
||||
-- Latest test results per pack view
|
||||
CREATE OR REPLACE VIEW pack_latest_test AS
|
||||
SELECT
|
||||
pack_id,
|
||||
pack_ref,
|
||||
pack_label,
|
||||
test_execution_id,
|
||||
pack_version,
|
||||
test_time,
|
||||
trigger_reason,
|
||||
total_tests,
|
||||
passed,
|
||||
failed,
|
||||
skipped,
|
||||
pass_rate,
|
||||
duration_ms
|
||||
FROM pack_test_summary
|
||||
WHERE rn = 1;
|
||||
|
||||
COMMENT ON VIEW pack_latest_test IS 'Latest test results for each pack';
|
||||
|
||||
-- Function to get pack test statistics
|
||||
CREATE OR REPLACE FUNCTION get_pack_test_stats(p_pack_id BIGINT)
|
||||
RETURNS TABLE (
|
||||
total_executions BIGINT,
|
||||
successful_executions BIGINT,
|
||||
failed_executions BIGINT,
|
||||
avg_pass_rate DECIMAL,
|
||||
avg_duration_ms BIGINT,
|
||||
last_test_time TIMESTAMPTZ,
|
||||
last_test_passed BOOLEAN
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
COUNT(*)::BIGINT AS total_executions,
|
||||
COUNT(*) FILTER (WHERE passed = total_tests)::BIGINT AS successful_executions,
|
||||
COUNT(*) FILTER (WHERE failed > 0)::BIGINT AS failed_executions,
|
||||
AVG(pass_rate) AS avg_pass_rate,
|
||||
AVG(duration_ms)::BIGINT AS avg_duration_ms,
|
||||
MAX(execution_time) AS last_test_time,
|
||||
(SELECT failed = 0 FROM pack_test_execution
|
||||
WHERE pack_id = p_pack_id
|
||||
ORDER BY execution_time DESC
|
||||
LIMIT 1) AS last_test_passed
|
||||
FROM pack_test_execution
|
||||
WHERE pack_id = p_pack_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION get_pack_test_stats IS 'Get statistical summary of test executions for a pack';
|
||||
|
||||
-- Function to check if pack has recent passing tests
|
||||
CREATE OR REPLACE FUNCTION pack_has_passing_tests(
|
||||
p_pack_id BIGINT,
|
||||
p_hours_ago INT DEFAULT 24
|
||||
)
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
v_has_passing_tests BOOLEAN;
|
||||
BEGIN
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM pack_test_execution
|
||||
WHERE pack_id = p_pack_id
|
||||
AND execution_time > NOW() - (p_hours_ago || ' hours')::INTERVAL
|
||||
AND failed = 0
|
||||
AND total_tests > 0
|
||||
) INTO v_has_passing_tests;
|
||||
|
||||
RETURN v_has_passing_tests;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION pack_has_passing_tests IS 'Check if pack has recent passing test executions';
|
||||
|
||||
-- Add trigger to update pack metadata on test execution
|
||||
CREATE OR REPLACE FUNCTION update_pack_test_metadata()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Could update pack table with last_tested timestamp if we add that column
|
||||
-- For now, just a placeholder for future functionality
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trigger_update_pack_test_metadata
|
||||
AFTER INSERT ON pack_test_execution
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_pack_test_metadata();
|
||||
|
||||
COMMENT ON TRIGGER trigger_update_pack_test_metadata ON pack_test_execution IS 'Updates pack metadata when tests are executed';
|
||||
|
||||
-- ============================================================================
|
||||
-- WEBHOOK FUNCTIONS
|
||||
-- ============================================================================
|
||||
|
||||
-- Drop existing functions to avoid signature conflicts
|
||||
DROP FUNCTION IF EXISTS enable_trigger_webhook(BIGINT, JSONB);
|
||||
DROP FUNCTION IF EXISTS enable_trigger_webhook(BIGINT);
|
||||
DROP FUNCTION IF EXISTS disable_trigger_webhook(BIGINT);
|
||||
DROP FUNCTION IF EXISTS regenerate_trigger_webhook_key(BIGINT);
|
||||
|
||||
-- Function to enable webhooks for a trigger
|
||||
CREATE OR REPLACE FUNCTION enable_trigger_webhook(
|
||||
p_trigger_id BIGINT,
|
||||
p_config JSONB DEFAULT '{}'::jsonb
|
||||
)
|
||||
RETURNS TABLE(
|
||||
webhook_enabled BOOLEAN,
|
||||
webhook_key VARCHAR(255),
|
||||
webhook_url TEXT
|
||||
) AS $$
|
||||
DECLARE
|
||||
v_webhook_key VARCHAR(255);
|
||||
v_api_base_url TEXT := 'http://localhost:8080'; -- Default, should be configured
|
||||
BEGIN
|
||||
-- Check if trigger exists
|
||||
IF NOT EXISTS (SELECT 1 FROM trigger WHERE id = p_trigger_id) THEN
|
||||
RAISE EXCEPTION 'Trigger with id % does not exist', p_trigger_id;
|
||||
END IF;
|
||||
|
||||
-- Generate webhook key if one doesn't exist
|
||||
SELECT t.webhook_key INTO v_webhook_key
|
||||
FROM trigger t
|
||||
WHERE t.id = p_trigger_id;
|
||||
|
||||
IF v_webhook_key IS NULL THEN
|
||||
v_webhook_key := generate_webhook_key();
|
||||
END IF;
|
||||
|
||||
-- Update trigger to enable webhooks
|
||||
UPDATE trigger
|
||||
SET
|
||||
webhook_enabled = TRUE,
|
||||
webhook_key = v_webhook_key,
|
||||
webhook_config = p_config,
|
||||
updated = NOW()
|
||||
WHERE id = p_trigger_id;
|
||||
|
||||
-- Return webhook details
|
||||
RETURN QUERY SELECT
|
||||
TRUE,
|
||||
v_webhook_key,
|
||||
v_api_base_url || '/api/v1/webhooks/' || v_webhook_key;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION enable_trigger_webhook(BIGINT, JSONB) IS
|
||||
'Enables webhooks for a trigger with optional configuration. Generates a new webhook key if one does not exist. Returns webhook details.';
|
||||
|
||||
-- Function to disable webhooks for a trigger
|
||||
CREATE OR REPLACE FUNCTION disable_trigger_webhook(
|
||||
p_trigger_id BIGINT
|
||||
)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
-- Check if trigger exists
|
||||
IF NOT EXISTS (SELECT 1 FROM trigger WHERE id = p_trigger_id) THEN
|
||||
RAISE EXCEPTION 'Trigger with id % does not exist', p_trigger_id;
|
||||
END IF;
|
||||
|
||||
-- Update trigger to disable webhooks
|
||||
-- Set webhook_key to NULL when disabling to remove it from API responses
|
||||
UPDATE trigger
|
||||
SET
|
||||
webhook_enabled = FALSE,
|
||||
webhook_key = NULL,
|
||||
updated = NOW()
|
||||
WHERE id = p_trigger_id;
|
||||
|
||||
RETURN TRUE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION disable_trigger_webhook(BIGINT) IS
|
||||
'Disables webhooks for a trigger. Webhook key is removed when disabled.';
|
||||
|
||||
-- Function to regenerate webhook key for a trigger
|
||||
CREATE OR REPLACE FUNCTION regenerate_trigger_webhook_key(
|
||||
p_trigger_id BIGINT
|
||||
)
|
||||
RETURNS TABLE(
|
||||
webhook_key VARCHAR(255),
|
||||
previous_key_revoked BOOLEAN
|
||||
) AS $$
|
||||
DECLARE
|
||||
v_new_key VARCHAR(255);
|
||||
v_old_key VARCHAR(255);
|
||||
v_webhook_enabled BOOLEAN;
|
||||
BEGIN
|
||||
-- Check if trigger exists
|
||||
IF NOT EXISTS (SELECT 1 FROM trigger WHERE id = p_trigger_id) THEN
|
||||
RAISE EXCEPTION 'Trigger with id % does not exist', p_trigger_id;
|
||||
END IF;
|
||||
|
||||
-- Get current webhook state
|
||||
SELECT t.webhook_key, t.webhook_enabled INTO v_old_key, v_webhook_enabled
|
||||
FROM trigger t
|
||||
WHERE t.id = p_trigger_id;
|
||||
|
||||
-- Check if webhooks are enabled
|
||||
IF NOT v_webhook_enabled THEN
|
||||
RAISE EXCEPTION 'Webhooks are not enabled for trigger %', p_trigger_id;
|
||||
END IF;
|
||||
|
||||
-- Generate new key
|
||||
v_new_key := generate_webhook_key();
|
||||
|
||||
-- Update trigger with new key
|
||||
UPDATE trigger
|
||||
SET
|
||||
webhook_key = v_new_key,
|
||||
updated = NOW()
|
||||
WHERE id = p_trigger_id;
|
||||
|
||||
-- Return new key and whether old key was present
|
||||
RETURN QUERY SELECT
|
||||
v_new_key,
|
||||
(v_old_key IS NOT NULL);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION regenerate_trigger_webhook_key(BIGINT) IS
|
||||
'Regenerates webhook key for a trigger. Returns new key and whether a previous key was revoked.';
|
||||
|
||||
-- Verify all webhook functions exist
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_proc p
|
||||
JOIN pg_namespace n ON p.pronamespace = n.oid
|
||||
WHERE n.nspname = current_schema()
|
||||
AND p.proname = 'enable_trigger_webhook'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'enable_trigger_webhook function not found after migration';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_proc p
|
||||
JOIN pg_namespace n ON p.pronamespace = n.oid
|
||||
WHERE n.nspname = current_schema()
|
||||
AND p.proname = 'disable_trigger_webhook'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'disable_trigger_webhook function not found after migration';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_proc p
|
||||
JOIN pg_namespace n ON p.pronamespace = n.oid
|
||||
WHERE n.nspname = current_schema()
|
||||
AND p.proname = 'regenerate_trigger_webhook_key'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'regenerate_trigger_webhook_key function not found after migration';
|
||||
END IF;
|
||||
|
||||
RAISE NOTICE 'All webhook functions successfully created';
|
||||
END $$;
|
||||
Reference in New Issue
Block a user