Files
coolify/tests/Feature/CleanupHelperContainersJobRcloneTest.php
033f0c84da v4.7 (#12478)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Co-authored-by: Aditya Tripathi <aditya@climactic.co>
2026-11-06 14:25:02 +01:00

139 lines
5.8 KiB
PHP

<?php
use App\Enums\ProcessStatus;
use App\Jobs\CleanupHelperContainersJob;
use App\Models\PrivateKey;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledVolumeBackup;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Process\PendingProcess;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Spatie\Activitylog\Models\Activity;
uses(RefreshDatabase::class);
beforeEach(function () {
Server::flushIdentityMap();
Storage::fake('ssh-keys');
Carbon::setTestNow('2026-11-01 12:00:00');
$this->team = Team::factory()->create();
});
afterEach(function () {
Carbon::setTestNow();
});
/**
* @param array<int, array{0: string, 1: string}> $containers name and creation time
*/
function fakeRcloneContainers(array $containers, array &$scripts): void
{
$lines = collect($containers)->map(fn (array $container, int $index): string => json_encode([
'ID' => 'id'.$index,
'Names' => $container[0],
'Image' => 'docker.io/rclone/rclone:1.75.2',
'CreatedAt' => $container[1].' +0000 UTC',
]))->implode("\n");
Process::fake(function (PendingProcess $process) use ($lines, &$scripts) {
$scripts[] = $script = remoteProcessScript($process);
return Process::result(str_contains($script, 'label=coolify.type=rclone') ? $lines : '[]');
});
}
it('removes rclone containers of ended jobs, and keeps those of running jobs', function (string $user, string $docker) {
$server = Server::factory()->create([
'team_id' => $this->team->id,
'user' => $user,
'private_key_id' => PrivateKey::factory()->create(['team_id' => $this->team->id])->id,
]);
$databaseBackup = ScheduledDatabaseBackup::create([
'frequency' => '0 0 * * *',
'database_type' => StandalonePostgresql::class,
'database_id' => 1,
'team_id' => $this->team->id,
]);
$databaseBackup->executions()->create(['uuid' => 'runningdb', 'status' => 'running']);
$databaseBackup->executions()->create(['uuid' => 'endeddb', 'status' => 'failed']);
$volumeBackup = ScheduledVolumeBackup::create([
'backupable_type' => StandalonePostgresql::class,
'backupable_id' => 1,
'team_id' => $this->team->id,
'frequency' => 'daily',
]);
$volumeExecution = $volumeBackup->executions()->create(['status' => 'running']);
$importOperation = (string) Str::uuid();
Activity::create(['log_name' => 'default', 'description' => 'import', 'properties' => [
'operation' => 'database_import',
'operation_uuid' => $importOperation,
'status' => ProcessStatus::IN_PROGRESS->value,
]]);
$scripts = [];
fakeRcloneContainers([
['backup-of-runningdb', '2026-11-01 10:00:00'],
['backup-of-endeddb', '2026-11-01 10:00:00'],
["volume-upload-{$volumeExecution->uuid}", '2026-11-01 10:00:00'],
["s3-restore-{$importOperation}", '2026-11-01 10:00:00'],
['s3-restore-'.Str::uuid(), '2026-11-01 10:00:00'],
['backup-of-juststarted', '2026-11-01 11:55:00'],
], $scripts);
(new CleanupHelperContainersJob($server))->handle();
$removed = collect(range(0, 5))->filter(fn (int $index): bool => collect($scripts)
->contains(fn (string $script): bool => str_contains($script, "{$docker} container rm -f 'id{$index}'")))->values()->all();
expect($removed)->toBe([1, 4]);
})->with([
'root' => ['root', 'docker'],
'non-root' => ['ubuntu', 'sudo docker'],
]);
it('keeps the helper containers of running backups and imports and removes orphaned backup uploads', function () {
$server = Server::factory()->create([
'team_id' => $this->team->id,
'private_key_id' => PrivateKey::factory()->create(['team_id' => $this->team->id])->id,
]);
ScheduledDatabaseBackup::create([
'frequency' => '0 0 * * *',
'database_type' => StandalonePostgresql::class,
'database_id' => 1,
'team_id' => $this->team->id,
])->executions()->create(['uuid' => 'runningdb', 'status' => 'running']);
$volumeExecution = ScheduledVolumeBackup::create([
'backupable_type' => StandalonePostgresql::class,
'backupable_id' => 1,
'team_id' => $this->team->id,
'frequency' => 'daily',
])->executions()->create(['status' => 'running']);
$importOperation = (string) Str::uuid();
Activity::create(['log_name' => 'default', 'description' => 'import', 'properties' => [
'operation' => 'database_import',
'operation_uuid' => $importOperation,
'status' => ProcessStatus::QUEUED->value,
]]);
$scripts = [];
Process::fake(function (PendingProcess $process) use (&$scripts, $volumeExecution, $importOperation) {
$scripts[] = $script = remoteProcessScript($process);
return Process::result(str_contains($script, 'coolify-helper') ? json_encode([
['ID' => 'running', 'Names' => 'backup-of-runningdb', 'Image' => 'ghcr.io/coollabsio/coolify-helper:1.0.13'],
['ID' => 'running', 'Names' => "volume-backup-{$volumeExecution->uuid}", 'Image' => 'ghcr.io/coollabsio/coolify-helper:1.0.13'],
['ID' => 'running', 'Names' => "backup-decompress-{$importOperation}", 'Image' => 'ghcr.io/coollabsio/coolify-helper:1.0.13'],
['ID' => 'orphan', 'Names' => 'backup-of-endeddb', 'Image' => 'ghcr.io/coollabsio/coolify-helper:1.0.13'],
]) : '');
});
(new CleanupHelperContainersJob($server))->handle();
$removals = collect($scripts)->filter(fn (string $script): bool => str_contains($script, 'docker container rm -f'))->values();
expect($removals)->toHaveCount(1)
->and($removals->first())->toContain("docker container rm -f 'orphan'");
});