Implement Keystone environment deployments
This commit is contained in:
@@ -2,15 +2,20 @@
|
||||
|
||||
namespace App\Drivers\Caddy;
|
||||
|
||||
use App\Data\Operations\Plan;
|
||||
use App\Data\Operations\PlannedStep;
|
||||
use App\Drivers\Concerns\RendersCompose;
|
||||
use App\Drivers\Concerns\SupportsSlices;
|
||||
use App\Drivers\GatewayDriver;
|
||||
use App\Data\Deployments\Plan;
|
||||
use App\Data\Deployments\PlannedStep as Step;
|
||||
use App\Enums\DeploymentStatus;
|
||||
use App\Enums\EnvironmentAttachmentRole;
|
||||
use App\Enums\ServiceType;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceSlice;
|
||||
|
||||
class Caddy2Driver extends GatewayDriver
|
||||
class Caddy2Driver extends GatewayDriver implements RendersCompose, SupportsSlices
|
||||
{
|
||||
public ?string $containerName;
|
||||
|
||||
public ?string $containerId;
|
||||
|
||||
public function __construct(
|
||||
@@ -23,55 +28,112 @@ class Caddy2Driver extends GatewayDriver
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
public function getDeploymentPlan(string $deploymentHash): Plan
|
||||
public function getOperationPlan(string $operationHash): Plan
|
||||
{
|
||||
$previousDeployment = $this->service?->deployments()
|
||||
->where('status', DeploymentStatus::COMPLETED)
|
||||
->first();
|
||||
|
||||
return new Plan(steps: [
|
||||
new Step(
|
||||
name: 'Generate Caddyfile',
|
||||
script: function () {
|
||||
$script = collect();
|
||||
$script->push('cd ~');
|
||||
$script->push('test -d services || mkdir services');
|
||||
$script->push('cd services');
|
||||
$script->push("test -d {$this->service->id} || mkdir {$this->service->id}");
|
||||
$script->push("cd {$this->service->id}");
|
||||
return $script->join("\n");
|
||||
}
|
||||
new PlannedStep(
|
||||
name: 'Render Caddy Compose files',
|
||||
script: "mkdir -p /home/keystone/gateway /home/keystone/services/{$this->service?->id}",
|
||||
),
|
||||
new Step(
|
||||
name: 'Run the docker image',
|
||||
script: function () use ($previousDeployment, $deploymentHash) {
|
||||
$script = collect();
|
||||
if ($this->containerName && $previousDeployment) {
|
||||
$script->push("docker stop \"{$this->containerName}-{$previousDeployment->hash}\" || true");
|
||||
} elseif ($this->containerId) {
|
||||
$script->push('docker stop ' . $this->containerId . ' || true');
|
||||
}
|
||||
|
||||
$runCommand = 'docker run -d';
|
||||
if ($this->containerName) {
|
||||
$runCommand .= " --name \"{$this->containerName}-{$deploymentHash}\"";
|
||||
}
|
||||
$runCommand .= ' -p 80:80 -p 443:443 caddy:2';
|
||||
|
||||
$script->push($runCommand);
|
||||
|
||||
return $script->join(" && ");
|
||||
}
|
||||
new PlannedStep(
|
||||
name: 'Start Caddy gateway',
|
||||
script: "docker compose -f /home/keystone/services/{$this->service?->id}/compose.yml up -d",
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
public function buildCaddyfile(): string
|
||||
public function serviceType(): ServiceType
|
||||
{
|
||||
$caddyfile = "http://{$this->service->name} {\n";
|
||||
$caddyfile .= " reverse_proxy {$this->service->credentials['backend']}\n";
|
||||
$caddyfile .= "}\n";
|
||||
return ServiceType::CADDY;
|
||||
}
|
||||
|
||||
return $caddyfile;
|
||||
public function versionTrack(): string
|
||||
{
|
||||
return '2';
|
||||
}
|
||||
|
||||
public function defaultImage(): string
|
||||
{
|
||||
return 'caddy:2';
|
||||
}
|
||||
|
||||
public function defaultPorts(): array
|
||||
{
|
||||
return [80, 443];
|
||||
}
|
||||
|
||||
public function firewallRules(): array
|
||||
{
|
||||
return ['80/tcp', '443/tcp'];
|
||||
}
|
||||
|
||||
public function environmentSchema(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function resourceDefaults(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function updateBehavior(): string
|
||||
{
|
||||
return 'stateless_redeploy';
|
||||
}
|
||||
|
||||
public function composeService(): array
|
||||
{
|
||||
return [
|
||||
'image' => $this->service?->available_image_digest
|
||||
?: $this->service?->current_image_digest
|
||||
?: $this->defaultImage(),
|
||||
'restart' => 'unless-stopped',
|
||||
'ports' => ['80:80', '443:443'],
|
||||
'volumes' => [
|
||||
'/home/keystone/gateway/Caddyfile:/etc/caddy/Caddyfile:ro',
|
||||
"keystone_service_{$this->service?->id}_caddy_data:/data",
|
||||
"keystone_service_{$this->service?->id}_caddy_config:/config",
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => ['CMD', 'caddy', 'version'],
|
||||
'interval' => '10s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 5,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function composeVolumes(): array
|
||||
{
|
||||
return [
|
||||
"keystone_service_{$this->service?->id}_caddy_data" => null,
|
||||
"keystone_service_{$this->service?->id}_caddy_config" => null,
|
||||
];
|
||||
}
|
||||
|
||||
public function environmentExports(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function supportedSliceTypes(): array
|
||||
{
|
||||
return ['route'];
|
||||
}
|
||||
|
||||
public function environmentExportsForSlice(ServiceSlice $slice, ?EnvironmentAttachmentRole $role = null): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function provisionSliceScript(ServiceSlice $slice): string
|
||||
{
|
||||
return implode("\n", [
|
||||
'set -euo pipefail',
|
||||
'mkdir -p /home/keystone/gateway/Caddyfile.d',
|
||||
'test -f /home/keystone/gateway/Caddyfile || touch /home/keystone/gateway/Caddyfile',
|
||||
"test ! -e /home/keystone/gateway/Caddyfile.d/{$slice->id}.caddy || true",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
21
app/Drivers/Concerns/RendersCompose.php
Normal file
21
app/Drivers/Concerns/RendersCompose.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Drivers\Concerns;
|
||||
|
||||
interface RendersCompose
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function composeService(): array;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function composeVolumes(): array;
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function environmentExports(): array;
|
||||
}
|
||||
21
app/Drivers/Concerns/SupportsSlices.php
Normal file
21
app/Drivers/Concerns/SupportsSlices.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Drivers\Concerns;
|
||||
|
||||
use App\Enums\EnvironmentAttachmentRole;
|
||||
use App\Models\ServiceSlice;
|
||||
|
||||
interface SupportsSlices
|
||||
{
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function supportedSliceTypes(): array;
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function environmentExportsForSlice(ServiceSlice $slice, ?EnvironmentAttachmentRole $role = null): array;
|
||||
|
||||
public function provisionSliceScript(ServiceSlice $slice): string;
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Drivers;
|
||||
|
||||
use App\Data\Deployments\Plan;
|
||||
use App\Data\Operations\Plan;
|
||||
use App\Data\Operations\PlannedStep;
|
||||
use App\Enums\ServiceType;
|
||||
use App\Models\Service;
|
||||
|
||||
abstract class Driver
|
||||
@@ -19,5 +21,41 @@ abstract class Driver
|
||||
?Service $service = null,
|
||||
);
|
||||
|
||||
abstract public function getDeploymentPlan(string $deploymentHash): Plan;
|
||||
abstract public function getOperationPlan(string $operationHash): Plan;
|
||||
|
||||
abstract public function serviceType(): ServiceType;
|
||||
|
||||
abstract public function versionTrack(): string;
|
||||
|
||||
abstract public function defaultImage(): string;
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
abstract public function defaultPorts(): array;
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
abstract public function firewallRules(): array;
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
abstract public function environmentSchema(): array;
|
||||
|
||||
/**
|
||||
* @return array{cpu?: string, memory_mb?: int}
|
||||
*/
|
||||
abstract public function resourceDefaults(): array;
|
||||
|
||||
abstract public function updateBehavior(): string;
|
||||
|
||||
/**
|
||||
* @return array<int, PlannedStep>
|
||||
*/
|
||||
public function preSwitchSteps(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
205
app/Drivers/Laravel/LaravelRuntimeDriver.php
Normal file
205
app/Drivers/Laravel/LaravelRuntimeDriver.php
Normal file
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
namespace App\Drivers\Laravel;
|
||||
|
||||
use App\Actions\Environments\BuildMigrationScript;
|
||||
use App\Data\Operations\Plan;
|
||||
use App\Data\Operations\PlannedStep;
|
||||
use App\Drivers\Concerns\RendersCompose;
|
||||
use App\Drivers\Driver;
|
||||
use App\Enums\SchedulerMode;
|
||||
use App\Enums\ServiceType;
|
||||
use App\Models\Service;
|
||||
|
||||
class LaravelRuntimeDriver extends Driver implements RendersCompose
|
||||
{
|
||||
public function __construct(
|
||||
public ?string $containerName = null,
|
||||
public ?string $containerId = null,
|
||||
public ?Service $service = null,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
public function getOperationPlan(string $operationHash): Plan
|
||||
{
|
||||
return new Plan(steps: [
|
||||
new PlannedStep(
|
||||
name: 'Render Laravel Compose file',
|
||||
script: "mkdir -p /home/keystone/services/{$this->service?->id}",
|
||||
),
|
||||
new PlannedStep(
|
||||
name: 'Run migrations',
|
||||
script: $this->service
|
||||
? app(BuildMigrationScript::class)->execute($this->service)
|
||||
: 'true',
|
||||
),
|
||||
new PlannedStep(
|
||||
name: 'Start Laravel replica',
|
||||
script: "docker compose -f /home/keystone/services/{$this->service?->id}/compose.yml up -d",
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
public function serviceType(): ServiceType
|
||||
{
|
||||
return ServiceType::LARAVEL;
|
||||
}
|
||||
|
||||
public function versionTrack(): string
|
||||
{
|
||||
return 'php-8.4';
|
||||
}
|
||||
|
||||
public function defaultImage(): string
|
||||
{
|
||||
return 'serversideup/php:8.4-frankenphp';
|
||||
}
|
||||
|
||||
public function defaultPorts(): array
|
||||
{
|
||||
return [80];
|
||||
}
|
||||
|
||||
public function firewallRules(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function environmentSchema(): array
|
||||
{
|
||||
return [
|
||||
'APP_ENV' => 'string',
|
||||
'SERVER_NAME' => 'string',
|
||||
];
|
||||
}
|
||||
|
||||
public function resourceDefaults(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function updateBehavior(): string
|
||||
{
|
||||
return 'stateless_gateway_cutover';
|
||||
}
|
||||
|
||||
public function composeService(): array
|
||||
{
|
||||
$image = $this->service?->available_image_digest
|
||||
?: $this->service?->current_image_digest
|
||||
?: ($this->service?->config['image'] ?? $this->defaultImage());
|
||||
|
||||
$service = [
|
||||
'image' => $image,
|
||||
'restart' => 'unless-stopped',
|
||||
'environment' => $this->environmentExports(),
|
||||
];
|
||||
|
||||
if ($command = $this->service?->config['command'] ?? null) {
|
||||
$service['command'] = $command;
|
||||
}
|
||||
|
||||
if (! in_array('worker', $this->service?->process_roles ?? [], true)) {
|
||||
$service['healthcheck'] = [
|
||||
'test' => ['CMD-SHELL', 'curl -fsS http://localhost'.($this->service?->config['health_path'] ?? '/up').' || exit 1'],
|
||||
'interval' => '10s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 5,
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->service?->default_cpu_limit) {
|
||||
$service['cpus'] = (string) $this->service->default_cpu_limit;
|
||||
}
|
||||
|
||||
if ($this->service?->default_memory_limit_mb) {
|
||||
$service['mem_limit'] = "{$this->service->default_memory_limit_mb}m";
|
||||
$service['memswap_limit'] = "{$this->service->default_memory_limit_mb}m";
|
||||
}
|
||||
|
||||
return $service;
|
||||
}
|
||||
|
||||
public function composeVolumes(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function environmentExports(): array
|
||||
{
|
||||
$environment = $this->service?->environment?->variables()
|
||||
->pluck('value', 'key')
|
||||
->all() ?? [];
|
||||
|
||||
$environment = [
|
||||
...$environment,
|
||||
'APP_ENV' => $this->service?->environment?->name ?? 'production',
|
||||
'SERVER_NAME' => ':80',
|
||||
];
|
||||
|
||||
if ($this->shouldAutorunScheduler()) {
|
||||
$environment['AUTORUN_LARAVEL_SCHEDULER'] = 'true';
|
||||
}
|
||||
|
||||
return $environment;
|
||||
}
|
||||
|
||||
private function shouldAutorunScheduler(): bool
|
||||
{
|
||||
if (! in_array('scheduler', $this->service?->process_roles ?? [], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$environment = $this->service?->environment;
|
||||
|
||||
if (! $environment?->scheduler_enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($environment->scheduler_target_service_id && $environment->scheduler_target_service_id !== $this->service?->id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $environment->scheduler_mode !== SchedulerMode::SINGLE
|
||||
|| (int) $this->service?->desired_replicas === 1;
|
||||
}
|
||||
|
||||
public function dockerfileTemplate(): string
|
||||
{
|
||||
$phpVersion = $this->service?->config['php_version'] ?? '8.4';
|
||||
$documentRoot = $this->service?->config['document_root'] ?? 'public';
|
||||
$jsBuildCommand = $this->service?->config['js_build_command'] ?? $this->service?->environment?->build_config['js_build_command'] ?? null;
|
||||
$jsPackageManager = $this->service?->config['js_package_manager'] ?? $this->service?->environment?->build_config['js_package_manager'] ?? 'bun';
|
||||
$jsBuildSteps = $this->jsBuildSteps($jsPackageManager, $jsBuildCommand);
|
||||
|
||||
return <<<DOCKERFILE
|
||||
FROM serversideup/php:{$phpVersion}-frankenphp
|
||||
|
||||
ENV SSL_MODE=off
|
||||
ENV AUTORUN_ENABLED=true
|
||||
ENV PHP_OPCACHE_ENABLE=1
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
COPY --chown=www-data:www-data . .
|
||||
|
||||
RUN composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
|
||||
{$jsBuildSteps}
|
||||
|
||||
ENV SERVER_DOCUMENT_ROOT=/var/www/html/{$documentRoot}
|
||||
DOCKERFILE;
|
||||
}
|
||||
|
||||
private function jsBuildSteps(string $packageManager, ?string $buildCommand): string
|
||||
{
|
||||
if (! $buildCommand) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return match ($packageManager) {
|
||||
'npm' => "\nRUN npm ci && {$buildCommand}",
|
||||
default => "\nRUN curl -fsSL https://bun.sh/install | bash && export PATH=\"/root/.bun/bin:\$PATH\" && bun install --frozen-lockfile && {$buildCommand}",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Drivers\Postgres;
|
||||
|
||||
use App\Data\Deployments\Plan;
|
||||
use App\Data\Deployments\PlannedStep as Step;
|
||||
use App\Drivers\DatabaseDriver;
|
||||
use App\Enums\DeploymentStatus;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Postgres17Driver extends DatabaseDriver
|
||||
{
|
||||
public Plan $deploymentPlan;
|
||||
|
||||
public function __construct(
|
||||
public ?string $containerName = null,
|
||||
public ?string $containerId = null,
|
||||
public ?Service $service = null,
|
||||
public ?array $credentials = null,
|
||||
) {
|
||||
$credentials = $credentials ?? $this->defaultCredentials();
|
||||
}
|
||||
|
||||
public function getDeploymentPlan(string $deploymentHash): Plan
|
||||
{
|
||||
$user = $credentials['user'] ?? null;
|
||||
$password = $credentials['password'] ?? null;
|
||||
$db = $credentials['db'] ?? null;
|
||||
|
||||
if (!$user || !$password || !$db) {
|
||||
throw new \InvalidArgumentException('Missing required credentials');
|
||||
}
|
||||
|
||||
$previousDeployment = $this->service?->deployments()
|
||||
->where('status', DeploymentStatus::COMPLETED)
|
||||
->first();
|
||||
|
||||
return new Plan(steps: [
|
||||
new Step(
|
||||
name: 'Run the docker image',
|
||||
secrets: [
|
||||
'password' => $password
|
||||
],
|
||||
script: function () use ($user, $password, $db, $previousDeployment, $deploymentHash) {
|
||||
$script = collect();
|
||||
|
||||
if ($this->containerName && $previousDeployment) {
|
||||
$script->push("docker stop \"{$this->containerName}-{$previousDeployment->hash}\" || true");
|
||||
} elseif ($this->containerId) {
|
||||
$script->push('docker stop ' . $this->containerId . ' || true');
|
||||
}
|
||||
|
||||
$runCommand = 'docker run -d';
|
||||
if ($this->containerName) {
|
||||
$runCommand .= " --name \"{$this->containerName}-{$deploymentHash}\"";
|
||||
}
|
||||
if ($password) {
|
||||
$runCommand .= ' -e POSTGRES_PASSWORD=[!password!]';
|
||||
}
|
||||
if ($user) {
|
||||
$runCommand .= " -e POSTGRES_USER={$user}";
|
||||
}
|
||||
if ($db) {
|
||||
$runCommand .= " -e POSTGRES_DB={$db}";
|
||||
}
|
||||
$runCommand .= ' -p 5432:5432 postgres:17';
|
||||
|
||||
$script->push($runCommand);
|
||||
|
||||
return $script->join(" && ");
|
||||
}
|
||||
),
|
||||
new Step(
|
||||
name: 'Configure firewall', // @todo this should create a Firewallrule
|
||||
script: 'ufw allow 5432/tcp || true',
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
public function defaultCredentials(): array
|
||||
{
|
||||
return [
|
||||
'password' => Str::random(16),
|
||||
'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}';\"";
|
||||
}
|
||||
}
|
||||
203
app/Drivers/Postgres/Postgres18Driver.php
Normal file
203
app/Drivers/Postgres/Postgres18Driver.php
Normal file
@@ -0,0 +1,203 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
154
app/Drivers/Valkey/Valkey8Driver.php
Normal file
154
app/Drivers/Valkey/Valkey8Driver.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace App\Drivers\Valkey;
|
||||
|
||||
use App\Data\Operations\Plan;
|
||||
use App\Data\Operations\PlannedStep;
|
||||
use App\Drivers\Concerns\RendersCompose;
|
||||
use App\Drivers\Concerns\SupportsSlices;
|
||||
use App\Drivers\Driver;
|
||||
use App\Enums\EnvironmentAttachmentRole;
|
||||
use App\Enums\ServiceType;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceSlice;
|
||||
|
||||
class Valkey8Driver extends Driver implements RendersCompose, SupportsSlices
|
||||
{
|
||||
public function __construct(
|
||||
public ?string $containerName = null,
|
||||
public ?string $containerId = null,
|
||||
public ?Service $service = null,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
public function getOperationPlan(string $operationHash): Plan
|
||||
{
|
||||
return new Plan(steps: [
|
||||
new PlannedStep(
|
||||
name: 'Render Compose file',
|
||||
script: "mkdir -p /home/keystone/services/{$this->service?->id}",
|
||||
),
|
||||
new PlannedStep(
|
||||
name: 'Start Valkey service',
|
||||
script: "docker compose -f /home/keystone/services/{$this->service?->id}/compose.yml up -d",
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
public function serviceType(): ServiceType
|
||||
{
|
||||
return ServiceType::VALKEY;
|
||||
}
|
||||
|
||||
public function versionTrack(): string
|
||||
{
|
||||
return '8';
|
||||
}
|
||||
|
||||
public function defaultImage(): string
|
||||
{
|
||||
return 'valkey/valkey:8';
|
||||
}
|
||||
|
||||
public function defaultPorts(): array
|
||||
{
|
||||
return [6379];
|
||||
}
|
||||
|
||||
public function firewallRules(): array
|
||||
{
|
||||
return ['6379/tcp'];
|
||||
}
|
||||
|
||||
public function environmentSchema(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function resourceDefaults(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function updateBehavior(): string
|
||||
{
|
||||
return 'stateful_downtime';
|
||||
}
|
||||
|
||||
public function supportedSliceTypes(): array
|
||||
{
|
||||
return ['logical_database'];
|
||||
}
|
||||
|
||||
public function environmentExportsForSlice(ServiceSlice $slice, ?EnvironmentAttachmentRole $role = null): array
|
||||
{
|
||||
$exports = [
|
||||
'REDIS_HOST' => $slice->config['host'] ?? "keystone-service-{$slice->service_id}",
|
||||
'REDIS_PORT' => (string) ($slice->config['port'] ?? 6379),
|
||||
'REDIS_DB' => (string) ($slice->config['database'] ?? 0),
|
||||
];
|
||||
|
||||
return match ($role) {
|
||||
EnvironmentAttachmentRole::CACHE => [
|
||||
...$exports,
|
||||
'CACHE_STORE' => 'redis',
|
||||
],
|
||||
EnvironmentAttachmentRole::QUEUE => [
|
||||
...$exports,
|
||||
'QUEUE_CONNECTION' => 'redis',
|
||||
],
|
||||
EnvironmentAttachmentRole::CUSTOM,
|
||||
EnvironmentAttachmentRole::DATABASE,
|
||||
EnvironmentAttachmentRole::GATEWAY,
|
||||
EnvironmentAttachmentRole::STORAGE,
|
||||
null => $exports,
|
||||
};
|
||||
}
|
||||
|
||||
public function provisionSliceScript(ServiceSlice $slice): string
|
||||
{
|
||||
$serviceKey = str($slice->service->name)->slug('_')->value() ?: 'valkey';
|
||||
|
||||
return 'docker compose -f /home/keystone/services/'.$slice->service_id.'/compose.yml exec -T '.$serviceKey.' valkey-cli -n '.escapeshellarg((string) ($slice->config['database'] ?? 0)).' PING';
|
||||
}
|
||||
|
||||
public function composeService(): array
|
||||
{
|
||||
$service = [
|
||||
'image' => $this->service?->available_image_digest
|
||||
?: $this->service?->current_image_digest
|
||||
?: $this->defaultImage(),
|
||||
'restart' => 'unless-stopped',
|
||||
'healthcheck' => [
|
||||
'test' => ['CMD', 'valkey-cli', 'ping'],
|
||||
'interval' => '10s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 5,
|
||||
],
|
||||
];
|
||||
|
||||
if ($this->service?->config['persistence'] ?? false) {
|
||||
$service['volumes'] = ["keystone_service_{$this->service->id}_valkey_data:/data"];
|
||||
$service['command'] = ['valkey-server', '--appendonly', 'yes'];
|
||||
}
|
||||
|
||||
return $service;
|
||||
}
|
||||
|
||||
public function composeVolumes(): array
|
||||
{
|
||||
if (! ($this->service?->config['persistence'] ?? false)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
"keystone_service_{$this->service->id}_valkey_data" => null,
|
||||
];
|
||||
}
|
||||
|
||||
public function environmentExports(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user