204 lines
6.6 KiB
PHP
204 lines
6.6 KiB
PHP
<?php
|
|
|
|
namespace App\Drivers\Postgres;
|
|
|
|
use App\Data\Operations\Plan;
|
|
use App\Data\Operations\PlannedStep;
|
|
use App\Drivers\Concerns\RendersCompose;
|
|
use App\Drivers\Concerns\SupportsSlices;
|
|
use App\Drivers\DatabaseDriver;
|
|
use App\Enums\EnvironmentAttachmentRole;
|
|
use App\Enums\ServiceType;
|
|
use App\Models\Service;
|
|
use App\Models\ServiceSlice;
|
|
use Illuminate\Support\Str;
|
|
|
|
class Postgres18Driver extends DatabaseDriver implements RendersCompose, SupportsSlices
|
|
{
|
|
public Plan $operationPlan;
|
|
|
|
public function __construct(
|
|
public ?string $containerName = null,
|
|
public ?string $containerId = null,
|
|
public ?Service $service = null,
|
|
public ?array $credentials = null,
|
|
) {
|
|
$this->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);
|
|
}
|
|
}
|