Merge remote-tracking branch 'origin/next' into audit-policies

# Conflicts:
#	tests/Feature/DatabaseBackupCreationApiTest.php
This commit is contained in:
Andras Bacsai 2026-04-19 17:40:12 +02:00
commit 1f9dd5326f
5 changed files with 132 additions and 5 deletions

View file

@ -48,7 +48,7 @@ public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $
); );
$commands = [ $commands = [
'docker container prune -f --filter "label=coolify.managed=true" --filter "label!=coolify.proxy=true"', 'docker container prune -f --filter "label=coolify.managed=true" --filter "label!=coolify.proxy=true" --filter "label!=coolify.type=database" --filter "label!=coolify.type=application" --filter "label!=coolify.type=service"',
$imagePruneCmd, $imagePruneCmd,
'docker builder prune -af', 'docker builder prune -af',
"docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f", "docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f",

View file

@ -747,7 +747,7 @@ public function create_backup(Request $request)
} }
if ($request->filled('s3_storage_uuid')) { if ($request->filled('s3_storage_uuid')) {
$existsInTeam = S3Storage::ownedByCurrentTeam()->where('uuid', $request->s3_storage_uuid)->exists(); $existsInTeam = S3Storage::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->s3_storage_uuid)->exists();
if (! $existsInTeam) { if (! $existsInTeam) {
return response()->json([ return response()->json([
'message' => 'Validation failed.', 'message' => 'Validation failed.',
@ -774,7 +774,7 @@ public function create_backup(Request $request)
// Convert s3_storage_uuid to s3_storage_id // Convert s3_storage_uuid to s3_storage_id
if (isset($backupData['s3_storage_uuid'])) { if (isset($backupData['s3_storage_uuid'])) {
$s3Storage = S3Storage::ownedByCurrentTeam()->where('uuid', $backupData['s3_storage_uuid'])->first(); $s3Storage = S3Storage::ownedByCurrentTeamAPI($teamId)->where('uuid', $backupData['s3_storage_uuid'])->first();
if ($s3Storage) { if ($s3Storage) {
$backupData['s3_storage_id'] = $s3Storage->id; $backupData['s3_storage_id'] = $s3Storage->id;
} elseif ($request->boolean('save_s3')) { } elseif ($request->boolean('save_s3')) {
@ -982,7 +982,7 @@ public function update_backup(Request $request)
], 422); ], 422);
} }
if ($request->filled('s3_storage_uuid')) { if ($request->filled('s3_storage_uuid')) {
$existsInTeam = S3Storage::ownedByCurrentTeam()->where('uuid', $request->s3_storage_uuid)->exists(); $existsInTeam = S3Storage::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->s3_storage_uuid)->exists();
if (! $existsInTeam) { if (! $existsInTeam) {
return response()->json([ return response()->json([
'message' => 'Validation failed.', 'message' => 'Validation failed.',
@ -1015,7 +1015,7 @@ public function update_backup(Request $request)
// Convert s3_storage_uuid to s3_storage_id // Convert s3_storage_uuid to s3_storage_id
if (isset($backupData['s3_storage_uuid'])) { if (isset($backupData['s3_storage_uuid'])) {
$s3Storage = S3Storage::ownedByCurrentTeam()->where('uuid', $backupData['s3_storage_uuid'])->first(); $s3Storage = S3Storage::ownedByCurrentTeamAPI($teamId)->where('uuid', $backupData['s3_storage_uuid'])->first();
if ($s3Storage) { if ($s3Storage) {
$backupData['s3_storage_id'] = $s3Storage->id; $backupData['s3_storage_id'] = $s3Storage->id;
} elseif ($request->boolean('save_s3')) { } elseif ($request->boolean('save_s3')) {

View file

@ -66,6 +66,13 @@ public static function ownedByCurrentTeam(array $select = ['*'])
return S3Storage::whereTeamId(currentTeam()->id)->select($selectArray->all())->orderBy('name'); return S3Storage::whereTeamId(currentTeam()->id)->select($selectArray->all())->orderBy('name');
} }
public static function ownedByCurrentTeamAPI(int $teamId, array $select = ['*'])
{
$selectArray = collect($select)->concat(['id']);
return S3Storage::whereTeamId($teamId)->select($selectArray->all())->orderBy('name');
}
public function isUsable() public function isUsable()
{ {
return $this->is_usable; return $this->is_usable;

View file

@ -2,6 +2,8 @@
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Project; use App\Models\Project;
use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
@ -53,6 +55,17 @@
'destination_id' => $this->destination->id, 'destination_id' => $this->destination->id,
'destination_type' => $this->destination->getMorphClass(), 'destination_type' => $this->destination->getMorphClass(),
]); ]);
$this->s3Storage = S3Storage::create([
'name' => 'test-s3',
'region' => 'us-east-1',
'key' => 'test-key',
'secret' => 'test-secret',
'bucket' => 'test-bucket',
'endpoint' => 'https://s3.example.com',
'team_id' => $this->team->id,
'is_usable' => true,
]);
}); });
function backupHeaders(): array function backupHeaders(): array
@ -164,4 +177,101 @@ function backupHeaders(): array
$response->assertStatus(404); $response->assertStatus(404);
$response->assertJson(['message' => 'Database not found.']); $response->assertJson(['message' => 'Database not found.']);
}); });
test('creates backup with s3 storage via API token', function () {
$response = $this->withHeaders(backupHeaders())
->postJson("/api/v1/databases/{$this->database->uuid}/backups", [
'frequency' => '0 2 * * 0',
'save_s3' => true,
's3_storage_uuid' => $this->s3Storage->uuid,
'enabled' => true,
]);
$response->assertStatus(201);
$response->assertJsonStructure(['uuid', 'message']);
$backup = ScheduledDatabaseBackup::where('uuid', $response->json('uuid'))->first();
expect($backup)->not->toBeNull();
expect($backup->s3_storage_id)->toBe($this->s3Storage->id);
expect($backup->save_s3)->toBeTrue();
expect($backup->team_id)->toBe($this->team->id);
});
test('rejects s3_storage_uuid from another team', function () {
$otherTeam = Team::factory()->create();
$otherS3 = S3Storage::create([
'name' => 'other-s3',
'region' => 'us-east-1',
'key' => 'other-key',
'secret' => 'other-secret',
'bucket' => 'other-bucket',
'endpoint' => 'https://s3.example.com',
'team_id' => $otherTeam->id,
'is_usable' => true,
]);
$response = $this->withHeaders(backupHeaders())
->postJson("/api/v1/databases/{$this->database->uuid}/backups", [
'frequency' => '0 2 * * 0',
'save_s3' => true,
's3_storage_uuid' => $otherS3->uuid,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors(['s3_storage_uuid']);
});
});
describe('PATCH /api/v1/databases/{uuid}/backups/{scheduled_backup_uuid}', function () {
test('updates backup to use s3 storage via API token', function () {
$backup = ScheduledDatabaseBackup::create([
'frequency' => 'daily',
'enabled' => true,
'database_id' => $this->database->id,
'database_type' => $this->database->getMorphClass(),
'team_id' => $this->team->id,
]);
$response = $this->withHeaders(backupHeaders())
->patchJson("/api/v1/databases/{$this->database->uuid}/backups/{$backup->uuid}", [
'save_s3' => true,
's3_storage_uuid' => $this->s3Storage->uuid,
]);
$response->assertStatus(200);
$backup->refresh();
expect($backup->s3_storage_id)->toBe($this->s3Storage->id);
expect($backup->save_s3)->toBeTrue();
});
test('rejects s3_storage_uuid from another team on update', function () {
$otherTeam = Team::factory()->create();
$otherS3 = S3Storage::create([
'name' => 'other-s3',
'region' => 'us-east-1',
'key' => 'other-key',
'secret' => 'other-secret',
'bucket' => 'other-bucket',
'endpoint' => 'https://s3.example.com',
'team_id' => $otherTeam->id,
'is_usable' => true,
]);
$backup = ScheduledDatabaseBackup::create([
'frequency' => 'daily',
'enabled' => true,
'database_id' => $this->database->id,
'database_type' => $this->database->getMorphClass(),
'team_id' => $this->team->id,
]);
$response = $this->withHeaders(backupHeaders())
->patchJson("/api/v1/databases/{$this->database->uuid}/backups/{$backup->uuid}", [
'save_s3' => true,
's3_storage_uuid' => $otherS3->uuid,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors(['s3_storage_uuid']);
});
}); });

View file

@ -437,6 +437,16 @@
expect($buildImagesToDelete->pluck('tag')->toArray())->toContain('commit1-build'); expect($buildImagesToDelete->pluck('tag')->toArray())->toContain('commit1-build');
}); });
it('container prune excludes persistent resource types', function () {
$sourceFile = file_get_contents(__DIR__.'/../../../../app/Actions/Server/CleanupDocker.php');
expect($sourceFile)->toContain('label!=coolify.type=database');
expect($sourceFile)->toContain('label!=coolify.type=application');
expect($sourceFile)->toContain('label!=coolify.type=service');
expect($sourceFile)->toContain('label!=coolify.proxy=true');
expect($sourceFile)->toContain('label=coolify.managed=true');
});
it('preserves build image for currently running tag', function () { it('preserves build image for currently running tag', function () {
$images = collect([ $images = collect([
['repository' => 'app-uuid', 'tag' => 'commit1', 'created_at' => '2024-01-01 10:00:00', 'image_ref' => 'app-uuid:commit1'], ['repository' => 'app-uuid', 'tag' => 'commit1', 'created_at' => '2024-01-01 10:00:00', 'image_ref' => 'app-uuid:commit1'],