mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(volume-backups): skip containers removed during the stop-during-backup scan
Closes #12167
This commit is contained in:
@@ -291,10 +291,16 @@ class VolumeBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
/**
|
||||
* Containers can exit and be removed (e.g. `--rm` backup helpers) between `docker ps` and
|
||||
* `docker inspect`. Skip those, but still fail when an existing container cannot be inspected.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function containersUsingVolume(string $source, Server $server): array
|
||||
{
|
||||
$output = instant_remote_process(
|
||||
["containers=\$(docker ps -q) || exit 1; for container in \$containers; do paused=\$(docker inspect --format '{{.State.Paused}}' \"\$container\") || exit 1; [ \"\$paused\" = true ] && continue; mounts=\$(docker inspect --format '{{range .Mounts}}{{println .Source}}{{println .Name}}{{end}}' \"\$container\") || exit 1; if printf '%s\\n' \"\$mounts\" | grep -Fqx -- ".escapeshellarg($source).'; then echo "$container"; fi; done'],
|
||||
["containers=\$(docker ps -q) || exit 1; for container in \$containers; do mounts=\$(docker inspect --format '{{if not .State.Paused}}{{range .Mounts}}{{println .Source}}{{println .Name}}{{end}}{{end}}' \"\$container\" 2>/dev/null) || { remaining=\$(docker ps -aq --filter \"id=\$container\") || exit 1; [ -z \"\$remaining\" ] && continue; docker inspect \"\$container\" >/dev/null; exit 1; }; if printf '%s\\n' \"\$mounts\" | grep -Fqx -- ".escapeshellarg($source).'; then echo "$container"; fi; done'],
|
||||
$server,
|
||||
disableMultiplexing: true,
|
||||
);
|
||||
|
||||
@@ -40,6 +40,7 @@ use Illuminate\Routing\Redirector;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
@@ -49,6 +50,7 @@ use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Livewire;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\Process\Process as SymfonyProcess;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
@@ -2354,10 +2356,69 @@ it('stops and restarts containers that use the volume when requested', function
|
||||
&& str_contains($process->command, 'state_file='));
|
||||
Process::assertRan(fn ($process) => str_contains($process->command, '{{println .Source}}{{println .Name}}')
|
||||
&& str_contains($process->command, "grep -Fqx -- 'app-data'")
|
||||
&& str_contains($process->command, '{{.State.Paused}}')
|
||||
&& str_contains($process->command, '{{if not .State.Paused}}')
|
||||
&& str_contains($process->command, 'continue'));
|
||||
});
|
||||
|
||||
/**
|
||||
* Runs the recorded container scan script in a local shell with a fake `docker` binary.
|
||||
*
|
||||
* @return array{exit_code: int, output: string}
|
||||
*/
|
||||
function runVolumeContainerScan(string $fakeDocker): array
|
||||
{
|
||||
$scan = '';
|
||||
Process::assertRan(function ($process) use (&$scan): bool {
|
||||
if (! str_contains($process->command, 'docker ps -q')) {
|
||||
return false;
|
||||
}
|
||||
$scan = $process->command;
|
||||
|
||||
return true;
|
||||
});
|
||||
$script = implode("\n", array_slice(explode("\n", $scan), 1, -1));
|
||||
|
||||
$bin = sys_get_temp_dir().'/volume-scan-'.uniqid();
|
||||
mkdir($bin);
|
||||
file_put_contents("{$bin}/docker", "#!/bin/sh\n{$fakeDocker}");
|
||||
chmod("{$bin}/docker", 0755);
|
||||
|
||||
$result = new SymfonyProcess(['sh', '-c', $script], env: ['PATH' => $bin.':'.getenv('PATH')]);
|
||||
$result->run();
|
||||
File::deleteDirectory($bin);
|
||||
|
||||
return ['exit_code' => $result->getExitCode(), 'output' => trim($result->getOutput())];
|
||||
}
|
||||
|
||||
it('skips containers that are removed while scanning for volume users', function () {
|
||||
config(['broadcasting.default' => 'null']);
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::create(['id' => 0]));
|
||||
$team = Team::factory()->create();
|
||||
[$application, $volume] = createVolumeBackupApplication($team);
|
||||
$backup = $volume->scheduledBackups()->create([
|
||||
'team_id' => $team->id,
|
||||
'frequency' => 'daily',
|
||||
'stop_during_backup' => true,
|
||||
]);
|
||||
|
||||
Process::fake(['*du -b*' => '128', '*' => '']);
|
||||
(new VolumeBackupJob($backup))->handle();
|
||||
|
||||
$docker = <<<'SH'
|
||||
case "$*" in
|
||||
"ps -q") printf 'aaa111\nbbb222\nccc333\n' ;;
|
||||
"ps -aq --filter id=aaa111") ;;
|
||||
"ps -aq --filter id=ccc333") echo ccc333 ;;
|
||||
*aaa111*) echo 'error: no such object: aaa111' >&2; exit 1 ;;
|
||||
*bbb222*) printf '/var/lib/docker/volumes/app-data/_data\napp-data\n' ;;
|
||||
*ccc333*) [ "$CCC_BROKEN" = 1 ] && { echo 'daemon error' >&2; exit 1; }; printf 'other\n' ;;
|
||||
esac
|
||||
SH;
|
||||
|
||||
expect(runVolumeContainerScan($docker))->toBe(['exit_code' => 0, 'output' => 'bbb222'])
|
||||
->and(runVolumeContainerScan("CCC_BROKEN=1\n".$docker)['exit_code'])->not->toBe(0);
|
||||
});
|
||||
|
||||
it('finds containers using a bind mounted host path before stopping', function () {
|
||||
config(['broadcasting.default' => 'null']);
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::create(['id' => 0]));
|
||||
|
||||
Reference in New Issue
Block a user