244 lines
12 KiB
PHP
244 lines
12 KiB
PHP
<?php
|
|
|
|
namespace classes;
|
|
|
|
/**
|
|
* Additive schema bootstrap for the Coolify infrastructure integration.
|
|
*
|
|
* The legacy stack has no centralized migration runner, so this class must be
|
|
* safe to call from request handlers, cron, and tests.
|
|
*/
|
|
class coolify_schema_bootstrap
|
|
{
|
|
private static bool $initialized = false;
|
|
private static ?bool $tablesExist = null;
|
|
|
|
public static function ensureTables(): void
|
|
{
|
|
if (self::$initialized) {
|
|
return;
|
|
}
|
|
|
|
global $db;
|
|
|
|
$queries = [
|
|
"CREATE TABLE IF NOT EXISTS coolify_instances (
|
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
label VARCHAR(128) NOT NULL,
|
|
base_url VARCHAR(512) NOT NULL,
|
|
api_token_secret TEXT NOT NULL,
|
|
default_project_uuid VARCHAR(128) NULL,
|
|
default_environment_uuid VARCHAR(128) NULL,
|
|
default_environment_name VARCHAR(128) NULL,
|
|
default_server_uuid VARCHAR(128) NULL,
|
|
default_destination_uuid VARCHAR(128) NULL,
|
|
status VARCHAR(32) NOT NULL DEFAULT 'unknown',
|
|
last_checked_at DATETIME NULL,
|
|
last_error TEXT NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME NULL,
|
|
INDEX idx_coolify_instances_status (status),
|
|
INDEX idx_coolify_instances_deleted_at (deleted_at)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
|
|
"CREATE TABLE IF NOT EXISTS coolify_targets (
|
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
instance_id BIGINT UNSIGNED NOT NULL,
|
|
replication_host_id BIGINT UNSIGNED NULL,
|
|
kind VARCHAR(16) NOT NULL,
|
|
label VARCHAR(128) NOT NULL,
|
|
role VARCHAR(16) NOT NULL DEFAULT 'replica',
|
|
server_uuid VARCHAR(128) NULL,
|
|
project_uuid VARCHAR(128) NULL,
|
|
environment_uuid VARCHAR(128) NULL,
|
|
environment_name VARCHAR(128) NULL,
|
|
destination_uuid VARCHAR(128) NULL,
|
|
resource_uuid VARCHAR(128) NULL,
|
|
resource_type VARCHAR(32) NOT NULL DEFAULT 'service',
|
|
resource_name VARCHAR(128) NULL,
|
|
deployment_status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
|
availability_state VARCHAR(32) NOT NULL DEFAULT 'degraded',
|
|
desired_compose_hash CHAR(64) NULL,
|
|
last_reconcile_status VARCHAR(32) NULL,
|
|
last_reconcile_json LONGTEXT NULL,
|
|
last_reconciled_at DATETIME NULL,
|
|
options_json LONGTEXT NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME NULL,
|
|
INDEX idx_coolify_targets_instance (instance_id),
|
|
INDEX idx_coolify_targets_replication_host (replication_host_id),
|
|
INDEX idx_coolify_targets_kind_status (kind, deployment_status),
|
|
INDEX idx_coolify_targets_availability (availability_state),
|
|
INDEX idx_coolify_targets_resource_uuid (resource_uuid),
|
|
INDEX idx_coolify_targets_deleted_at (deleted_at)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
|
|
"CREATE TABLE IF NOT EXISTS coolify_operations (
|
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
target_id BIGINT UNSIGNED NULL,
|
|
instance_id BIGINT UNSIGNED NULL,
|
|
operation VARCHAR(32) NOT NULL,
|
|
status VARCHAR(32) NOT NULL DEFAULT 'running',
|
|
guarded TINYINT(1) NOT NULL DEFAULT 1,
|
|
message VARCHAR(512) NULL,
|
|
error_message TEXT NULL,
|
|
context_json LONGTEXT NULL,
|
|
actor_user_id INT NULL,
|
|
started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
completed_at DATETIME NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
INDEX idx_coolify_operations_target (target_id),
|
|
INDEX idx_coolify_operations_instance (instance_id),
|
|
INDEX idx_coolify_operations_status (status),
|
|
INDEX idx_coolify_operations_operation (operation)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
|
|
"CREATE TABLE IF NOT EXISTS coolify_audit_logs (
|
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
target_id BIGINT UNSIGNED NULL,
|
|
instance_id BIGINT UNSIGNED NULL,
|
|
replication_host_id BIGINT UNSIGNED NULL,
|
|
action VARCHAR(64) NOT NULL,
|
|
actor_user_id INT NULL,
|
|
severity VARCHAR(16) NOT NULL DEFAULT 'info',
|
|
context_json LONGTEXT NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
INDEX idx_coolify_audit_target (target_id),
|
|
INDEX idx_coolify_audit_instance (instance_id),
|
|
INDEX idx_coolify_audit_replication_host (replication_host_id),
|
|
INDEX idx_coolify_audit_action (action),
|
|
INDEX idx_coolify_audit_created_at (created_at)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
|
|
"CREATE TABLE IF NOT EXISTS coolify_instance_gateways (
|
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
instance_id BIGINT UNSIGNED NULL,
|
|
hostname VARCHAR(255) NOT NULL,
|
|
target_ip VARCHAR(64) NOT NULL,
|
|
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
|
priority INT NOT NULL DEFAULT 100,
|
|
health_state VARCHAR(32) NOT NULL DEFAULT 'unknown',
|
|
lb_state VARCHAR(32) NOT NULL DEFAULT 'unknown',
|
|
last_probe_json LONGTEXT NULL,
|
|
last_probed_at DATETIME NULL,
|
|
last_reconciled_at DATETIME NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME NULL,
|
|
UNIQUE KEY uniq_coolify_instance_gateways_target_ip (target_ip),
|
|
INDEX idx_coolify_instance_gateways_instance (instance_id),
|
|
INDEX idx_coolify_instance_gateways_enabled (enabled),
|
|
INDEX idx_coolify_instance_gateways_lb_state (lb_state),
|
|
INDEX idx_coolify_instance_gateways_deleted_at (deleted_at)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
];
|
|
|
|
foreach ($queries as $sql) {
|
|
$db->query($sql);
|
|
}
|
|
|
|
self::ensureColumn('coolify_instances', 'default_destination_uuid', 'VARCHAR(128) NULL');
|
|
self::ensureColumn('coolify_targets', 'availability_state', "VARCHAR(32) NOT NULL DEFAULT 'degraded'");
|
|
self::ensureColumn('coolify_targets', 'desired_compose_hash', 'CHAR(64) NULL');
|
|
self::ensureColumn('coolify_targets', 'last_reconcile_json', 'LONGTEXT NULL');
|
|
self::ensureColumn('coolify_operations', 'guarded', 'TINYINT(1) NOT NULL DEFAULT 1');
|
|
self::ensureColumn('coolify_instance_gateways', 'last_reconciled_at', 'DATETIME NULL');
|
|
|
|
self::ensureModuleConfigDefault('Coolify', 'enabled', 'false', 'bool');
|
|
self::ensureModuleConfigDefault('Coolify', 'lb_automation_enabled', 'false', 'bool');
|
|
self::ensureModuleConfigDefault('Coolify', 'lb_automation_mode', 'report_only', 'string');
|
|
self::ensureModuleConfigDefault('Coolify', 'hetzner_load_balancer_id', '', 'string');
|
|
self::ensureModuleConfigDefault('Coolify', 'hetzner_cloud_api_token', '', 'string');
|
|
self::ensureModuleConfigDefault('Coolify', 'public_gateway_host', 'api-v2.truckwash.io', 'string');
|
|
self::ensureModuleConfigDefault('Coolify', 'public_gateway_probe_path', '', 'string');
|
|
self::ensureModuleConfigDefault('Coolify', 'github_runner_token', '', 'string');
|
|
self::ensureModuleConfigDefault('Coolify', 'github_runner_service_uuid', '', 'string');
|
|
self::ensureModuleConfigDefault('Coolify', 'github_runner_frontend_repository', 'copenhagentruckwash/pleno-vue', 'string');
|
|
self::ensureModuleConfigDefault('Coolify', 'github_runner_backend_repository', 'copenhagentruckwash/api', 'string');
|
|
self::ensureModuleConfigDefault('Coolify', 'github_runner_labels', 'self-hosted,Linux,X64,default', 'string');
|
|
self::ensureModuleConfigDefault('Coolify', 'github_runner_count_per_repo', '1', 'int');
|
|
|
|
self::ensureDefaultGateway('node1.truckwash.io', '94.130.142.41', 10);
|
|
self::ensureDefaultGateway('node2.truckwash.io', '65.21.214.30', 20);
|
|
self::ensureDefaultGateway('node3.truckwash.io', '23.88.23.183', 30);
|
|
|
|
self::$initialized = true;
|
|
self::$tablesExist = true;
|
|
}
|
|
|
|
public static function tablesExist(): bool
|
|
{
|
|
if (self::$tablesExist !== null) {
|
|
return self::$tablesExist;
|
|
}
|
|
|
|
global $db;
|
|
|
|
foreach (['coolify_instances', 'coolify_targets', 'coolify_operations', 'coolify_audit_logs', 'coolify_instance_gateways'] as $table) {
|
|
$tableSql = $db->escape_string($table);
|
|
$result = $db->query("SHOW TABLES LIKE '$tableSql'");
|
|
if ($result === false || $result->num_rows === 0) {
|
|
self::$tablesExist = false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
self::$tablesExist = true;
|
|
return self::$tablesExist;
|
|
}
|
|
|
|
private static function ensureColumn(string $table, string $column, string $definition): void
|
|
{
|
|
global $db;
|
|
|
|
$table = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
|
|
$column = preg_replace('/[^a-zA-Z0-9_]/', '', $column);
|
|
if ($table === '' || $column === '') {
|
|
return;
|
|
}
|
|
|
|
$result = $db->query("SHOW COLUMNS FROM `$table` LIKE '$column'");
|
|
if ($result !== false && $result->num_rows > 0) {
|
|
return;
|
|
}
|
|
|
|
$db->query("ALTER TABLE `$table` ADD COLUMN `$column` $definition");
|
|
}
|
|
|
|
private static function ensureModuleConfigDefault(string $module, string $variable, string $value, string $type): void
|
|
{
|
|
global $db;
|
|
|
|
$moduleSql = $db->escape_string($module);
|
|
$variableSql = $db->escape_string($variable);
|
|
$result = $db->query("SELECT value FROM module_config WHERE module = '$moduleSql' AND variable = '$variableSql' LIMIT 1");
|
|
if ($result !== false && $result->num_rows > 0) {
|
|
return;
|
|
}
|
|
|
|
$valueSql = $db->escape_string($value);
|
|
$typeSql = $db->escape_string($type);
|
|
$db->query("INSERT INTO module_config (module, variable, value, type) VALUES ('$moduleSql', '$variableSql', '$valueSql', '$typeSql')");
|
|
}
|
|
|
|
private static function ensureDefaultGateway(string $hostname, string $targetIp, int $priority): void
|
|
{
|
|
global $db;
|
|
|
|
$targetIpSql = $db->escape_string($targetIp);
|
|
$result = $db->query("SELECT id FROM coolify_instance_gateways WHERE target_ip = '$targetIpSql' LIMIT 1");
|
|
if ($result !== false && $result->num_rows > 0) {
|
|
return;
|
|
}
|
|
|
|
$hostnameSql = $db->escape_string($hostname);
|
|
$db->query(
|
|
"INSERT INTO coolify_instance_gateways (hostname, target_ip, enabled, priority)
|
|
VALUES ('$hostnameSql', '$targetIpSql', 1, " . (int)$priority . ")"
|
|
);
|
|
}
|
|
}
|