mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(server): detach additional-destination apps instead of deleting them on server delete
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b7defddc32
commit
83a5667387
@@ -35,6 +35,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
- Keep the dashboard proxy configuration of a server with the IP `host.docker.internal` that is not the default localhost server. Before, Coolify did not see this server as localhost after it saved the server, for example after a proxy restart, and deleted `coolify.yaml`, so the dashboard was not reachable at its domain ([#12444](https://github.com/coollabsio/coolify/issues/12444))
|
||||
- Show the disk usage of a server on the **Metrics** tab, on the **Fleet** page, and in the MCP `get_server_metrics` tool when Sentinel reports the Coolify data disk under a different mount point than `/`, `/data`, or `/data/coolify`. For example, this occurs on a NAS where `/data` is a symlink into a data volume. Before, the disk usage was always empty on these servers ([#12448](https://github.com/coollabsio/coolify/issues/12448))
|
||||
- Pin the Usesend service template to `usesend/usesend:v1.10.0`, which has the Next.js, Node.js 24, and nodemailer security updates. Before, the template used `latest`, so a server kept an old cached image until you pulled the images again. The template does not use MinIO, so the MinIO-to-RustFS change of v1.10.0 does not affect it ([#12443](https://github.com/coollabsio/coolify/issues/12443))
|
||||
- Keep an application that uses a server only as an additional destination when you delete that server with **Delete all resources** or with `DELETE /api/v1/servers/{uuid}?force=true`. Before, Coolify deleted the application on all its servers, also after **Promote** made another server its main server. Coolify now removes the application only from the deleted server and stops its containers there. A server that has only such applications can be deleted without **Delete all resources**. The delete confirmation lists the resources that it deletes and the applications that keep running on another server
|
||||
|
||||
## [v4.6.0] - 2026-11-02
|
||||
|
||||
|
||||
@@ -946,7 +946,7 @@ class ServersController extends Controller
|
||||
new OA\Parameter(
|
||||
name: 'force',
|
||||
in: 'query',
|
||||
description: 'Also delete all resources on the server. Needs the deploy permission.',
|
||||
description: 'Also delete all resources whose main destination is on the server. Applications that use the server only as an additional destination are not deleted. They are removed from the server and keep running on their other servers. Needs the deploy permission.',
|
||||
required: false,
|
||||
schema: new OA\Schema(type: 'boolean', default: false)
|
||||
),
|
||||
@@ -1026,7 +1026,8 @@ class ServersController extends Controller
|
||||
return response()->json(['message' => 'Server deleted.']);
|
||||
}
|
||||
|
||||
if ($server->definedResources()->count() > 0 && ! $force) {
|
||||
$resources = $server->primaryResources();
|
||||
if ($resources->isNotEmpty() && ! $force) {
|
||||
return response()->json(['message' => 'Server has resources. Use ?force=true to delete all resources and the server, or delete resources manually first.'], 400);
|
||||
}
|
||||
if ($server->is_coolify_host) {
|
||||
@@ -1053,8 +1054,10 @@ class ServersController extends Controller
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Applications that use the server only as an additional destination keep running on their other servers.
|
||||
$server->detachAdditionalApplications();
|
||||
if ($force) {
|
||||
foreach ($server->definedResources() as $resource) {
|
||||
foreach ($resources as $resource) {
|
||||
DeleteResourceJob::dispatch($resource);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,20 +64,19 @@ class Delete extends Component
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->server->hasDefinedResources() && ! $this->force_delete_resources) {
|
||||
$resources = $this->server->primaryResources();
|
||||
if ($resources->isNotEmpty() && ! $this->force_delete_resources) {
|
||||
$this->dispatch('error', 'Server has defined resources. Please delete them first or select "Delete all resources".');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$resources = $this->server->definedResources();
|
||||
if ($isArchive) {
|
||||
$claimer = app(ServerTransferClaimer::class);
|
||||
$claimer->forgetDnsRecords($this->server);
|
||||
// Applications that use the server only as an additional destination stay; they lose this server.
|
||||
$claimer->detachAdditionalApplications($this->server);
|
||||
$resources = $claimer->archiveResources($this->server);
|
||||
app(ServerTransferClaimer::class)->forgetDnsRecords($this->server);
|
||||
}
|
||||
// Applications that use the server only as an additional destination stay; they lose this server.
|
||||
// Another instance manages a transferred server, so the containers stay as they are there.
|
||||
$this->server->detachAdditionalApplications(stopContainers: ! $isArchive);
|
||||
if ($this->force_delete_resources) {
|
||||
foreach ($resources as $resource) {
|
||||
DeleteResourceJob::dispatch($resource, deleteFromCoolifyOnly: $isArchive);
|
||||
@@ -109,11 +108,13 @@ class Delete extends Component
|
||||
{
|
||||
$checkboxes = [];
|
||||
|
||||
if ($this->server->hasDefinedResources() && ! $this->server->isTransferredAway()) {
|
||||
$resourceCount = $this->server->definedResources()->count();
|
||||
$resourcesToDelete = $this->server->primaryResources();
|
||||
$detachedApplications = $this->server->additionalApplications();
|
||||
|
||||
if ($resourcesToDelete->isNotEmpty() && ! $this->server->isTransferredAway()) {
|
||||
$checkboxes[] = [
|
||||
'id' => 'force_delete_resources',
|
||||
'label' => "Delete all resources ({$resourceCount} total)",
|
||||
'label' => "Delete all resources ({$resourcesToDelete->count()} total): ".$resourcesToDelete->pluck('name')->implode(', '),
|
||||
'default_warning' => 'Server cannot be deleted while it has resources.',
|
||||
];
|
||||
}
|
||||
@@ -154,6 +155,8 @@ class Delete extends Component
|
||||
|
||||
return view('livewire.server.delete', [
|
||||
'checkboxes' => $checkboxes,
|
||||
'resourcesToDelete' => $resourcesToDelete,
|
||||
'detachedApplications' => $detachedApplications,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
+87
-26
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Actions\Application\StopApplicationOneServer;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Actions\Server\InstallDocker;
|
||||
use App\Actions\Server\InstallPrerequisites;
|
||||
@@ -32,6 +33,7 @@ use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Http\Exceptions\ThrottleRequestsException;
|
||||
@@ -1751,6 +1753,47 @@ $siteAddress {
|
||||
return $applications->concat($databases)->concat($services->get());
|
||||
}
|
||||
|
||||
/**
|
||||
* The resources that deleting this server with "Delete all resources" deletes. An application that has this server
|
||||
* only as an additional destination runs on another server, so it is detached instead.
|
||||
*
|
||||
* @return Collection<int, Model>
|
||||
*/
|
||||
public function primaryResources(): Collection
|
||||
{
|
||||
return $this->primaryApplications()
|
||||
->concat($this->databases())
|
||||
->concat($this->services()->get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this server from the applications that use it only as an additional destination.
|
||||
* The applications keep running on their other servers.
|
||||
*
|
||||
* @param bool $stopContainers stop and remove the containers of the applications on this server (best effort)
|
||||
*/
|
||||
public function detachAdditionalApplications(bool $stopContainers = true): void
|
||||
{
|
||||
$applications = $this->additionalApplications();
|
||||
if ($stopContainers) {
|
||||
foreach ($applications as $application) {
|
||||
try {
|
||||
StopApplicationOneServer::run($application, $this);
|
||||
} catch (\Throwable $e) {
|
||||
logger()->warning('Could not stop the containers of an application on a deleted server.', [
|
||||
'application_id' => $application->id,
|
||||
'server_id' => $this->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
DB::table('additional_destinations')
|
||||
->where('server_id', $this->id)
|
||||
->whereIn('application_id', $applications->pluck('id'))
|
||||
->delete();
|
||||
}
|
||||
|
||||
public function stopUnmanaged($id)
|
||||
{
|
||||
return instant_remote_process([dockerStopCommand(0, escapeshellarg($id), $this)], $this);
|
||||
@@ -1841,18 +1884,6 @@ $siteAddress {
|
||||
}
|
||||
}
|
||||
|
||||
public function hasDefinedResources()
|
||||
{
|
||||
$applications = $this->applications()->count() > 0;
|
||||
$databases = $this->databases()->count() > 0;
|
||||
$services = $this->services()->count() > 0;
|
||||
if ($applications || $databases || $services) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function databases()
|
||||
{
|
||||
// Get destination IDs for this server in two efficient queries
|
||||
@@ -1892,22 +1923,13 @@ $siteAddress {
|
||||
->reject(fn ($item) => $item instanceof StandalonePostgresql && $item->isInstanceDatabase());
|
||||
}
|
||||
|
||||
/**
|
||||
* The applications whose primary destination is on this server, plus the applications that use this server as an
|
||||
* additional destination.
|
||||
*/
|
||||
public function applications()
|
||||
{
|
||||
// Get destination IDs for this server in two efficient queries
|
||||
$standaloneDockerIds = StandaloneDocker::where('server_id', $this->id)->pluck('id');
|
||||
$swarmDockerIds = SwarmDocker::where('server_id', $this->id)->pluck('id');
|
||||
|
||||
// Query all applications in a single query using polymorphic conditions
|
||||
$applications = Application::where(function ($query) use ($standaloneDockerIds, $swarmDockerIds) {
|
||||
$query->where(function ($q) use ($standaloneDockerIds) {
|
||||
$q->where('destination_type', StandaloneDocker::class)
|
||||
->whereIn('destination_id', $standaloneDockerIds);
|
||||
})->orWhere(function ($q) use ($swarmDockerIds) {
|
||||
$q->where('destination_type', SwarmDocker::class)
|
||||
->whereIn('destination_id', $swarmDockerIds);
|
||||
});
|
||||
})->get();
|
||||
$applications = $this->primaryApplications();
|
||||
|
||||
// Get additional server applications
|
||||
$additionalApplicationIds = DB::table('additional_destinations')
|
||||
@@ -1922,6 +1944,45 @@ $siteAddress {
|
||||
return $applications;
|
||||
}
|
||||
|
||||
/**
|
||||
* The applications whose primary destination is on this server.
|
||||
*
|
||||
* @return Collection<int, Application>
|
||||
*/
|
||||
public function primaryApplications(): Collection
|
||||
{
|
||||
// Get destination IDs for this server in two efficient queries
|
||||
$standaloneDockerIds = StandaloneDocker::where('server_id', $this->id)->pluck('id');
|
||||
$swarmDockerIds = SwarmDocker::where('server_id', $this->id)->pluck('id');
|
||||
|
||||
// Query all applications in a single query using polymorphic conditions
|
||||
return Application::where(function ($query) use ($standaloneDockerIds, $swarmDockerIds) {
|
||||
$query->where(function ($q) use ($standaloneDockerIds) {
|
||||
$q->where('destination_type', StandaloneDocker::class)
|
||||
->whereIn('destination_id', $standaloneDockerIds);
|
||||
})->orWhere(function ($q) use ($swarmDockerIds) {
|
||||
$q->where('destination_type', SwarmDocker::class)
|
||||
->whereIn('destination_id', $swarmDockerIds);
|
||||
});
|
||||
})->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* The applications that use this server only as an additional destination. Their primary destination is on
|
||||
* another server.
|
||||
*
|
||||
* @return Collection<int, Application>
|
||||
*/
|
||||
public function additionalApplications(): Collection
|
||||
{
|
||||
$applicationIds = DB::table('additional_destinations')
|
||||
->where('server_id', $this->id)
|
||||
->whereNotIn('application_id', $this->primaryApplications()->pluck('id'))
|
||||
->pluck('application_id');
|
||||
|
||||
return Application::query()->with('destination.server')->whereIn('id', $applicationIds)->get();
|
||||
}
|
||||
|
||||
public function dockerComposeBasedApplications()
|
||||
{
|
||||
return $this->applications()->filter(function ($application) {
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Services\ServerTransfer;
|
||||
|
||||
use App\Enums\GithubRunnerStatus;
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\GithubRunnerExecution;
|
||||
use App\Models\ManagedDnsRecord;
|
||||
use App\Models\Server;
|
||||
@@ -185,49 +184,14 @@ class ServerTransferClaimer
|
||||
}
|
||||
|
||||
$this->forgetDnsRecords($server);
|
||||
$this->detachAdditionalApplications($server);
|
||||
foreach ($this->archiveResources($server) as $resource) {
|
||||
// Another instance manages the server, so the containers of its additional applications stay as they are.
|
||||
$server->detachAdditionalApplications(stopContainers: false);
|
||||
foreach ($server->primaryResources() as $resource) {
|
||||
(new DeleteResourceJob($resource, deleteFromCoolifyOnly: true))->handle();
|
||||
}
|
||||
$server->forceDelete();
|
||||
}
|
||||
|
||||
/**
|
||||
* The resources that the archive of a transferred server deletes. An application that has the server only as an
|
||||
* additional destination runs on another server of this instance, so it is not one of them.
|
||||
*
|
||||
* @return Collection<int, Model>
|
||||
*/
|
||||
public function archiveResources(Server $server): Collection
|
||||
{
|
||||
return $this->primaryApplications($server)
|
||||
->concat($server->databases())
|
||||
->concat($server->services()->get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the server from applications that run on it only as an additional destination.
|
||||
*/
|
||||
public function detachAdditionalApplications(Server $server): void
|
||||
{
|
||||
$primaryIds = $this->primaryApplications($server)->pluck('id');
|
||||
DB::table('additional_destinations')
|
||||
->where('server_id', $server->id)
|
||||
->whereNotIn('application_id', $primaryIds)
|
||||
->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Application>
|
||||
*/
|
||||
private function primaryApplications(Server $server): Collection
|
||||
{
|
||||
return $server->applications()
|
||||
->filter(fn (Application $application) => (int) data_get($application, 'destination.server_id') === (int) $server->id)
|
||||
->unique('id')
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Model>
|
||||
*/
|
||||
@@ -235,7 +199,7 @@ class ServerTransferClaimer
|
||||
{
|
||||
// The server itself owns the DNS record of its Cloudflare tunnel.
|
||||
return collect([$server])
|
||||
->concat($this->primaryApplications($server))
|
||||
->concat($server->primaryApplications())
|
||||
->concat($server->services()->with('applications')->get()->flatMap->applications);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -19974,7 +19974,7 @@
|
||||
{
|
||||
"name": "force",
|
||||
"in": "query",
|
||||
"description": "Also delete all resources on the server. Needs the deploy permission.",
|
||||
"description": "Also delete all resources whose main destination is on the server. Applications that use the server only as an additional destination are not deleted. They are removed from the server and keep running on their other servers. Needs the deploy permission.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
|
||||
+1
-1
@@ -12620,7 +12620,7 @@ paths:
|
||||
-
|
||||
name: force
|
||||
in: query
|
||||
description: 'Also delete all resources on the server. Needs the deploy permission.'
|
||||
description: 'Also delete all resources whose main destination is on the server. Applications that use the server only as an additional destination are not deleted. They are removed from the server and keep running on their other servers. Needs the deploy permission.'
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
|
||||
@@ -21,22 +21,47 @@
|
||||
This server was transferred to another Coolify instance. Only the records on this instance
|
||||
are deleted, also the records of its resources. The server, its containers, and its data
|
||||
stay as they are.
|
||||
@elseif ($server->definedResources()->count() > 0)
|
||||
@elseif ($resourcesToDelete->isNotEmpty())
|
||||
It currently contains managed resources. Enable force deletion in the confirmation only
|
||||
if those resources should also be removed.
|
||||
@endif
|
||||
</p>
|
||||
@if (! $server->isTransferredAway() && $resourcesToDelete->isNotEmpty())
|
||||
<div>
|
||||
<p>Deleted with "Delete all resources":</p>
|
||||
<ul class="list-disc pl-5">
|
||||
@foreach ($resourcesToDelete as $resource)
|
||||
<li>{{ $resource->name }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
@if ($detachedApplications->isNotEmpty())
|
||||
<div>
|
||||
<p>Removed only from this server (not deleted):</p>
|
||||
<ul class="list-disc pl-5">
|
||||
@foreach ($detachedApplications as $application)
|
||||
<li>{{ $application->name }} will keep running on {{ data_get($application, 'destination.server.name') }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
@if ($server->hostinger_virtual_machine_id && ! $server->isTransferredAway())
|
||||
<p>
|
||||
Hostinger deletes the VPS only at the end of its paid period, and only if you select this in the confirmation dialog.
|
||||
<a class="underline" href="https://hpanel.hostinger.com/" target="_blank"
|
||||
rel="noopener noreferrer">Manage the VPS and billing in hPanel.</a>
|
||||
</p>
|
||||
@endif
|
||||
</p>
|
||||
<p>Type the server name in the confirmation dialog to continue.</p>
|
||||
<x-slot:action>
|
||||
@php
|
||||
$deleteActions = $server->isTransferredAway()
|
||||
? ['This server and the records of its resources will be permanently deleted from this Coolify instance. Nothing on the server changes.']
|
||||
: ['This server will be permanently deleted from Coolify.'];
|
||||
foreach ($detachedApplications as $application) {
|
||||
$deleteActions[] = "Application {$application->name} will be removed from this server and will keep running on ".data_get($application, 'destination.server.name').'.';
|
||||
}
|
||||
$deleteWarningMessage = null;
|
||||
if ($server->isTransferredAway()) {
|
||||
$deleteWarningMessage = '<strong>This removes the option to undo the transfer.</strong><br>'
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<?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');
|
||||
});
|
||||
Reference in New Issue
Block a user