mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-22 22:23:48 +00:00
fix(docker): replace deprecated stop flags with version-aware ones
Store each server's Docker version and build stop commands via dockerStopCommand() so newer Docker uses --timeout instead of deprecated --time/-t. Also show Docker version in server details.
This commit is contained in:
@@ -40,7 +40,7 @@ class StopApplication
|
||||
|
||||
foreach ($containersToStop as $containerName) {
|
||||
instant_remote_process(command: [
|
||||
"docker stop --time=$timeout $containerName",
|
||||
dockerStopCommand($timeout, $containerName, $server),
|
||||
"docker rm -f $containerName",
|
||||
], server: $server, throwError: false);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class StopApplicationOneServer
|
||||
if ($containerName) {
|
||||
instant_remote_process(
|
||||
[
|
||||
"docker stop --time=$timeout $containerName",
|
||||
dockerStopCommand($timeout, $containerName, $server),
|
||||
"docker rm -f $containerName",
|
||||
],
|
||||
$server
|
||||
|
||||
@@ -104,7 +104,7 @@ class StartClickhouse
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = dockerStopCommand(10, $container_name, $this->database->destination->server).' 2>/dev/null || true';
|
||||
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
||||
@@ -191,7 +191,7 @@ class StartDragonfly
|
||||
if ($this->database->enable_ssl) {
|
||||
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
|
||||
}
|
||||
$this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = dockerStopCommand(10, $container_name, $this->database->destination->server).' 2>/dev/null || true';
|
||||
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
||||
@@ -209,7 +209,7 @@ class StartKeydb
|
||||
if (! is_null($this->database->keydb_conf) && ! empty($this->database->keydb_conf)) {
|
||||
$this->commands[] = "chown 999:999 $this->configuration_dir/keydb.conf";
|
||||
}
|
||||
$this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = dockerStopCommand(10, $container_name, $this->database->destination->server).' 2>/dev/null || true';
|
||||
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
||||
@@ -208,7 +208,7 @@ class StartMariadb
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = dockerStopCommand(10, $container_name, $this->database->destination->server).' 2>/dev/null || true';
|
||||
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
||||
@@ -257,7 +257,7 @@ class StartMongodb
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = dockerStopCommand(10, $container_name, $this->database->destination->server).' 2>/dev/null || true';
|
||||
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
if ($this->database->enable_ssl) {
|
||||
|
||||
@@ -209,7 +209,7 @@ class StartMysql
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = dockerStopCommand(10, $container_name, $this->database->destination->server).' 2>/dev/null || true';
|
||||
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ class StartPostgresql
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = dockerStopCommand(10, $container_name, $this->database->destination->server).' 2>/dev/null || true';
|
||||
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
if ($this->database->enable_ssl) {
|
||||
|
||||
@@ -204,7 +204,7 @@ class StartRedis
|
||||
if (! is_null($this->database->redis_conf) && ! empty($this->database->redis_conf)) {
|
||||
$this->commands[] = "chown 999:999 $this->configuration_dir/redis.conf";
|
||||
}
|
||||
$this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = dockerStopCommand(10, $container_name, $this->database->destination->server).' 2>/dev/null || true';
|
||||
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
||||
@@ -56,7 +56,7 @@ class StopDatabase
|
||||
{
|
||||
$server = $database->destination->server;
|
||||
instant_remote_process(command: [
|
||||
"docker stop -t $timeout $containerName",
|
||||
dockerStopCommand($timeout, $containerName, $server),
|
||||
"docker rm -f $containerName",
|
||||
], server: $server, throwError: false);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class StopProxy
|
||||
}
|
||||
|
||||
instant_remote_process(command: [
|
||||
"docker stop -t=$timeout $containerName 2>/dev/null || true",
|
||||
dockerStopCommand($timeout, $containerName, $server).' 2>/dev/null || true',
|
||||
"docker rm -f $containerName 2>/dev/null || true",
|
||||
'# Wait for container to be fully removed',
|
||||
'for i in {1..10}; do',
|
||||
|
||||
@@ -67,7 +67,7 @@ class StopService
|
||||
$timeout = count($containersToStop) > 5 ? 10 : 30;
|
||||
$commands = [];
|
||||
$containerList = implode(' ', $containersToStop);
|
||||
$commands[] = "docker stop -t $timeout $containerList";
|
||||
$commands[] = dockerStopCommand($timeout, $containerList, $server);
|
||||
$commands[] = "docker rm -f $containerList";
|
||||
instant_remote_process(
|
||||
command: $commands,
|
||||
|
||||
@@ -431,6 +431,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
["docker version --format '{{.Server.Version}}'"],
|
||||
$serverToCheck
|
||||
);
|
||||
$serverToCheck->rememberDockerVersion($dockerVersion);
|
||||
|
||||
$versionParts = explode('.', $dockerVersion);
|
||||
$majorVersion = (int) $versionParts[0];
|
||||
@@ -3972,11 +3973,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
|
||||
if ($skipRemove) {
|
||||
$this->execute_remote_command(
|
||||
["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
[dockerStopCommand($timeout, $containerName, $this->server), 'hidden' => true, 'ignore_errors' => true]
|
||||
);
|
||||
} else {
|
||||
$this->execute_remote_command(
|
||||
["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
|
||||
[dockerStopCommand($timeout, $containerName, $this->server), 'hidden' => true, 'ignore_errors' => true],
|
||||
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
$containerList = implode(' ', array_map('escapeshellarg', $containerNames));
|
||||
$commands = [
|
||||
"docker stop -t $timeout $containerList",
|
||||
dockerStopCommand($timeout, $containerList, $server),
|
||||
"docker rm -f $containerList",
|
||||
];
|
||||
instant_remote_process(
|
||||
|
||||
@@ -98,7 +98,7 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
|
||||
// === STOP PHASE ===
|
||||
$commands = $commands->merge([
|
||||
"echo 'Stopping proxy...'",
|
||||
"docker stop -t=$stopTimeout $containerName 2>/dev/null || true",
|
||||
dockerStopCommand($stopTimeout, $containerName, $this->server).' 2>/dev/null || true',
|
||||
"docker rm -f $containerName 2>/dev/null || true",
|
||||
'# Wait for container to be fully removed',
|
||||
'for i in {1..15}; do',
|
||||
|
||||
@@ -194,6 +194,20 @@ class ServerConnectionCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$output = trim($output);
|
||||
if (! empty($output)) {
|
||||
$dockerInfo = json_decode($output, true);
|
||||
$dockerVersion = dockerEngineVersionFromJson($output);
|
||||
if ($dockerVersion !== null) {
|
||||
$this->server->rememberDockerVersion($dockerVersion);
|
||||
}
|
||||
|
||||
$composeOutput = instant_remote_process_with_timeout(
|
||||
['docker compose version --short'],
|
||||
$this->server,
|
||||
false
|
||||
);
|
||||
$composeVersion = parseDockerEngineVersion($composeOutput);
|
||||
if ($composeVersion !== null) {
|
||||
$this->server->rememberComposeVersion($composeVersion);
|
||||
}
|
||||
|
||||
return isset($dockerInfo['Server']['Version']);
|
||||
}
|
||||
|
||||
@@ -354,7 +354,7 @@ class Previews extends Component
|
||||
|
||||
foreach ($containersToStop as $containerName) {
|
||||
instant_remote_process(command: [
|
||||
"docker stop --time=$timeout $containerName",
|
||||
dockerStopCommand($timeout, $containerName, $server),
|
||||
"docker rm -f $containerName",
|
||||
], server: $server, throwError: false);
|
||||
}
|
||||
|
||||
@@ -371,6 +371,8 @@ class Show extends Component
|
||||
$this->server->settings->is_usable = $this->isUsable = true;
|
||||
$this->server->settings->save();
|
||||
ServerReachabilityChanged::dispatch($this->server);
|
||||
$this->server->gatherServerMetadata();
|
||||
$this->server->refresh();
|
||||
} else {
|
||||
$this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br>Error: '.$error);
|
||||
|
||||
@@ -671,12 +673,18 @@ class Show extends Component
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
if (! $this->server->isFunctional()) {
|
||||
$this->dispatch('error', 'Validate the server connection before fetching details.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->server->gatherServerMetadata();
|
||||
if ($result) {
|
||||
$this->server->refresh();
|
||||
$this->server->refresh()->load('settings');
|
||||
$this->dispatch('success', 'Server details refreshed.');
|
||||
} else {
|
||||
$this->dispatch('error', 'Could not fetch server details. Is the server reachable?');
|
||||
$this->dispatch('error', 'Could not collect server details. Check the application logs for the remote command output.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
handleError($e, $this);
|
||||
|
||||
+48
-2
@@ -961,7 +961,7 @@ $schema://$host {
|
||||
|
||||
public function stopUnmanaged($id)
|
||||
{
|
||||
return instant_remote_process(['docker stop -t 0 '.escapeshellarg($id)], $this);
|
||||
return instant_remote_process([dockerStopCommand(0, escapeshellarg($id), $this)], $this);
|
||||
}
|
||||
|
||||
public function restartUnmanaged($id)
|
||||
@@ -1351,7 +1351,7 @@ $schema://$host {
|
||||
|
||||
try {
|
||||
$output = instant_remote_process([
|
||||
'echo "---PRETTY_NAME---" && grep PRETTY_NAME /etc/os-release | cut -d= -f2 | tr -d \'"\' && echo "---ARCH---" && uname -m && echo "---KERNEL---" && uname -r && echo "---CPUS---" && nproc && echo "---MEMORY---" && free -b | awk \'/Mem:/{print $2}\' && echo "---UPTIME_SINCE---" && uptime -s',
|
||||
'echo "---PRETTY_NAME---" && grep PRETTY_NAME /etc/os-release | cut -d= -f2 | tr -d \'"\' && echo "---ARCH---" && uname -m && echo "---KERNEL---" && uname -r && echo "---CPUS---" && nproc && echo "---MEMORY---" && free -b | awk \'/Mem:/{print $2}\' && echo "---UPTIME_SINCE---" && uptime -s && echo "---DOCKER---" && (docker version --format \'{{.Server.Version}}\' 2>/dev/null || true) && echo "---COMPOSE---" && (docker compose version --short 2>/dev/null || true)',
|
||||
], $this, false);
|
||||
|
||||
if (! $output) {
|
||||
@@ -1381,6 +1381,23 @@ $schema://$host {
|
||||
|
||||
$this->update(['server_metadata' => $metadata]);
|
||||
|
||||
try {
|
||||
$detectedDockerVersion = parseDockerEngineVersion($sections['DOCKER'] ?? null);
|
||||
if ($detectedDockerVersion !== null) {
|
||||
$this->rememberDockerVersion($detectedDockerVersion);
|
||||
}
|
||||
|
||||
$detectedComposeVersion = parseDockerEngineVersion($sections['COMPOSE'] ?? null);
|
||||
if ($detectedComposeVersion !== null) {
|
||||
$this->rememberComposeVersion($detectedComposeVersion);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::debug('Failed to store server runtime versions', [
|
||||
'server_id' => $this->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
} catch (\Throwable $e) {
|
||||
Log::debug('Failed to gather server metadata', [
|
||||
@@ -1604,11 +1621,40 @@ $schema://$host {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function dockerVersion(): ?string
|
||||
{
|
||||
return $this->settings?->docker_version;
|
||||
}
|
||||
|
||||
public function rememberDockerVersion(?string $version): void
|
||||
{
|
||||
$this->settings->update([
|
||||
'docker_version' => parseDockerEngineVersion($version),
|
||||
'docker_version_checked_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function composeVersion(): ?string
|
||||
{
|
||||
return $this->settings?->compose_version;
|
||||
}
|
||||
|
||||
public function rememberComposeVersion(?string $version): void
|
||||
{
|
||||
$this->settings->update([
|
||||
'compose_version' => parseDockerEngineVersion($version),
|
||||
'compose_version_checked_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function validateDockerEngineVersion()
|
||||
{
|
||||
$dockerVersionRaw = instant_remote_process(['docker version --format json'], $this, false);
|
||||
$dockerVersionJson = json_decode($dockerVersionRaw, true);
|
||||
$dockerVersion = data_get($dockerVersionJson, 'Server.Version', '0.0.0');
|
||||
$this->rememberDockerVersion(is_string($dockerVersion) ? $dockerVersion : null);
|
||||
$composeVersionRaw = instant_remote_process(['docker compose version --short'], $this, false);
|
||||
$this->rememberComposeVersion(is_string($composeVersionRaw) ? $composeVersionRaw : null);
|
||||
$dockerVersion = checkMinimumDockerEngineVersion($dockerVersion);
|
||||
if (is_null($dockerVersion)) {
|
||||
$this->settings->is_usable = false;
|
||||
|
||||
@@ -51,6 +51,10 @@ use OpenApi\Attributes as OA;
|
||||
'delete_unused_volumes' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused volumes should be deleted.'],
|
||||
'delete_unused_networks' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused networks should be deleted.'],
|
||||
'connection_timeout' => ['type' => 'integer', 'description' => 'SSH connection timeout in seconds.'],
|
||||
'docker_version' => ['type' => 'string', 'nullable' => true, 'description' => 'Detected Docker Engine version on the server.'],
|
||||
'docker_version_checked_at' => ['type' => 'string', 'nullable' => true, 'description' => 'When Docker Engine version was last detected.'],
|
||||
'compose_version' => ['type' => 'string', 'nullable' => true, 'description' => 'Detected Docker Compose plugin version on the server.'],
|
||||
'compose_version_checked_at' => ['type' => 'string', 'nullable' => true, 'description' => 'When Docker Compose version was last detected.'],
|
||||
]
|
||||
)]
|
||||
class ServerSetting extends Model
|
||||
@@ -100,6 +104,10 @@ class ServerSetting extends Model
|
||||
'deployment_queue_limit',
|
||||
'disable_application_image_retention',
|
||||
'connection_timeout',
|
||||
'docker_version',
|
||||
'docker_version_checked_at',
|
||||
'compose_version',
|
||||
'compose_version_checked_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -113,6 +121,8 @@ class ServerSetting extends Model
|
||||
'is_terminal_enabled' => 'boolean',
|
||||
'disable_application_image_retention' => 'boolean',
|
||||
'connection_timeout' => 'integer',
|
||||
'docker_version_checked_at' => 'datetime',
|
||||
'compose_version_checked_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -198,6 +198,61 @@ function checkMinimumDockerEngineVersion($dockerVersion)
|
||||
|
||||
return $dockerVersion;
|
||||
}
|
||||
|
||||
function parseDockerEngineVersion(?string $rawVersion): ?string
|
||||
{
|
||||
if ($rawVersion === null || trim($rawVersion) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/\d+\.\d+(?:\.\d+)?/', $rawVersion, $matches) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parts = explode('.', $matches[0]);
|
||||
|
||||
return sprintf('%d.%d.%d', (int) $parts[0], (int) ($parts[1] ?? 0), (int) ($parts[2] ?? 0));
|
||||
}
|
||||
|
||||
function dockerEngineVersionFromJson(?string $raw): ?string
|
||||
{
|
||||
if ($raw === null || trim($raw) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$decoded = json_decode($raw, true);
|
||||
if (! is_array($decoded)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$version = $decoded['Server']['Version'] ?? null;
|
||||
|
||||
return is_string($version) ? parseDockerEngineVersion($version) : null;
|
||||
}
|
||||
|
||||
function dockerStopTimeoutOption(?string $dockerVersion): string
|
||||
{
|
||||
$normalized = parseDockerEngineVersion($dockerVersion);
|
||||
if ($normalized !== null && version_compare($normalized, '28.0.0', '>=')) {
|
||||
return '--timeout';
|
||||
}
|
||||
|
||||
return '--time';
|
||||
}
|
||||
|
||||
function dockerStopCommand(int $timeout, string $containers, Server|string|null $dockerVersion = null): string
|
||||
{
|
||||
$version = $dockerVersion instanceof Server
|
||||
? $dockerVersion->dockerVersion()
|
||||
: $dockerVersion;
|
||||
|
||||
$option = dockerStopTimeoutOption($version);
|
||||
$flag = $option === '--timeout'
|
||||
? "--timeout={$timeout}"
|
||||
: "--time={$timeout}";
|
||||
|
||||
return "docker stop {$flag} {$containers}";
|
||||
}
|
||||
function escapeShellValue(string $value): string
|
||||
{
|
||||
return "'".str_replace("'", "'\\''", $value)."'";
|
||||
|
||||
@@ -63,6 +63,7 @@ return [
|
||||
|
||||
'docker' => [
|
||||
'minimum_required_version' => '24.0',
|
||||
'stop_timeout_flag_since' => '28.0.0',
|
||||
],
|
||||
|
||||
'ssh' => [
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->string('docker_version')->nullable();
|
||||
$table->timestamp('docker_version_checked_at')->nullable();
|
||||
$table->string('compose_version')->nullable();
|
||||
$table->timestamp('compose_version_checked_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'docker_version',
|
||||
'docker_version_checked_at',
|
||||
'compose_version',
|
||||
'compose_version_checked_at',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -3,6 +3,12 @@
|
||||
<x-application.settings-section id="server-overview-section" title="Server overview"
|
||||
helper="Operating system and hardware details for the server running this Coolify instance.">
|
||||
<x-slot:actions>
|
||||
@if ($server->server_metadata)
|
||||
<x-forms.button type="button" class="size-8! px-0!"
|
||||
wire:click="refreshServerMetadata" title="Refresh server details">
|
||||
<x-reicon name="refresh" class="size-3.5" />
|
||||
</x-forms.button>
|
||||
@endif
|
||||
<x-status-badge :status="$server->isFunctional() ? 'Ready' : 'Validation required'"
|
||||
:type="$server->isFunctional() ? 'success' : 'warning'" />
|
||||
</x-slot:actions>
|
||||
@@ -25,27 +31,14 @@
|
||||
</div>
|
||||
|
||||
@if ($server->server_metadata)
|
||||
@php($meta = $server->server_metadata)
|
||||
<dl
|
||||
class="mt-4 grid gap-x-6 gap-y-5 border-t border-neutral-200 pt-4 sm:grid-cols-2 lg:grid-cols-3 dark:border-white/[0.08]">
|
||||
@foreach ([
|
||||
'Operating system' => $meta['os'] ?? 'N/A',
|
||||
'Architecture' => $meta['arch'] ?? 'N/A',
|
||||
'Kernel' => $meta['kernel'] ?? 'N/A',
|
||||
'CPU cores' => $meta['cpus'] ?? 'N/A',
|
||||
'Memory' => isset($meta['memory_bytes']) ? round($meta['memory_bytes'] / 1073741824, 1) . ' GB' : 'N/A',
|
||||
'Up since' => $meta['uptime_since'] ?? 'N/A',
|
||||
] as $detailLabel => $detailValue)
|
||||
<div>
|
||||
<dt class="text-xs font-medium text-neutral-500 dark:text-fg-dim">
|
||||
{{ $detailLabel }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm font-medium text-neutral-950 dark:text-fg">
|
||||
{{ $detailValue }}
|
||||
</dd>
|
||||
</div>
|
||||
@endforeach
|
||||
</dl>
|
||||
@include('livewire.server.partials.server-details', ['server' => $server])
|
||||
@else
|
||||
<div class="mt-4 border-t border-neutral-200 pt-4 dark:border-white/[0.08]">
|
||||
<x-forms.button type="button" wire:click="refreshServerMetadata">
|
||||
<x-reicon name="refresh" class="size-3.5" />
|
||||
Fetch server details
|
||||
</x-forms.button>
|
||||
</div>
|
||||
@endif
|
||||
</x-application.settings-section>
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
@php($meta = $server->server_metadata)
|
||||
<dl
|
||||
class="mt-4 grid gap-x-6 gap-y-5 border-t border-neutral-200 pt-4 sm:grid-cols-2 lg:grid-cols-3 dark:border-white/[0.08]">
|
||||
@foreach ([
|
||||
'Operating system' => $meta['os'] ?? 'N/A',
|
||||
'Architecture' => $meta['arch'] ?? 'N/A',
|
||||
'Kernel' => $meta['kernel'] ?? 'N/A',
|
||||
'CPU cores' => $meta['cpus'] ?? 'N/A',
|
||||
'Memory' => isset($meta['memory_bytes']) ? round($meta['memory_bytes'] / 1073741824, 1) . ' GB' : 'N/A',
|
||||
'Docker version' => $server->dockerVersion() ?? 'N/A',
|
||||
'Compose version' => $server->composeVersion() ?? 'N/A',
|
||||
'Up since' => $meta['uptime_since'] ?? 'N/A',
|
||||
] as $detailLabel => $detailValue)
|
||||
<div>
|
||||
<dt class="text-xs font-medium text-neutral-500 dark:text-fg-dim">
|
||||
{{ $detailLabel }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm font-medium text-neutral-950 dark:text-fg">
|
||||
{{ $detailValue }}
|
||||
</dd>
|
||||
</div>
|
||||
@endforeach
|
||||
</dl>
|
||||
@@ -89,6 +89,12 @@
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@endif
|
||||
@if ($server->server_metadata)
|
||||
<x-forms.button type="button" class="size-8! px-0!"
|
||||
wire:click="refreshServerMetadata" title="Refresh server details">
|
||||
<x-reicon name="refresh" class="size-3.5" />
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@if ($server->isTransferredAway())
|
||||
<x-status-badge label="Transferred away" type="warning" />
|
||||
@else
|
||||
@@ -119,27 +125,7 @@
|
||||
</div>
|
||||
|
||||
@if ($server->server_metadata)
|
||||
@php($meta = $server->server_metadata)
|
||||
<dl
|
||||
class="mt-4 grid gap-x-6 gap-y-5 border-t border-neutral-200 pt-4 sm:grid-cols-2 lg:grid-cols-3 dark:border-white/[0.08]">
|
||||
@foreach ([
|
||||
'Operating system' => $meta['os'] ?? 'N/A',
|
||||
'Architecture' => $meta['arch'] ?? 'N/A',
|
||||
'Kernel' => $meta['kernel'] ?? 'N/A',
|
||||
'CPU cores' => $meta['cpus'] ?? 'N/A',
|
||||
'Memory' => isset($meta['memory_bytes']) ? round($meta['memory_bytes'] / 1073741824, 1) . ' GB' : 'N/A',
|
||||
'Up since' => $meta['uptime_since'] ?? 'N/A',
|
||||
] as $detailLabel => $detailValue)
|
||||
<div>
|
||||
<dt class="text-xs font-medium text-neutral-500 dark:text-fg-dim">
|
||||
{{ $detailLabel }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm font-medium text-neutral-950 dark:text-fg">
|
||||
{{ $detailValue }}
|
||||
</dd>
|
||||
</div>
|
||||
@endforeach
|
||||
</dl>
|
||||
@include('livewire.server.partials.server-details', ['server' => $server])
|
||||
@else
|
||||
<div class="mt-4 border-t border-neutral-200 pt-4 dark:border-white/[0.08]">
|
||||
<x-forms.button type="button" wire:click="refreshServerMetadata">
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\ServerConnectionCheckJob;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function createServerForDockerVersionStorageTest(): Server
|
||||
{
|
||||
$team = Team::factory()->create();
|
||||
$privateKey = PrivateKey::factory()->create(['team_id' => $team->id]);
|
||||
|
||||
return Server::factory()->create([
|
||||
'team_id' => $team->id,
|
||||
'private_key_id' => $privateKey->id,
|
||||
'ip' => '203.0.113.10',
|
||||
]);
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
Storage::fake('ssh-keys');
|
||||
Carbon::setTestNow('2026-08-13 12:00:00');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Carbon::setTestNow();
|
||||
});
|
||||
|
||||
it('has docker and compose version columns on server settings', function () {
|
||||
expect(Schema::hasColumns('server_settings', [
|
||||
'docker_version',
|
||||
'docker_version_checked_at',
|
||||
'compose_version',
|
||||
'compose_version_checked_at',
|
||||
]))->toBeTrue();
|
||||
});
|
||||
|
||||
it('stores a parsed docker version on the server settings', function () {
|
||||
$server = createServerForDockerVersionStorageTest();
|
||||
|
||||
$server->rememberDockerVersion('29.4.3-ce');
|
||||
|
||||
$settings = $server->settings->fresh();
|
||||
|
||||
expect($settings->docker_version)->toBe('29.4.3')
|
||||
->and($settings->docker_version_checked_at?->equalTo(Carbon::parse('2026-08-13 12:00:00')))->toBeTrue()
|
||||
->and($server->dockerVersion())->toBe('29.4.3');
|
||||
});
|
||||
|
||||
it('updates the stored docker version when the engine changes', function () {
|
||||
$server = createServerForDockerVersionStorageTest();
|
||||
$server->rememberDockerVersion('27.5.1');
|
||||
|
||||
Carbon::setTestNow('2026-08-13 13:00:00');
|
||||
$server->rememberDockerVersion('29.4.3');
|
||||
|
||||
$settings = $server->settings->fresh();
|
||||
|
||||
expect($settings->docker_version)->toBe('29.4.3')
|
||||
->and($settings->docker_version_checked_at?->equalTo(Carbon::parse('2026-08-13 13:00:00')))->toBeTrue();
|
||||
});
|
||||
|
||||
it('builds stop commands from the stored server docker version', function (?string $storedVersion, string $expectedCommand) {
|
||||
$server = createServerForDockerVersionStorageTest();
|
||||
if ($storedVersion !== null) {
|
||||
$server->rememberDockerVersion($storedVersion);
|
||||
}
|
||||
|
||||
expect(dockerStopCommand(30, 'app-1', $server->fresh()))->toBe($expectedCommand);
|
||||
})->with([
|
||||
'unset' => [null, 'docker stop --time=30 app-1'],
|
||||
'docker 28+' => ['29.4.3', 'docker stop --timeout=30 app-1'],
|
||||
'docker 27' => ['27.5.1', 'docker stop --time=30 app-1'],
|
||||
]);
|
||||
|
||||
it('stores a parsed compose version on the server settings', function () {
|
||||
$server = createServerForDockerVersionStorageTest();
|
||||
|
||||
$server->rememberComposeVersion('v2.32.4');
|
||||
|
||||
$settings = $server->settings->fresh();
|
||||
|
||||
expect($settings->compose_version)->toBe('2.32.4')
|
||||
->and($settings->compose_version_checked_at?->equalTo(Carbon::parse('2026-08-13 12:00:00')))->toBeTrue()
|
||||
->and($server->composeVersion())->toBe('2.32.4');
|
||||
});
|
||||
|
||||
it('records docker and compose versions during connection checks', function () {
|
||||
$server = createServerForDockerVersionStorageTest();
|
||||
|
||||
Process::fake([
|
||||
'*compose*' => Process::result(output: 'v2.32.4', exitCode: 0),
|
||||
'*' => Process::result(
|
||||
output: '{"Server":{"Version":"29.4.3"}}',
|
||||
exitCode: 0,
|
||||
),
|
||||
]);
|
||||
|
||||
(new ServerConnectionCheckJob($server, disableMux: false))->handle();
|
||||
|
||||
$settings = $server->settings->fresh();
|
||||
|
||||
expect($settings->docker_version)->toBe('29.4.3')
|
||||
->and($settings->compose_version)->toBe('2.32.4');
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Server\Show;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->create(['id' => 0]));
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
$this->user->teams()->attach($this->team, ['role' => 'owner']);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->privateKey = PrivateKey::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$this->server = Server::factory()->create([
|
||||
'id' => 0,
|
||||
'uuid' => 'localhost',
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $this->privateKey->id,
|
||||
'name' => 'localhost',
|
||||
'ip' => 'host.docker.internal',
|
||||
'user' => 'root',
|
||||
'port' => 22,
|
||||
]);
|
||||
});
|
||||
|
||||
function localhostMetadataProcessOutput(): string
|
||||
{
|
||||
return implode("\n", [
|
||||
'---PRETTY_NAME---',
|
||||
'Ubuntu 22.04.3 LTS',
|
||||
'---ARCH---',
|
||||
'x86_64',
|
||||
'---KERNEL---',
|
||||
'5.15.0-91-generic',
|
||||
'---CPUS---',
|
||||
'4',
|
||||
'---MEMORY---',
|
||||
'8589934592',
|
||||
'---UPTIME_SINCE---',
|
||||
'2024-01-15 10:30:00',
|
||||
'---DOCKER---',
|
||||
'29.4.3-ce',
|
||||
'---COMPOSE---',
|
||||
'v2.32.4',
|
||||
]);
|
||||
}
|
||||
|
||||
it('shows a fetch server details action on localhost when metadata is missing', function () {
|
||||
$this->server->settings()->update([
|
||||
'is_reachable' => true,
|
||||
'is_usable' => true,
|
||||
]);
|
||||
|
||||
Livewire::test(Show::class, ['server_uuid' => $this->server->uuid])
|
||||
->assertSee('Fetch server details')
|
||||
->assertDontSee('Ubuntu 22.04.3 LTS');
|
||||
});
|
||||
|
||||
it('renders stored localhost server details instead of the fetch action', function () {
|
||||
$this->server->update([
|
||||
'server_metadata' => [
|
||||
'os' => 'Ubuntu 22.04.3 LTS',
|
||||
'arch' => 'x86_64',
|
||||
'kernel' => '5.15.0-91-generic',
|
||||
'cpus' => 4,
|
||||
'memory_bytes' => 8589934592,
|
||||
'uptime_since' => '2024-01-15 10:30:00',
|
||||
'collected_at' => now()->toIso8601String(),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->server->rememberDockerVersion('29.4.3-ce');
|
||||
$this->server->rememberComposeVersion('v2.32.4');
|
||||
|
||||
Livewire::test(Show::class, ['server_uuid' => $this->server->uuid])
|
||||
->assertSee('Operating system')
|
||||
->assertSee('Ubuntu 22.04.3 LTS')
|
||||
->assertSee('x86_64')
|
||||
->assertSee('Docker version')
|
||||
->assertSee('29.4.3')
|
||||
->assertSee('Compose version')
|
||||
->assertSee('2.32.4')
|
||||
->assertSee('Refresh server details')
|
||||
->assertDontSee('Fetch server details');
|
||||
});
|
||||
|
||||
it('fetches localhost server details from the overview action', function () {
|
||||
$this->server->settings()->update([
|
||||
'is_reachable' => true,
|
||||
'is_usable' => true,
|
||||
]);
|
||||
|
||||
Process::fake([
|
||||
'*' => Process::result(output: localhostMetadataProcessOutput(), exitCode: 0),
|
||||
]);
|
||||
|
||||
Livewire::test(Show::class, ['server_uuid' => $this->server->uuid])
|
||||
->call('refreshServerMetadata')
|
||||
->assertSee('Operating system')
|
||||
->assertSee('Ubuntu 22.04.3 LTS')
|
||||
->assertSee('Docker version')
|
||||
->assertSee('29.4.3')
|
||||
->assertSee('Compose version')
|
||||
->assertSee('2.32.4');
|
||||
|
||||
$server = $this->server->fresh();
|
||||
|
||||
expect($server->server_metadata['os'])->toBe('Ubuntu 22.04.3 LTS')
|
||||
->and($server->dockerVersion())->toBe('29.4.3')
|
||||
->and($server->composeVersion())->toBe('2.32.4');
|
||||
});
|
||||
|
||||
it('refetches localhost compose version from the overview refresh action', function () {
|
||||
$this->server->update([
|
||||
'server_metadata' => [
|
||||
'os' => 'Ubuntu 22.04.3 LTS',
|
||||
'arch' => 'x86_64',
|
||||
'kernel' => '5.15.0-91-generic',
|
||||
'cpus' => 4,
|
||||
'memory_bytes' => 8589934592,
|
||||
'uptime_since' => '2024-01-15 10:30:00',
|
||||
'collected_at' => now()->toIso8601String(),
|
||||
],
|
||||
]);
|
||||
$this->server->rememberDockerVersion('29.4.3');
|
||||
$this->server->settings()->update([
|
||||
'is_reachable' => true,
|
||||
'is_usable' => true,
|
||||
]);
|
||||
|
||||
Process::fake([
|
||||
'*' => Process::result(output: localhostMetadataProcessOutput(), exitCode: 0),
|
||||
]);
|
||||
|
||||
Livewire::test(Show::class, ['server_uuid' => $this->server->uuid])
|
||||
->assertSee('Compose version')
|
||||
->assertSee('N/A')
|
||||
->call('refreshServerMetadata')
|
||||
->assertSee('2.32.4');
|
||||
|
||||
expect($this->server->fresh()->composeVersion())->toBe('2.32.4');
|
||||
});
|
||||
|
||||
it('collects localhost server details after a successful connection check', function () {
|
||||
$this->server->settings()->update([
|
||||
'is_reachable' => false,
|
||||
'is_usable' => false,
|
||||
'server_timezone' => 'UTC',
|
||||
'connection_timeout' => 10,
|
||||
]);
|
||||
|
||||
Process::fake([
|
||||
'*' => Process::sequence([
|
||||
Process::result(output: 'bin', exitCode: 0),
|
||||
Process::result(output: localhostMetadataProcessOutput(), exitCode: 0),
|
||||
]),
|
||||
]);
|
||||
|
||||
Livewire::test(Show::class, ['server_uuid' => $this->server->uuid])
|
||||
->call('checkLocalhostConnection');
|
||||
|
||||
$server = $this->server->fresh();
|
||||
|
||||
expect($server->server_metadata['os'])->toBe('Ubuntu 22.04.3 LTS')
|
||||
->and($server->dockerVersion())->toBe('29.4.3')
|
||||
->and($server->composeVersion())->toBe('2.32.4')
|
||||
->and($server->settings->is_reachable)->toBeTrue()
|
||||
->and($server->settings->is_usable)->toBeTrue();
|
||||
});
|
||||
@@ -1,10 +1,13 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Server\Show;
|
||||
use App\Livewire\Server\ValidateAndInstall;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
@@ -16,8 +19,13 @@ beforeEach(function () {
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->privateKey = PrivateKey::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$this->server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $this->privateKey->id,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -84,6 +92,60 @@ it('returns null from gatherServerMetadata when server is not functional', funct
|
||||
expect($this->server->gatherServerMetadata())->toBeNull();
|
||||
});
|
||||
|
||||
it('stores a parsed docker version when gathering server metadata', function () {
|
||||
$this->server->settings->update([
|
||||
'is_reachable' => true,
|
||||
'is_usable' => true,
|
||||
]);
|
||||
|
||||
Process::fake([
|
||||
'*' => Process::result(output: implode("\n", [
|
||||
'---PRETTY_NAME---',
|
||||
'Debian GNU/Linux 12 (bookworm)',
|
||||
'---ARCH---',
|
||||
'aarch64',
|
||||
'---KERNEL---',
|
||||
'6.1.0-17-arm64',
|
||||
'---CPUS---',
|
||||
'8',
|
||||
'---MEMORY---',
|
||||
'17179869184',
|
||||
'---UPTIME_SINCE---',
|
||||
'2024-03-01 08:00:00',
|
||||
'---DOCKER---',
|
||||
'29.4.3-ce',
|
||||
'---COMPOSE---',
|
||||
'v2.32.4',
|
||||
]), exitCode: 0),
|
||||
]);
|
||||
|
||||
expect($this->server->gatherServerMetadata()['os'])->toBe('Debian GNU/Linux 12 (bookworm)')
|
||||
->and($this->server->fresh()->dockerVersion())->toBe('29.4.3')
|
||||
->and($this->server->fresh()->composeVersion())->toBe('2.32.4');
|
||||
});
|
||||
|
||||
it('shows the stored docker version in remote server details', function () {
|
||||
$this->server->update([
|
||||
'server_metadata' => [
|
||||
'os' => 'Debian GNU/Linux 12 (bookworm)',
|
||||
'arch' => 'aarch64',
|
||||
'kernel' => '6.1.0-17-arm64',
|
||||
'cpus' => 8,
|
||||
'memory_bytes' => 17179869184,
|
||||
'uptime_since' => '2024-03-01 08:00:00',
|
||||
'collected_at' => now()->toIso8601String(),
|
||||
],
|
||||
]);
|
||||
$this->server->rememberDockerVersion('27.5.1');
|
||||
$this->server->rememberComposeVersion('2.29.7');
|
||||
|
||||
Livewire::test(Show::class, ['server_uuid' => $this->server->uuid])
|
||||
->assertSee('Docker version')
|
||||
->assertSee('27.5.1')
|
||||
->assertSee('Compose version')
|
||||
->assertSee('2.29.7');
|
||||
});
|
||||
|
||||
it('can overwrite server_metadata with new values', function () {
|
||||
$this->server->update(['server_metadata' => ['os' => 'Ubuntu 20.04', 'cpus' => 2]]);
|
||||
$this->server->refresh();
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Docker 28.0 deprecated `--time` on `docker stop` / `docker restart` in favor of `--timeout`.
|
||||
* `--timeout` is rejected by Docker < 28, so Coolify picks the flag from the stored engine version.
|
||||
* Unknown versions keep the historical `--time` flag.
|
||||
*
|
||||
* @see https://github.com/coollabsio/coolify/issues/11244
|
||||
*/
|
||||
it('parses docker engine versions', function (?string $raw, ?string $expected) {
|
||||
expect(parseDockerEngineVersion($raw))->toBe($expected);
|
||||
})->with([
|
||||
'semver' => ['29.4.3', '29.4.3'],
|
||||
'two-part' => ['28.0', '28.0.0'],
|
||||
'suffix' => ['28.0.1-ce', '28.0.1'],
|
||||
'build metadata' => ['29.4.3+azure', '29.4.3'],
|
||||
'null' => [null, null],
|
||||
'empty' => ['', null],
|
||||
'garbage' => ['not-a-version', null],
|
||||
]);
|
||||
|
||||
it('extracts the server version from docker version json', function () {
|
||||
expect(dockerEngineVersionFromJson('{"Server":{"Version":"29.4.3"}}'))->toBe('29.4.3')
|
||||
->and(dockerEngineVersionFromJson('{"Client":{"Version":"29.4.3"}}'))->toBeNull()
|
||||
->and(dockerEngineVersionFromJson('not-json'))->toBeNull();
|
||||
});
|
||||
|
||||
it('uses --timeout on docker 28 and newer', function (string $version) {
|
||||
expect(dockerStopCommand(30, 'app-1', $version))->toBe('docker stop --timeout=30 app-1');
|
||||
})->with(['28.0.0', '28.0.1', '28.0', '29.4.3', '29.4.3-ce']);
|
||||
|
||||
it('uses --time on docker older than 28', function (string $version) {
|
||||
expect(dockerStopCommand(30, 'app-1', $version))->toBe('docker stop --time=30 app-1');
|
||||
})->with(['24.0.0', '26.1.4', '27.5.1']);
|
||||
|
||||
it('uses the historical --time flag when the docker version is unknown', function (?string $version) {
|
||||
expect(dockerStopCommand(30, 'app-1', $version))->toBe('docker stop --time=30 app-1');
|
||||
})->with([
|
||||
'null' => [null],
|
||||
'empty' => [''],
|
||||
'garbage' => ['unknown'],
|
||||
]);
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Docker deprecated `--time` on `docker stop` / `docker restart` in v28.0.
|
||||
* Call sites must go through dockerStopCommand() so the flag is chosen from
|
||||
* the stored engine version (`--timeout` on 28+, `--time` before that or if unknown).
|
||||
*
|
||||
* @see https://github.com/coollabsio/coolify/issues/11244
|
||||
* @see https://github.com/coollabsio/coolify/issues/10791
|
||||
*/
|
||||
it('does not hardcode docker --time outside the version-aware helper', function () {
|
||||
$projectRoot = dockerStopDeprecatedFlagProjectRoot();
|
||||
$helperFile = $projectRoot.DIRECTORY_SEPARATOR.'bootstrap'.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'docker.php';
|
||||
|
||||
$offenders = collect([
|
||||
...dockerStopDeprecatedFlagPhpFiles($projectRoot.DIRECTORY_SEPARATOR.'app'),
|
||||
...dockerStopDeprecatedFlagPhpFiles($projectRoot.DIRECTORY_SEPARATOR.'bootstrap'.DIRECTORY_SEPARATOR.'helpers'),
|
||||
])
|
||||
->reject(fn (string $file): bool => $file === $helperFile)
|
||||
->mapWithKeys(function (string $file) use ($projectRoot) {
|
||||
preg_match_all('/docker\s+(?:stop|restart)\s+--time\b/', file_get_contents($file), $matches);
|
||||
|
||||
return [str_replace($projectRoot.DIRECTORY_SEPARATOR, '', $file) => $matches[0]];
|
||||
})
|
||||
->filter(fn (array $matches): bool => $matches !== [])
|
||||
->map(fn (array $matches, string $file): string => $file.': '.implode(', ', array_unique($matches)));
|
||||
|
||||
expect($offenders->values()->implode("\n"))->toBe('');
|
||||
});
|
||||
|
||||
it('builds application container stop commands from the stored docker version', function (string $relativePath) {
|
||||
$contents = file_get_contents(dockerStopDeprecatedFlagProjectRoot().DIRECTORY_SEPARATOR.$relativePath);
|
||||
|
||||
expect($contents)
|
||||
->toContain('dockerStopCommand(')
|
||||
->not->toContain('docker stop --time=')
|
||||
->not->toContain('docker stop -t $timeout');
|
||||
})->with([
|
||||
'deployment graceful shutdown' => ['app/Jobs/ApplicationDeploymentJob.php'],
|
||||
'stop application' => ['app/Actions/Application/StopApplication.php'],
|
||||
'stop application on one server' => ['app/Actions/Application/StopApplicationOneServer.php'],
|
||||
'stop preview containers' => ['app/Livewire/Project/Application/Previews.php'],
|
||||
]);
|
||||
|
||||
function dockerStopDeprecatedFlagProjectRoot(): string
|
||||
{
|
||||
return dirname(__DIR__, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
function dockerStopDeprecatedFlagPhpFiles(string $directory): array
|
||||
{
|
||||
$files = [];
|
||||
|
||||
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory)) as $file) {
|
||||
if ($file->isFile() && $file->getExtension() === 'php') {
|
||||
$files[] = $file->getPathname();
|
||||
}
|
||||
}
|
||||
|
||||
sort($files);
|
||||
|
||||
return $files;
|
||||
}
|
||||
Reference in New Issue
Block a user