mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
Deleting a server with "Delete all resources" (UI) or ?force=true (API) deleted every application that used the server, also applications that use it only as an additional destination, for example after Promote. Server delete now deletes only resources whose main destination is on the server. Applications that use it only as an additional destination lose their additional_destinations row for the server, and their containers on it are stopped (best effort). The check for resources and the transfer archive path use the same Server::primaryResources(). The delete confirmation lists the resources to delete and the applications that keep running on another server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
143 lines
6.6 KiB
PHP
143 lines
6.6 KiB
PHP
<?php
|
|
|
|
use App\Actions\Application\StopApplicationOneServer;
|
|
use App\Jobs\DeleteResourceJob;
|
|
use App\Livewire\Project\Shared\Destination;
|
|
use App\Livewire\Server\Delete;
|
|
use App\Models\Application;
|
|
use App\Models\Environment;
|
|
use App\Models\InstanceSettings;
|
|
use App\Models\PrivateKey;
|
|
use App\Models\Project;
|
|
use App\Models\Server;
|
|
use App\Models\StandaloneDocker;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
Server::flushIdentityMap();
|
|
Queue::fake();
|
|
config(['app.maintenance.store' => 'array']);
|
|
InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true]);
|
|
|
|
$this->team = Team::factory()->create();
|
|
$this->user = User::factory()->create();
|
|
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
$privateKey = PrivateKey::factory()->create(['team_id' => $this->team->id]);
|
|
$this->serverToDelete = Server::factory()->create(['team_id' => $this->team->id, 'private_key_id' => $privateKey->id, 'name' => 'worker-1', 'ip' => '203.0.113.51']);
|
|
$this->destinationToDelete = StandaloneDocker::where('server_id', $this->serverToDelete->id)->firstOrFail();
|
|
$this->otherServer = Server::factory()->create(['team_id' => $this->team->id, 'private_key_id' => $privateKey->id, 'name' => 'worker-2', 'ip' => '203.0.113.52']);
|
|
$this->otherDestination = StandaloneDocker::where('server_id', $this->otherServer->id)->firstOrFail();
|
|
|
|
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
|
$this->environment = Environment::factory()->create(['project_id' => $project->id]);
|
|
|
|
$this->makeApplication = fn (StandaloneDocker $destination, string $name): Application => Application::factory()->create([
|
|
'name' => $name,
|
|
'environment_id' => $this->environment->id,
|
|
'destination_id' => $destination->id,
|
|
'destination_type' => $destination->getMorphClass(),
|
|
]);
|
|
});
|
|
|
|
afterEach(function () {
|
|
Server::flushIdentityMap();
|
|
});
|
|
|
|
function deleteServer(object $test, string $path, bool $force): void
|
|
{
|
|
if ($path === 'api') {
|
|
$token = $test->user->createToken('server-delete', ['write', 'deploy', 'delete'])->plainTextToken;
|
|
$test->withHeaders(['Authorization' => 'Bearer '.$token])
|
|
->deleteJson("/api/v1/servers/{$test->serverToDelete->uuid}".($force ? '?force=true' : ''))
|
|
->assertOk();
|
|
|
|
return;
|
|
}
|
|
|
|
$test->actingAs($test->user);
|
|
Livewire::test(Delete::class, ['server_uuid' => $test->serverToDelete->uuid])
|
|
->call('delete', 'password', $force ? ['force_delete_resources'] : [])
|
|
->assertHasNoErrors()
|
|
->assertNotDispatched('error');
|
|
}
|
|
|
|
function additionalDestinationsOf(Application $application, Server $server): int
|
|
{
|
|
return DB::table('additional_destinations')
|
|
->where('application_id', $application->id)
|
|
->where('server_id', $server->id)
|
|
->count();
|
|
}
|
|
|
|
test('deleting a server with force deletes its own applications and only detaches applications of other servers', function (string $path) {
|
|
$additional = ($this->makeApplication)($this->otherDestination, 'ref-admin');
|
|
$additional->additional_networks()->attach($this->destinationToDelete->id, ['server_id' => $this->serverToDelete->id]);
|
|
$primary = ($this->makeApplication)($this->destinationToDelete, 'own-app');
|
|
|
|
StopApplicationOneServer::shouldRun()->once()
|
|
->withArgs(fn (Application $application, Server $server) => $application->is($additional) && $server->is($this->serverToDelete));
|
|
|
|
deleteServer($this, $path, force: true);
|
|
|
|
expect(Server::withTrashed()->find($this->serverToDelete->id)->trashed())->toBeTrue()
|
|
->and(Application::find($additional->id))->not->toBeNull()
|
|
->and(additionalDestinationsOf($additional, $this->serverToDelete))->toBe(0);
|
|
|
|
Queue::assertPushed(DeleteResourceJob::class, 1);
|
|
Queue::assertPushed(DeleteResourceJob::class, fn (DeleteResourceJob $job) => $job->resource->is($primary));
|
|
})->with(['api', 'ui']);
|
|
|
|
test('a server with only additional-destination applications is deleted without force and detaches them', function (string $path) {
|
|
$additional = ($this->makeApplication)($this->otherDestination, 'ref-admin');
|
|
$additional->additional_networks()->attach($this->destinationToDelete->id, ['server_id' => $this->serverToDelete->id]);
|
|
StopApplicationOneServer::shouldRun()->once();
|
|
|
|
deleteServer($this, $path, force: false);
|
|
|
|
expect(Server::withTrashed()->find($this->serverToDelete->id)->trashed())->toBeTrue()
|
|
->and(Application::find($additional->id))->not->toBeNull()
|
|
->and(additionalDestinationsOf($additional, $this->serverToDelete))->toBe(0);
|
|
Queue::assertNotPushed(DeleteResourceJob::class);
|
|
})->with(['api', 'ui']);
|
|
|
|
test('an application promoted to another server survives the deletion of its old server', function () {
|
|
$application = ($this->makeApplication)($this->destinationToDelete, 'ref-admin');
|
|
$application->additional_networks()->attach($this->otherDestination->id, ['server_id' => $this->otherServer->id]);
|
|
StopApplicationOneServer::shouldRun()->once();
|
|
|
|
$this->actingAs($this->user);
|
|
Livewire::test(Destination::class, ['resource' => $application])
|
|
->call('promote', $this->otherDestination->id, $this->otherServer->id);
|
|
expect(additionalDestinationsOf($application, $this->serverToDelete))->toBe(1);
|
|
|
|
deleteServer($this, 'ui', force: true);
|
|
|
|
$application = Application::find($application->id);
|
|
expect($application)->not->toBeNull()
|
|
->and($application->destination_id)->toBe($this->otherDestination->id)
|
|
->and(additionalDestinationsOf($application, $this->serverToDelete))->toBe(0);
|
|
Queue::assertNotPushed(DeleteResourceJob::class);
|
|
});
|
|
|
|
test('the delete confirmation lists the resources to delete and the applications to detach', function () {
|
|
$additional = ($this->makeApplication)($this->otherDestination, 'ref-admin');
|
|
$additional->additional_networks()->attach($this->destinationToDelete->id, ['server_id' => $this->serverToDelete->id]);
|
|
($this->makeApplication)($this->destinationToDelete, 'own-app');
|
|
|
|
$this->actingAs($this->user);
|
|
$component = Livewire::test(Delete::class, ['server_uuid' => $this->serverToDelete->uuid]);
|
|
|
|
expect($component->viewData('checkboxes')[0]['label'])->toBe('Delete all resources (1 total): own-app')
|
|
->and($component->viewData('detachedApplications')->pluck('name')->all())->toBe(['ref-admin']);
|
|
$component->assertSee('ref-admin will keep running on worker-2');
|
|
});
|