mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
Docker Compose gives a recreated container the old mount at each path that the image declares with VOLUME, also a volume deleted in Coolify. Before `up`, remove only containers that still mount a Coolify volume whose storage entry was deleted. The stale volume delete dialog now warns when a container still uses the volume. Closes #6875 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
93 lines
4.3 KiB
PHP
93 lines
4.3 KiB
PHP
<?php
|
|
|
|
use Tests\TestCase;
|
|
|
|
uses(TestCase::class);
|
|
|
|
/**
|
|
* `docker compose up` gives a recreated container the old mount at each path that the image
|
|
* declares with VOLUME, also a volume that the user deleted in Coolify (#6875).
|
|
*/
|
|
function inspectedComposeContainer(string $service, array $imageVolumes, array $mounts): string
|
|
{
|
|
return json_encode([
|
|
'name' => "/{$service}-abc123",
|
|
'service' => $service,
|
|
'imageVolumes' => $imageVolumes === [] ? null : array_fill_keys($imageVolumes, new stdClass),
|
|
'mounts' => $mounts,
|
|
]);
|
|
}
|
|
|
|
function namedVolumeMount(string $name, string $destination): array
|
|
{
|
|
return ['Type' => 'volume', 'Name' => $name, 'Source' => "/var/lib/docker/volumes/{$name}/_data", 'Destination' => $destination];
|
|
}
|
|
|
|
it('finds a container whose deleted volume Compose would keep', function () {
|
|
$projectMounts = inspectedComposeContainer('db', ['/var/lib/postgresql/data'], [
|
|
namedVolumeMount('abc123_db-data', '/var/lib/postgresql/data'),
|
|
]);
|
|
|
|
expect(composeContainersWithRemovedVolumes($projectMounts, ['db' => ['image' => 'postgres:17']], 'abc123', ['abc123_other']))
|
|
->toBe(['db-abc123' => 'abc123_db-data']);
|
|
});
|
|
|
|
it('keeps a volume that is only removed from the Compose file, because its storage row still exists', function () {
|
|
$projectMounts = inspectedComposeContainer('db', ['/var/lib/postgresql/data'], [
|
|
namedVolumeMount('abc123_db-data', '/var/lib/postgresql/data'),
|
|
]);
|
|
|
|
expect(composeContainersWithRemovedVolumes($projectMounts, ['db' => ['image' => 'postgres:17']], 'abc123', ['abc123_db-data']))->toBe([]);
|
|
});
|
|
|
|
it('keeps external volumes and bind mounts, because Coolify did not make them', function (array $mount) {
|
|
$projectMounts = inspectedComposeContainer('app', ['/data/'], [$mount]);
|
|
|
|
expect(composeContainersWithRemovedVolumes($projectMounts, ['app' => ['image' => 'app', 'volumes' => ['/other:/other']]], 'abc123', []))->toBe([]);
|
|
})->with([
|
|
'external volume' => [namedVolumeMount('shared-data', '/data')],
|
|
'bind mount' => [['Type' => 'bind', 'Source' => '/data/coolify/services/abc123/data', 'Destination' => '/data']],
|
|
]);
|
|
|
|
it('keeps a container when the Compose file still mounts something at the path', function (mixed $volume) {
|
|
$projectMounts = inspectedComposeContainer('db', ['/var/lib/postgresql/data'], [
|
|
namedVolumeMount('abc123_db-data', '/var/lib/postgresql/data'),
|
|
]);
|
|
|
|
expect(composeContainersWithRemovedVolumes($projectMounts, ['db' => ['volumes' => [$volume]]], 'abc123', []))->toBe([]);
|
|
})->with([
|
|
'short syntax' => 'abc123_db-data:/var/lib/postgresql/data',
|
|
'short syntax with mode' => 'other-data:/var/lib/postgresql/data/:rw',
|
|
'source with a default value' => '${DATA_PATH:-/srv/db}:/var/lib/postgresql/data',
|
|
'anonymous volume' => '/var/lib/postgresql/data',
|
|
'long syntax' => [['type' => 'volume', 'source' => 'other', 'target' => '/var/lib/postgresql/data']],
|
|
]);
|
|
|
|
it('keeps a container whose anonymous volume Compose keeps on purpose', function () {
|
|
$projectMounts = inspectedComposeContainer('db', ['/var/lib/postgresql/data'], [
|
|
namedVolumeMount(str_repeat('a1', 32), '/var/lib/postgresql/data'),
|
|
]);
|
|
|
|
expect(composeContainersWithRemovedVolumes($projectMounts, ['db' => ['image' => 'postgres:17']], 'abc123', []))->toBe([]);
|
|
});
|
|
|
|
it('keeps a container when its image declares no volume at the removed path', function () {
|
|
$projectMounts = inspectedComposeContainer('app', [], [
|
|
namedVolumeMount('abc123_app-data', '/app/data'),
|
|
['Type' => 'bind', 'Source' => '/data/coolify/secrets/s1', 'Destination' => '/run/secrets/s1'],
|
|
['Type' => 'tmpfs', 'Destination' => '/tmp'],
|
|
]);
|
|
|
|
expect(composeContainersWithRemovedVolumes($projectMounts, ['app' => ['image' => 'app']], 'abc123', []))->toBe([]);
|
|
});
|
|
|
|
it('keeps containers of services that up does not start, and paths it cannot read', function () {
|
|
$projectMounts = implode("\n", [
|
|
inspectedComposeContainer('old-service', ['/data'], [namedVolumeMount('abc123_old', '/data')]),
|
|
inspectedComposeContainer('web', ['/data'], [namedVolumeMount('abc123_web', '/data')]),
|
|
'Error: No such object',
|
|
]);
|
|
|
|
expect(composeContainersWithRemovedVolumes($projectMounts, ['web' => ['volumes' => ['web-data:${DATA_TARGET}']]], 'abc123', []))->toBe([]);
|
|
});
|