fix(backups): stop containers during volume backup creation
Track and recover stopped containers across interrupted backup executions.
This commit is contained in:
parent
63961e0799
commit
4eec1ac547
11 changed files with 131 additions and 81 deletions
|
|
@ -112,7 +112,7 @@ public function handle(): void
|
|||
}
|
||||
|
||||
try {
|
||||
$this->recoverPausedVolumeBackupContainers();
|
||||
$this->recoverStoppedVolumeBackupContainers();
|
||||
$this->processScheduledVolumeBackups();
|
||||
} catch (\Exception $e) {
|
||||
Log::channel('scheduled-errors')->error('Failed to process scheduled volume backups', [
|
||||
|
|
@ -365,11 +365,11 @@ private function processScheduledVolumeBackups(): void
|
|||
});
|
||||
}
|
||||
|
||||
private function recoverPausedVolumeBackupContainers(): void
|
||||
private function recoverStoppedVolumeBackupContainers(): void
|
||||
{
|
||||
ScheduledVolumeBackupExecution::query()
|
||||
->where(fn (Builder $query) => $query
|
||||
->where('pause_recovery_pending', true)
|
||||
->where('stop_recovery_pending', true)
|
||||
->orWhere('s3_cleanup_pending', true))
|
||||
->chunkById(self::CHUNK_SIZE, function ($executions): void {
|
||||
foreach ($executions as $execution) {
|
||||
|
|
@ -385,7 +385,7 @@ private function processScheduledVolumeBackup(ScheduledVolumeBackup $backup): vo
|
|||
|
||||
if ($backup->executions()
|
||||
->where(fn (Builder $query) => $query
|
||||
->where('pause_recovery_pending', true)
|
||||
->where('stop_recovery_pending', true)
|
||||
->orWhere('s3_cleanup_pending', true))
|
||||
->exists()) {
|
||||
$this->skippedCount++;
|
||||
|
|
|
|||
|
|
@ -83,11 +83,11 @@ public function handle(): void
|
|||
.' '.escapeshellarg($image)
|
||||
.' tar -czf - -C /volume . > '.escapeshellarg($backupLocation);
|
||||
|
||||
if ($this->backup->pause_during_backup) {
|
||||
if ($this->backup->stop_during_backup) {
|
||||
$containers = $this->containersUsingVolume($source, $server);
|
||||
if ($containers !== []) {
|
||||
$this->execution->update(['pause_recovery_pending' => true]);
|
||||
$archiveCommand = $this->archiveWithPausedContainers(
|
||||
$this->execution->update(['stop_recovery_pending' => true]);
|
||||
$archiveCommand = $this->archiveWithStoppedContainers(
|
||||
$archiveCommand,
|
||||
$containers,
|
||||
VolumeBackupRecoveryJob::stateFile($this->execution),
|
||||
|
|
@ -101,8 +101,8 @@ public function handle(): void
|
|||
$archiveCommand,
|
||||
], $server, timeout: $this->timeout, disableMultiplexing: true);
|
||||
$this->execution->update([
|
||||
'pause_container_ids' => null,
|
||||
'pause_recovery_pending' => false,
|
||||
'stop_container_ids' => null,
|
||||
'stop_recovery_pending' => false,
|
||||
]);
|
||||
|
||||
$size = (int) instant_remote_process(
|
||||
|
|
@ -253,7 +253,7 @@ private function containersUsingVolume(string $source, Server $server): array
|
|||
/**
|
||||
* @param array<int, string> $containers
|
||||
*/
|
||||
private function archiveWithPausedContainers(string $archiveCommand, array $containers, string $stateFile): string
|
||||
private function archiveWithStoppedContainers(string $archiveCommand, array $containers, string $stateFile): string
|
||||
{
|
||||
if ($containers === []) {
|
||||
return $archiveCommand;
|
||||
|
|
@ -263,9 +263,12 @@ private function archiveWithPausedContainers(string $archiveCommand, array $cont
|
|||
$script = "set -eu\n"
|
||||
.'state_file='.escapeshellarg($stateFile)."\n"
|
||||
.": > \"\$state_file\"\n"
|
||||
."cleanup() { status=\$?; trap - EXIT HUP INT TERM; while IFS= read -r container; do docker unpause \"\$container\" >/dev/null || status=1; done < \"\$state_file\"; [ \$status -eq 0 ] && rm -f \"\$state_file\"; exit \$status; }\n"
|
||||
."trap cleanup EXIT HUP INT TERM\n"
|
||||
."for container in {$containerList}; do echo \"\$container\" >> \"\$state_file\"; docker pause \"\$container\" >/dev/null; done\n"
|
||||
."cleanup() { status=\$?; trap - EXIT HUP INT TERM; while IFS= read -r container; do docker start \"\$container\" >/dev/null || status=1; done < \"\$state_file\"; [ \$status -eq 0 ] && rm -f \"\$state_file\"; exit \$status; }\n"
|
||||
."trap cleanup EXIT\n"
|
||||
."trap 'exit 129' HUP\n"
|
||||
."trap 'exit 130' INT\n"
|
||||
."trap 'exit 143' TERM\n"
|
||||
."for container in {$containerList}; do echo \"\$container\" >> \"\$state_file\"; docker stop \"\$container\" >/dev/null; done\n"
|
||||
.$archiveCommand;
|
||||
|
||||
return 'sh -c '.escapeshellarg($script);
|
||||
|
|
@ -273,7 +276,7 @@ private function archiveWithPausedContainers(string $archiveCommand, array $cont
|
|||
|
||||
private function recoverIncompleteBackup(ScheduledVolumeBackupExecution $execution): string
|
||||
{
|
||||
if (! $execution->pause_recovery_pending && ! $execution->s3_cleanup_pending) {
|
||||
if (! $execution->stop_recovery_pending && ! $execution->s3_cleanup_pending) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public static function recover(ScheduledVolumeBackupExecution $execution): void
|
|||
{
|
||||
$execution->loadMissing('scheduledVolumeBackup.volume.resource');
|
||||
|
||||
if ($execution->pause_recovery_pending) {
|
||||
if ($execution->stop_recovery_pending) {
|
||||
self::recoverContainers($execution);
|
||||
}
|
||||
|
||||
|
|
@ -75,22 +75,22 @@ private static function recoverContainers(ScheduledVolumeBackupExecution $execut
|
|||
->filter(fn (string $container): bool => preg_match('/^[a-f0-9]{6,64}$/i', $container) === 1)
|
||||
->values()
|
||||
->all();
|
||||
$execution->update(['pause_container_ids' => $containers]);
|
||||
$execution->update(['stop_container_ids' => $containers]);
|
||||
|
||||
$remainingFile = $stateFile.'.remaining';
|
||||
$script = 'status=0; : > '.escapeshellarg($remainingFile).'; '
|
||||
.'if [ -f '.escapeshellarg($stateFile).' ]; then while IFS= read -r container; do '
|
||||
.'[ -z "$container" ] && continue; paused=$(docker inspect --format \'{{.State.Paused}}\' "$container" 2>/dev/null) '
|
||||
.'[ -z "$container" ] && continue; running=$(docker inspect --format \'{{.State.Running}}\' "$container" 2>/dev/null) '
|
||||
.'|| { echo "$container" >> '.escapeshellarg($remainingFile).'; status=1; continue; }; '
|
||||
.'if [ "$paused" = true ] && ! docker unpause "$container" >/dev/null; then echo "$container" >> '
|
||||
.'if [ "$running" != true ] && ! docker start "$container" >/dev/null; then echo "$container" >> '
|
||||
.escapeshellarg($remainingFile).'; status=1; fi; done < '.escapeshellarg($stateFile).'; fi; '
|
||||
.'if [ -s '.escapeshellarg($remainingFile).' ]; then mv '.escapeshellarg($remainingFile).' '.escapeshellarg($stateFile)
|
||||
.'; else rm -f '.escapeshellarg($stateFile).' '.escapeshellarg($remainingFile).'; fi; exit $status';
|
||||
|
||||
instant_remote_process(['sh -c '.escapeshellarg($script)], $server, disableMultiplexing: true);
|
||||
$execution->update([
|
||||
'pause_container_ids' => null,
|
||||
'pause_recovery_pending' => false,
|
||||
'stop_container_ids' => null,
|
||||
'stop_recovery_pending' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +112,6 @@ public static function cleanupS3Upload(ScheduledVolumeBackupExecution $execution
|
|||
|
||||
public static function stateFile(ScheduledVolumeBackupExecution $execution): string
|
||||
{
|
||||
return '/tmp/coolify-volume-backup-'.$execution->uuid.'.paused';
|
||||
return '/tmp/coolify-volume-backup-'.$execution->uuid.'.stopped';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class VolumeBackups extends Component
|
|||
|
||||
public bool $disableLocalBackup = false;
|
||||
|
||||
public bool $pauseDuringBackup = false;
|
||||
public bool $stopDuringBackup = false;
|
||||
|
||||
public ?int $s3StorageId = null;
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ protected function rules(): array
|
|||
'enabled' => ['required', 'boolean'],
|
||||
'saveToS3' => ['required', 'boolean'],
|
||||
'disableLocalBackup' => ['required', 'boolean'],
|
||||
'pauseDuringBackup' => ['required', 'boolean'],
|
||||
'stopDuringBackup' => ['required', 'boolean'],
|
||||
's3StorageId' => ['nullable', 'integer'],
|
||||
'retentionAmountLocally' => ['required', 'integer', 'min:0', 'max:10000'],
|
||||
'retentionDaysLocally' => ['required', 'integer', 'min:0'],
|
||||
|
|
@ -90,7 +90,7 @@ public function mount(): void
|
|||
$this->enabled = $this->backup->enabled;
|
||||
$this->saveToS3 = $this->backup->save_s3;
|
||||
$this->disableLocalBackup = $this->backup->disable_local_backup;
|
||||
$this->pauseDuringBackup = $this->backup->pause_during_backup;
|
||||
$this->stopDuringBackup = $this->backup->stop_during_backup;
|
||||
$this->s3StorageId = $this->backup->s3_storage_id ?? $this->availableS3Storages->first()?->id;
|
||||
$this->retentionAmountLocally = $this->backup->retention_amount_locally;
|
||||
$this->retentionDaysLocally = $this->backup->retention_days_locally;
|
||||
|
|
@ -188,7 +188,7 @@ public function delete(?string $password = null, array $selectedActions = [])
|
|||
if ($this->backup->executions()
|
||||
->where(fn ($query) => $query
|
||||
->where('status', 'running')
|
||||
->orWhere('pause_recovery_pending', true)
|
||||
->orWhere('stop_recovery_pending', true)
|
||||
->orWhere('s3_cleanup_pending', true))
|
||||
->exists()) {
|
||||
$this->dispatch('error', 'Wait for the running volume backup and container recovery to finish before deleting this schedule.');
|
||||
|
|
@ -246,7 +246,7 @@ public function cleanupFailed(): void
|
|||
|
||||
$deletedCount = $this->backup?->executions()
|
||||
->where('status', 'failed')
|
||||
->where('pause_recovery_pending', false)
|
||||
->where('stop_recovery_pending', false)
|
||||
->where('s3_cleanup_pending', false)
|
||||
->where(fn ($query) => $query
|
||||
->whereNull('filename')
|
||||
|
|
@ -292,7 +292,7 @@ public function deleteBackup(int $executionId, string $password, array $selected
|
|||
return false;
|
||||
}
|
||||
|
||||
if ($execution->status === 'running' || $execution->pause_recovery_pending || $execution->s3_cleanup_pending) {
|
||||
if ($execution->status === 'running' || $execution->stop_recovery_pending || $execution->s3_cleanup_pending) {
|
||||
$this->dispatch('error', 'Wait for the backup and recovery operations to finish before deleting it.');
|
||||
|
||||
return false;
|
||||
|
|
@ -369,7 +369,7 @@ private function persistBackup(bool $enabled): ScheduledVolumeBackup
|
|||
'enabled' => $enabled,
|
||||
'save_s3' => $this->saveToS3,
|
||||
'disable_local_backup' => $this->disableLocalBackup,
|
||||
'pause_during_backup' => $this->pauseDuringBackup,
|
||||
'stop_during_backup' => $this->stopDuringBackup,
|
||||
's3_storage_id' => $this->saveToS3 ? $this->s3StorageId : null,
|
||||
'retention_amount_locally' => $this->retentionAmountLocally,
|
||||
'retention_days_locally' => $this->retentionDaysLocally,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class ScheduledVolumeBackup extends BaseModel
|
|||
'enabled',
|
||||
'save_s3',
|
||||
'disable_local_backup',
|
||||
'pause_during_backup',
|
||||
'stop_during_backup',
|
||||
'retention_amount_locally',
|
||||
'retention_days_locally',
|
||||
'retention_max_storage_locally',
|
||||
|
|
@ -33,7 +33,7 @@ protected function casts(): array
|
|||
'enabled' => 'boolean',
|
||||
'save_s3' => 'boolean',
|
||||
'disable_local_backup' => 'boolean',
|
||||
'pause_during_backup' => 'boolean',
|
||||
'stop_during_backup' => 'boolean',
|
||||
'retention_amount_locally' => 'integer',
|
||||
'retention_days_locally' => 'integer',
|
||||
'retention_max_storage_locally' => 'float',
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ class ScheduledVolumeBackupExecution extends BaseModel
|
|||
'message',
|
||||
'size',
|
||||
'filename',
|
||||
'pause_container_ids',
|
||||
'pause_recovery_pending',
|
||||
'stop_container_ids',
|
||||
'stop_recovery_pending',
|
||||
's3_cleanup_pending',
|
||||
'finished_at',
|
||||
'local_storage_deleted',
|
||||
|
|
@ -27,8 +27,8 @@ protected function casts(): array
|
|||
return [
|
||||
'size' => 'integer',
|
||||
'finished_at' => 'datetime',
|
||||
'pause_container_ids' => 'array',
|
||||
'pause_recovery_pending' => 'boolean',
|
||||
'stop_container_ids' => 'array',
|
||||
'stop_recovery_pending' => 'boolean',
|
||||
's3_cleanup_pending' => 'boolean',
|
||||
'local_storage_deleted' => 'boolean',
|
||||
's3_storage_deleted' => 'boolean',
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ public function up(): void
|
|||
$table->boolean('enabled')->default(true);
|
||||
$table->boolean('save_s3')->default(false);
|
||||
$table->boolean('disable_local_backup')->default(false);
|
||||
$table->boolean('pause_during_backup')->default(false);
|
||||
$table->boolean('stop_during_backup')->default(false);
|
||||
$table->unsignedInteger('retention_amount_locally')->default(7);
|
||||
$table->unsignedInteger('retention_amount_s3')->default(7);
|
||||
$table->unsignedInteger('timeout')->default(3600);
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ public function up(): void
|
|||
$table->longText('message')->nullable();
|
||||
$table->unsignedBigInteger('size')->default(0);
|
||||
$table->text('filename')->nullable();
|
||||
$table->json('pause_container_ids')->nullable();
|
||||
$table->boolean('pause_recovery_pending')->default(false);
|
||||
$table->json('stop_container_ids')->nullable();
|
||||
$table->boolean('stop_recovery_pending')->default(false);
|
||||
$table->boolean('s3_cleanup_pending')->default(false);
|
||||
$table->timestamp('finished_at')->nullable();
|
||||
$table->boolean('local_storage_deleted')->default(false);
|
||||
|
|
|
|||
|
|
@ -65,6 +65,15 @@
|
|||
</div>
|
||||
@endcan
|
||||
@endif
|
||||
@if ($resource instanceof \App\Models\Application)
|
||||
@can('update', $resource)
|
||||
<x-modal-input buttonTitle="Configure Backup" title="Configure Volume Backup" :wireIgnore="false">
|
||||
<livewire:project.application.backup.create :application="$resource"
|
||||
:selected-volume-id="$storage->id"
|
||||
wire:key="configure-readonly-volume-backup-{{ $storage->id }}" />
|
||||
</x-modal-input>
|
||||
@endcan
|
||||
@endif
|
||||
@else
|
||||
@can('update', $resource)
|
||||
@if ($isFirst)
|
||||
|
|
@ -150,15 +159,4 @@
|
|||
@endcan
|
||||
@endif
|
||||
</form>
|
||||
@if ($isReadOnly && $resource instanceof \App\Models\Application)
|
||||
@can('update', $resource)
|
||||
<div class="pt-2">
|
||||
<x-modal-input buttonTitle="Configure Backup" title="Configure Volume Backup" :wireIgnore="false">
|
||||
<livewire:project.application.backup.create :application="$resource"
|
||||
:selected-volume-id="$storage->id"
|
||||
wire:key="configure-readonly-volume-backup-{{ $storage->id }}" />
|
||||
</x-modal-input>
|
||||
</div>
|
||||
@endcan
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -33,12 +33,16 @@
|
|||
<span class="font-medium text-neutral-800 dark:text-neutral-200">{{ $storage->name }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-3 text-sm rounded bg-warning/10 text-warning">
|
||||
Backups made while the application is writing to this volume may be inconsistent or corrupted. You can
|
||||
gracefully stop containers during the archive step for a safer file-level backup, but this briefly
|
||||
interrupts the application.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full max-w-md">
|
||||
<x-forms.checkbox id="pauseDuringBackup" label="Pause containers while creating the archive"
|
||||
helper="Off by default. Containers using this volume are resumed immediately after the archive is created." />
|
||||
<x-forms.checkbox instantSave id="stopDuringBackup" label="Stop containers while creating the archive"
|
||||
helper="Off by default. Containers using this volume are gracefully stopped and restarted immediately after the archive is created." />
|
||||
@if ($availableS3Storages->isNotEmpty())
|
||||
<x-forms.checkbox instantSave id="saveToS3" label="S3 Enabled" />
|
||||
@elseif ($saveToS3)
|
||||
|
|
@ -80,11 +84,7 @@
|
|||
|
||||
<div class="flex flex-col gap-4">
|
||||
<h3>Settings</h3>
|
||||
<div class="p-3 text-sm rounded bg-warning/10 text-warning">
|
||||
Backups made while the application is writing to this volume may be inconsistent or corrupted. You can
|
||||
pause containers during the archive step for a more consistent backup, but this briefly interrupts the
|
||||
application.
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
|
||||
<x-forms.input id="frequency" label="Frequency" required
|
||||
helper="Use every_minute, hourly, daily, weekly, monthly, yearly, or a cron expression." />
|
||||
|
|
|
|||
|
|
@ -96,18 +96,25 @@
|
|||
->assertSee($volume->name);
|
||||
});
|
||||
|
||||
it('shows the configure backup modal trigger instead of inline backup settings on a volume', function () {
|
||||
it('shows the configure backup modal trigger inside the volume card instead of inline backup settings', function () {
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::create(['id' => 0]));
|
||||
$team = Team::factory()->create();
|
||||
signInForVolumeBackups($this, $team);
|
||||
[$application, $volume] = createVolumeBackupApplication($team);
|
||||
|
||||
Livewire::test(Show::class, [
|
||||
$component = Livewire::test(Show::class, [
|
||||
'storage' => $volume,
|
||||
'resource' => $application,
|
||||
])
|
||||
->set('isReadOnly', true)
|
||||
->assertSee('Configure Backup')
|
||||
->assertDontSee('Backups made while the application is writing');
|
||||
|
||||
$html = $component->html();
|
||||
|
||||
expect(strpos($html, 'Configure Backup'))
|
||||
->toBeGreaterThan(strpos($html, '<form'))
|
||||
->toBeLessThan(strpos($html, '</form>'));
|
||||
});
|
||||
|
||||
it('only shows the backup enabled badge for an enabled volume backup', function () {
|
||||
|
|
@ -151,7 +158,7 @@
|
|||
'enabled',
|
||||
'save_s3',
|
||||
'disable_local_backup',
|
||||
'pause_during_backup',
|
||||
'stop_during_backup',
|
||||
'retention_amount_locally',
|
||||
'retention_days_locally',
|
||||
'retention_max_storage_locally',
|
||||
|
|
@ -167,8 +174,8 @@
|
|||
'message',
|
||||
'size',
|
||||
'filename',
|
||||
'pause_container_ids',
|
||||
'pause_recovery_pending',
|
||||
'stop_container_ids',
|
||||
'stop_recovery_pending',
|
||||
's3_cleanup_pending',
|
||||
'finished_at',
|
||||
'local_storage_deleted',
|
||||
|
|
@ -215,7 +222,7 @@
|
|||
'Backup Now',
|
||||
'Delete Backups and Schedule',
|
||||
'Persistent volume:',
|
||||
'Pause containers while creating the archive',
|
||||
'Stop containers while creating the archive',
|
||||
'S3 Enabled',
|
||||
'Disable Local Backup',
|
||||
'S3 Storage',
|
||||
|
|
@ -397,11 +404,12 @@ function signInForVolumeBackups($testCase, Team $team): User
|
|||
->assertSee('Scheduled Backup')
|
||||
->assertSee('Persistent volume:')
|
||||
->assertSee('inconsistent or corrupted')
|
||||
->assertSee('gracefully stop containers')
|
||||
->set('frequency', 'daily')
|
||||
->set('retentionAmountLocally', 5)
|
||||
->set('retentionDaysLocally', 14)
|
||||
->set('retentionMaxStorageLocally', 1.5)
|
||||
->set('pauseDuringBackup', true)
|
||||
->set('stopDuringBackup', true)
|
||||
->call('save')
|
||||
->assertDispatched('success');
|
||||
|
||||
|
|
@ -413,7 +421,7 @@ function signInForVolumeBackups($testCase, Team $team): User
|
|||
->and($backup->retention_amount_locally)->toBe(5)
|
||||
->and($backup->retention_days_locally)->toBe(14)
|
||||
->and($backup->retention_max_storage_locally)->toBe(1.5)
|
||||
->and($backup->pause_during_backup)->toBeTrue()
|
||||
->and($backup->stop_during_backup)->toBeTrue()
|
||||
->and($backup->save_s3)->toBeFalse();
|
||||
});
|
||||
|
||||
|
|
@ -487,6 +495,45 @@ function signInForVolumeBackups($testCase, Team $team): User
|
|||
->and($backup->fresh()->disable_local_backup)->toBeFalse();
|
||||
});
|
||||
|
||||
it('saves each editable volume backup checkbox immediately', function () {
|
||||
$team = Team::factory()->create();
|
||||
signInForVolumeBackups($this, $team);
|
||||
[$application, $volume] = createVolumeBackupApplication($team);
|
||||
$s3Storage = S3Storage::create([
|
||||
'name' => 'Volume backups',
|
||||
'region' => 'us-east-1',
|
||||
'key' => 'key',
|
||||
'secret' => 'secret',
|
||||
'bucket' => 'bucket',
|
||||
'endpoint' => 'https://s3.example.com',
|
||||
'team_id' => $team->id,
|
||||
'is_usable' => true,
|
||||
]);
|
||||
$backup = ScheduledVolumeBackup::create([
|
||||
'local_persistent_volume_id' => $volume->id,
|
||||
'team_id' => $team->id,
|
||||
's3_storage_id' => $s3Storage->id,
|
||||
'frequency' => 'daily',
|
||||
'save_s3' => true,
|
||||
]);
|
||||
|
||||
$component = Livewire::test(VolumeBackups::class, ['storage' => $volume, 'resource' => $application]);
|
||||
$html = $component->html();
|
||||
|
||||
foreach (['stopDuringBackup', 'saveToS3', 'disableLocalBackup'] as $property) {
|
||||
preg_match('/<input\b(?=[^>]*wire:model=(?:"'.$property.'"|'.$property.'))[^>]*>/', $html, $matches);
|
||||
|
||||
expect($matches[0] ?? null)->not->toBeNull()
|
||||
->and($matches[0])->toContain("wire:click='instantSave'");
|
||||
}
|
||||
|
||||
$component->set('stopDuringBackup', true)->call('instantSave')->assertDispatched('success');
|
||||
expect($backup->refresh()->stop_during_backup)->toBeTrue();
|
||||
|
||||
$component->set('stopDuringBackup', false)->call('instantSave')->assertDispatched('success');
|
||||
expect($backup->refresh()->stop_during_backup)->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows volume S3 backups to be disabled when no usable storage remains', function () {
|
||||
$team = Team::factory()->create();
|
||||
signInForVolumeBackups($this, $team);
|
||||
|
|
@ -829,7 +876,7 @@ function signInForVolumeBackups($testCase, Team $team): User
|
|||
->and($oldExecution->fresh()->local_storage_deleted)->toBeFalse();
|
||||
});
|
||||
|
||||
it('pauses and resumes containers that use the volume when requested', function () {
|
||||
it('stops and restarts containers that use the volume when requested', function () {
|
||||
config(['broadcasting.default' => 'null']);
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::create(['id' => 0]));
|
||||
$team = Team::factory()->create();
|
||||
|
|
@ -838,7 +885,7 @@ function signInForVolumeBackups($testCase, Team $team): User
|
|||
'local_persistent_volume_id' => $volume->id,
|
||||
'team_id' => $team->id,
|
||||
'frequency' => 'daily',
|
||||
'pause_during_backup' => true,
|
||||
'stop_during_backup' => true,
|
||||
]);
|
||||
|
||||
Process::fake([
|
||||
|
|
@ -849,9 +896,11 @@ function signInForVolumeBackups($testCase, Team $team): User
|
|||
|
||||
(new VolumeBackupJob($backup))->handle();
|
||||
|
||||
Process::assertRan(fn ($process) => str_contains($process->command, 'trap cleanup EXIT HUP INT TERM')
|
||||
&& str_contains($process->command, 'docker pause')
|
||||
&& str_contains($process->command, 'docker unpause')
|
||||
Process::assertRan(fn ($process) => str_contains($process->command, 'trap cleanup EXIT')
|
||||
&& str_contains($process->command, 'exit 143')
|
||||
&& str_contains($process->command, 'TERM')
|
||||
&& str_contains($process->command, 'docker stop')
|
||||
&& str_contains($process->command, 'docker start')
|
||||
&& 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'")
|
||||
|
|
@ -859,7 +908,7 @@ function signInForVolumeBackups($testCase, Team $team): User
|
|||
&& str_contains($process->command, 'continue'));
|
||||
});
|
||||
|
||||
it('finds containers using a bind mounted host path before pausing', function () {
|
||||
it('finds containers using a bind mounted host path before stopping', function () {
|
||||
config(['broadcasting.default' => 'null']);
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::create(['id' => 0]));
|
||||
$team = Team::factory()->create();
|
||||
|
|
@ -869,7 +918,7 @@ function signInForVolumeBackups($testCase, Team $team): User
|
|||
'local_persistent_volume_id' => $volume->id,
|
||||
'team_id' => $team->id,
|
||||
'frequency' => 'daily',
|
||||
'pause_during_backup' => true,
|
||||
'stop_during_backup' => true,
|
||||
]);
|
||||
|
||||
Process::fake([
|
||||
|
|
@ -884,7 +933,7 @@ function signInForVolumeBackups($testCase, Team $team): User
|
|||
&& str_contains($process->command, "grep -Fqx -- '/srv/app-data'"));
|
||||
});
|
||||
|
||||
it('retries container recovery when a timed out backup left a container paused', function () {
|
||||
it('retries container recovery when a timed out backup left a container stopped', function () {
|
||||
Queue::fake();
|
||||
$team = Team::factory()->create();
|
||||
[$application, $volume] = createVolumeBackupApplication($team);
|
||||
|
|
@ -896,23 +945,23 @@ function signInForVolumeBackups($testCase, Team $team): User
|
|||
$execution = ScheduledVolumeBackupExecution::create([
|
||||
'scheduled_volume_backup_id' => $backup->id,
|
||||
'status' => 'running',
|
||||
'pause_recovery_pending' => true,
|
||||
'stop_recovery_pending' => true,
|
||||
]);
|
||||
Process::fake([
|
||||
'*cat *coolify-volume-backup-*' => 'abc123',
|
||||
'*docker unpause*' => Process::result(errorOutput: 'daemon unavailable', exitCode: 1),
|
||||
'*docker start*' => Process::result(errorOutput: 'daemon unavailable', exitCode: 1),
|
||||
]);
|
||||
|
||||
(new VolumeBackupJob($backup))->failed(new RuntimeException('Worker timed out'));
|
||||
|
||||
expect($execution->fresh()->status)->toBe('failed')
|
||||
->and($execution->fresh()->message)->toContain('Container recovery failed')
|
||||
->and($execution->fresh()->pause_container_ids)->toBe(['abc123'])
|
||||
->and($execution->fresh()->pause_recovery_pending)->toBeTrue();
|
||||
->and($execution->fresh()->stop_container_ids)->toBe(['abc123'])
|
||||
->and($execution->fresh()->stop_recovery_pending)->toBeTrue();
|
||||
Queue::assertPushed(VolumeBackupRecoveryJob::class);
|
||||
});
|
||||
|
||||
it('resumes containers and removes a partial archive when archiving fails', function () {
|
||||
it('restarts containers and removes a partial archive when archiving fails', function () {
|
||||
config(['broadcasting.default' => 'null']);
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::create(['id' => 0]));
|
||||
$team = Team::factory()->create();
|
||||
|
|
@ -921,11 +970,11 @@ function signInForVolumeBackups($testCase, Team $team): User
|
|||
'local_persistent_volume_id' => $volume->id,
|
||||
'team_id' => $team->id,
|
||||
'frequency' => 'daily',
|
||||
'pause_during_backup' => true,
|
||||
'stop_during_backup' => true,
|
||||
]);
|
||||
Process::fake([
|
||||
'*docker ps -q*' => 'abc123',
|
||||
'*trap cleanup EXIT HUP INT TERM*' => Process::result(errorOutput: 'archive failed', exitCode: 1),
|
||||
'*trap cleanup EXIT*' => Process::result(errorOutput: 'archive failed', exitCode: 1),
|
||||
'*' => '',
|
||||
]);
|
||||
|
||||
|
|
@ -933,7 +982,7 @@ function signInForVolumeBackups($testCase, Team $team): User
|
|||
|
||||
$execution = ScheduledVolumeBackupExecution::query()->sole();
|
||||
expect($execution->status)->toBe('failed')
|
||||
->and($execution->pause_container_ids)->toBeNull();
|
||||
->and($execution->stop_container_ids)->toBeNull();
|
||||
Process::assertRan(fn ($process) => str_contains($process->command, 'rm -f')
|
||||
&& str_contains($process->command, '.tar.gz'));
|
||||
});
|
||||
|
|
@ -1079,7 +1128,7 @@ function signInForVolumeBackups($testCase, Team $team): User
|
|||
$execution = ScheduledVolumeBackupExecution::create([
|
||||
'scheduled_volume_backup_id' => $backup->id,
|
||||
'status' => 'failed',
|
||||
'pause_recovery_pending' => true,
|
||||
'stop_recovery_pending' => true,
|
||||
]);
|
||||
|
||||
(new ScheduledJobManager)->handle();
|
||||
|
|
|
|||
Loading…
Reference in a new issue