credentials = $credentials ?? $this->defaultCredentials(); } public function getOperationPlan(string $operationHash): Plan { $credentials = $this->credentials ?? $this->defaultCredentials(); $user = $credentials['user'] ?? null; $password = $credentials['password'] ?? null; $db = $credentials['db'] ?? null; if (! $user || ! $password || ! $db) { throw new \InvalidArgumentException('Missing required credentials'); } return new Plan(steps: [ new PlannedStep( name: 'Render Compose file', script: "mkdir -p /home/keystone/services/{$this->service?->id}", ), new PlannedStep( name: 'Start Postgres service', script: "docker compose -f /home/keystone/services/{$this->service?->id}/compose.yml up -d", ), new PlannedStep( name: 'Check Postgres health', script: "docker compose -f /home/keystone/services/{$this->service?->id}/compose.yml ps --status running", ), new PlannedStep( name: 'Configure firewall', script: 'ufw allow 5432/tcp || true', ), ]); } public function serviceType(): ServiceType { return ServiceType::POSTGRES; } public function versionTrack(): string { return '18'; } public function defaultImage(): string { return 'postgres:18'; } public function defaultPorts(): array { return [5432]; } public function firewallRules(): array { return ['5432/tcp']; } public function environmentSchema(): array { return [ 'POSTGRES_USER' => 'string', 'POSTGRES_PASSWORD' => 'secret', 'POSTGRES_DB' => 'string', ]; } public function resourceDefaults(): array { return []; } public function updateBehavior(): string { return 'stateful_downtime'; } public function defaultCredentials(): array { return [ 'password' => Str::random(32), 'user' => 'keystone', 'db' => 'keystone', ]; } public function createUser(string $user, string $password): string { return "psql -U {$this->credentials['user']} -d {$this->credentials['db']} -c \"CREATE USER {$user} WITH PASSWORD '{$password}';\""; } public function supportedSliceTypes(): array { return ['database_user']; } public function environmentExportsForSlice(ServiceSlice $slice, ?EnvironmentAttachmentRole $role = null): array { $credentials = $slice->credentials ?? []; return [ 'DB_CONNECTION' => 'pgsql', 'DB_HOST' => $slice->config['host'] ?? "keystone-service-{$slice->service_id}", 'DB_PORT' => (string) ($slice->config['port'] ?? 5432), 'DB_DATABASE' => $credentials['database'] ?? $slice->name, 'DB_USERNAME' => $credentials['username'] ?? $slice->name, 'DB_PASSWORD' => $credentials['password'] ?? '', ]; } public function provisionSliceScript(ServiceSlice $slice): string { $credentials = $slice->credentials ?? []; $database = $credentials['database'] ?? $slice->name; $username = $credentials['username'] ?? $slice->name; $password = $credentials['password'] ?? Str::password(32); $admin = ($this->credentials ?? $this->defaultCredentials())['user'] ?? 'keystone'; $serviceKey = str($slice->service->name)->slug('_')->value() ?: 'postgres'; return implode("\n", [ 'set -euo pipefail', "docker compose -f /home/keystone/services/{$slice->service_id}/compose.yml exec -T {$serviceKey} psql -U ".escapeshellarg($admin).' -d postgres <<\'KEYSTONE_SQL\'', "SELECT 'CREATE DATABASE \"{$this->sqlIdentifier($database)}\"' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '{$this->sqlLiteral($database)}')\\gexec", 'DO $$ BEGIN CREATE USER "'.$this->sqlIdentifier($username).'" WITH PASSWORD \''.$this->sqlLiteral($password).'\'; EXCEPTION WHEN duplicate_object THEN ALTER USER "'.$this->sqlIdentifier($username).'" WITH PASSWORD \''.$this->sqlLiteral($password).'\'; END $$;', "GRANT ALL PRIVILEGES ON DATABASE \"{$this->sqlIdentifier($database)}\" TO \"{$this->sqlIdentifier($username)}\";", 'KEYSTONE_SQL', ]); } public function composeService(): array { $credentials = $this->credentials ?? $this->defaultCredentials(); return [ 'image' => $this->service?->available_image_digest ?: $this->service?->current_image_digest ?: $this->defaultImage(), 'restart' => 'unless-stopped', 'environment' => [ 'POSTGRES_USER' => $credentials['user'], 'POSTGRES_PASSWORD' => $credentials['password'], 'POSTGRES_DB' => $credentials['db'], ], 'volumes' => [ "keystone_service_{$this->service?->id}_postgres_data:/var/lib/postgresql/data", ], 'healthcheck' => [ 'test' => ['CMD-SHELL', 'pg_isready -U '.$credentials['user']], 'interval' => '10s', 'timeout' => '5s', 'retries' => 5, ], ]; } public function composeVolumes(): array { return [ "keystone_service_{$this->service?->id}_postgres_data" => null, ]; } public function environmentExports(): array { return []; } private function sqlIdentifier(string $value): string { return str_replace('"', '""', $value); } private function sqlLiteral(string $value): string { return str_replace("'", "''", $value); } }