mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 14:17:18 +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:
1 parent
6481ffffcf
commit
e79230cf37
32 files changed
+697
-64
No files matched your search
@@ -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