mirror of
https://github.com/coollabsio/coolify.git
synced 2026-09-28 02:06:37 -04:00
fix(service): support metadata-only deletion on unreachable servers
Add a Coolify-only deletion path, improve Docker cleanup handling, and notify teams when service deletion fails.
This commit is contained in:
@@ -127,7 +127,6 @@ Thank you so much!
|
||||
<a href="https://www.movavi.com/mac.html?utm_source=coolify.io"><img width="60px" alt="Movavi" src="https://cdn.coollabs.io/sponsors/movavi.png"/></a>
|
||||
<a href="https://usefoil.com/"><img width="60px" alt="ABXY" src="https://usefoil.com/favicon.svg"/></a>
|
||||
<a href="https://www.launchfa.st/?utm_source=coolify.io"><img width="60px" alt="LaunchFast Boilerplates" src="https://github.com/LaunchFast-Boilerplates.png"/></a>
|
||||
<a href="https://vanaways.co.uk/?utm_source=coolify.io"><img width="60px" alt="Vanaways" src="https://github.com/Vanaways.png"/></a>
|
||||
<a href="https://www.netrouting.com/?utm_source=coolify.io"><img width="60px" alt="Netrouting" src="https://github.com/netroutingcom.png"/></a>
|
||||
<a href="https://github.com/mindedtech"><img width="60px" alt="MindEd Tech" src="https://github.com/mindedtech.png"/></a>
|
||||
<a href="https://youstable.com/?utm_source=coolify.io"><img width="60px" alt="YouStable" src="https://github.com/youstable.png"/></a>
|
||||
|
||||
@@ -3,13 +3,22 @@
|
||||
namespace App\Actions\Service;
|
||||
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use RuntimeException;
|
||||
|
||||
class DeleteService
|
||||
{
|
||||
public function cleanupRemote(Service $service, bool $deleteVolumes, bool $deleteConnectedNetworks, bool $deleteConfigurations): void
|
||||
{
|
||||
$server = data_get($service, 'server');
|
||||
if ($deleteVolumes && $server->isFunctional()) {
|
||||
if (! $server?->isFunctional()) {
|
||||
throw new RuntimeException('Server is not functional.');
|
||||
}
|
||||
|
||||
$this->removeContainers($service);
|
||||
|
||||
if ($deleteVolumes) {
|
||||
$commands = [];
|
||||
foreach ($service->applications()->get() as $application) {
|
||||
foreach ($application->persistentStorages()->get() as $storage) {
|
||||
@@ -22,7 +31,7 @@ class DeleteService
|
||||
}
|
||||
}
|
||||
foreach ($commands as $command) {
|
||||
instant_remote_process([$command], $server, false);
|
||||
instant_remote_process([$command], $server);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +41,28 @@ class DeleteService
|
||||
if ($deleteConfigurations) {
|
||||
$service->deleteConfigurations();
|
||||
}
|
||||
instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false);
|
||||
}
|
||||
|
||||
public function removeSubresourceContainer(ServiceApplication|ServiceDatabase $resource): void
|
||||
{
|
||||
$service = $resource->service;
|
||||
$server = $service?->server;
|
||||
if (! $server?->isFunctional()) {
|
||||
throw new RuntimeException('Server is not functional.');
|
||||
}
|
||||
|
||||
$this->removeContainers($service, $resource->id);
|
||||
}
|
||||
|
||||
private function removeContainers(Service $service, ?int $subresourceId = null): void
|
||||
{
|
||||
$filters = "--filter 'label=coolify.serviceId={$service->id}'";
|
||||
if ($subresourceId !== null) {
|
||||
$filters .= " --filter 'label=coolify.service.subId={$subresourceId}'";
|
||||
}
|
||||
|
||||
$command = "container_ids=\$(docker ps -aq {$filters}); [ -z \"\$container_ids\" ] || docker rm -f \$container_ids";
|
||||
instant_remote_process([$command], $service->server);
|
||||
}
|
||||
|
||||
public function deleteLocal(Service $service): void
|
||||
|
||||
@@ -972,6 +972,7 @@ class ServicesController extends Controller
|
||||
new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)),
|
||||
new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)),
|
||||
new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)),
|
||||
new OA\Parameter(name: 'delete_from_coolify_only', in: 'query', required: false, description: 'Remove only Coolify metadata without deleting Docker resources.', schema: new OA\Schema(type: 'boolean', default: false)),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
@@ -1021,22 +1022,28 @@ class ServicesController extends Controller
|
||||
|
||||
$service->delete();
|
||||
|
||||
$deleteFromCoolifyOnly = $request->boolean('delete_from_coolify_only') || ! $service->server?->isFunctional();
|
||||
|
||||
DeleteResourceJob::dispatch(
|
||||
resource: $service,
|
||||
deleteVolumes: $request->boolean('delete_volumes', true),
|
||||
deleteConnectedNetworks: $request->boolean('delete_connected_networks', true),
|
||||
deleteConfigurations: $request->boolean('delete_configurations', true),
|
||||
dockerCleanup: $request->boolean('docker_cleanup', true)
|
||||
dockerCleanup: $request->boolean('docker_cleanup', true),
|
||||
deleteFromCoolifyOnly: $deleteFromCoolifyOnly,
|
||||
);
|
||||
|
||||
auditLog('api.service.deleted', [
|
||||
'team_id' => $teamId,
|
||||
'service_uuid' => $service->uuid,
|
||||
'service_name' => $service->name,
|
||||
'delete_from_coolify_only' => $deleteFromCoolifyOnly,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Service deletion request queued.',
|
||||
'message' => $deleteFromCoolifyOnly
|
||||
? 'Server is not reachable. The service will be removed from Coolify only; Docker resources may remain.'
|
||||
: 'Service deletion request queued.',
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use App\Notifications\Internal\GeneralNotification;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
@@ -38,7 +39,8 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
public bool $deleteVolumes = true,
|
||||
public bool $deleteConnectedNetworks = true,
|
||||
public bool $deleteConfigurations = true,
|
||||
public bool $dockerCleanup = true
|
||||
public bool $dockerCleanup = true,
|
||||
public bool $deleteFromCoolifyOnly = false,
|
||||
) {
|
||||
$this->onQueue('high');
|
||||
}
|
||||
@@ -51,6 +53,12 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->deleteFromCoolifyOnly && $this->resource instanceof Service) {
|
||||
$this->deleteLocalResource();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($this->resource->type()) {
|
||||
case 'application':
|
||||
@@ -89,6 +97,19 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
if ($this->resource instanceof Service) {
|
||||
if ($this->resource->trashed()) {
|
||||
$this->resource->restore();
|
||||
}
|
||||
|
||||
$this->resource->server?->team?->notify(new GeneralNotification(
|
||||
"Service deletion failed for '{$this->resource->name}'. Docker resources may still exist on server '{$this->resource->server?->name}'. You can retry the cleanup or select 'Remove from Coolify only' in the deletion dialog. Error: {$e->getMessage()}",
|
||||
success: false,
|
||||
));
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
Log::warning('Remote cleanup failed while deleting resource; continuing with local deletion.', [
|
||||
'resource_id' => $this->resource->id,
|
||||
'resource_type' => $this->resource->type(),
|
||||
@@ -106,6 +127,12 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
]);
|
||||
}
|
||||
|
||||
$this->deleteLocalResource();
|
||||
|
||||
}
|
||||
|
||||
private function deleteLocalResource(): void
|
||||
{
|
||||
DB::transaction(function (): void {
|
||||
if ($this->resource instanceof Service) {
|
||||
app(DeleteService::class)->deleteLocal($this->resource);
|
||||
@@ -126,7 +153,6 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->resource->environment_variables()->delete();
|
||||
$this->resource->forceDelete();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private function isDatabase(): bool
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Actions\Service\DeleteService;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
@@ -254,6 +255,7 @@ class Index extends Component
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
app(DeleteService::class)->removeSubresourceContainer($this->serviceDatabase);
|
||||
$this->serviceDatabase->delete();
|
||||
$this->dispatch('success', 'Database deleted.');
|
||||
|
||||
@@ -501,6 +503,7 @@ class Index extends Component
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
app(DeleteService::class)->removeSubresourceContainer($this->serviceApplication);
|
||||
$this->serviceApplication->delete();
|
||||
$this->dispatch('success', 'Application deleted.');
|
||||
|
||||
|
||||
@@ -111,7 +111,33 @@ class Danger extends Component
|
||||
$this->delete_volumes,
|
||||
$this->delete_connected_networks,
|
||||
$this->delete_configurations,
|
||||
$this->docker_cleanup
|
||||
$this->docker_cleanup,
|
||||
)->afterResponse();
|
||||
|
||||
return redirectRoute($this, 'project.resource.index', [
|
||||
'project_uuid' => $this->projectUuid,
|
||||
'environment_uuid' => $this->environmentUuid,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteFromCoolifyOnly(string $password): mixed
|
||||
{
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
if (! $this->resource instanceof Service) {
|
||||
return 'Service not found.';
|
||||
}
|
||||
|
||||
try {
|
||||
$this->authorize('delete', $this->resource);
|
||||
DeleteResourceJob::dispatch(
|
||||
resource: $this->resource,
|
||||
deleteFromCoolifyOnly: true,
|
||||
)->afterResponse();
|
||||
|
||||
return redirectRoute($this, 'project.resource.index', [
|
||||
|
||||
@@ -15,7 +15,7 @@ class GeneralNotification extends Notification implements ShouldQueue
|
||||
|
||||
public $tries = 1;
|
||||
|
||||
public function __construct(public string $message)
|
||||
public function __construct(public string $message, public bool $success = true)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
@@ -28,9 +28,9 @@ class GeneralNotification extends Notification implements ShouldQueue
|
||||
public function toDiscord(): DiscordMessage
|
||||
{
|
||||
return new DiscordMessage(
|
||||
title: 'Coolify: General Notification',
|
||||
title: $this->success ? 'Coolify: General Notification' : 'Coolify: Action required',
|
||||
description: $this->message,
|
||||
color: DiscordMessage::infoColor(),
|
||||
color: $this->success ? DiscordMessage::infoColor() : DiscordMessage::errorColor(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ class GeneralNotification extends Notification implements ShouldQueue
|
||||
public function toPushover(): PushoverMessage
|
||||
{
|
||||
return new PushoverMessage(
|
||||
title: 'General Notification',
|
||||
level: 'info',
|
||||
title: $this->success ? 'General Notification' : 'Action required',
|
||||
level: $this->success ? 'info' : 'error',
|
||||
message: $this->message,
|
||||
);
|
||||
}
|
||||
@@ -53,16 +53,16 @@ class GeneralNotification extends Notification implements ShouldQueue
|
||||
public function toSlack(): SlackMessage
|
||||
{
|
||||
return new SlackMessage(
|
||||
title: 'Coolify: General Notification',
|
||||
title: $this->success ? 'Coolify: General Notification' : 'Coolify: Action required',
|
||||
description: $this->message,
|
||||
color: SlackMessage::infoColor(),
|
||||
color: $this->success ? SlackMessage::infoColor() : SlackMessage::errorColor(),
|
||||
);
|
||||
}
|
||||
|
||||
public function toWebhook(): array
|
||||
{
|
||||
return [
|
||||
'success' => true,
|
||||
'success' => $this->success,
|
||||
'message' => $this->message,
|
||||
'event' => 'general',
|
||||
'url' => base_url(),
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
default => 'resource',
|
||||
};
|
||||
@endphp
|
||||
@if ($resource instanceof \App\Models\Service && !$resource->server?->isFunctional())
|
||||
<x-callout type="warning" title="Server is not reachable" class="mb-4">
|
||||
Coolify cannot remove or verify Docker resources on this server. The deletion dialog will default to
|
||||
removing this service from Coolify only. Its containers, volumes, networks, and configuration files may
|
||||
remain on the server.
|
||||
</x-callout>
|
||||
@endif
|
||||
<x-application.settings-section id="danger-zone-section" title="Danger zone"
|
||||
helper="Destructive resource actions cannot be undone.">
|
||||
<x-danger-zone title="Delete {{ $resourceLabel }}">
|
||||
@@ -30,6 +37,19 @@
|
||||
confirmationText="{{ $resourceName }}"
|
||||
confirmationLabel="Enter the resource name to confirm permanent deletion"
|
||||
shortConfirmationLabel="Resource name" />
|
||||
@if ($resource instanceof \App\Models\Service)
|
||||
<x-modal-confirmation title="Remove service from Coolify only?"
|
||||
buttonTitle="Remove from Coolify only"
|
||||
isErrorButton submitAction="deleteFromCoolifyOnly"
|
||||
:actions="[
|
||||
'Permanently remove this service from Coolify.',
|
||||
'Leave all containers, volumes, networks, and configuration files on the server.',
|
||||
]"
|
||||
warningMessage="Coolify will no longer track or manage the Docker resources for this service. Use this only when normal cleanup cannot complete."
|
||||
confirmationText="{{ $resourceName }}"
|
||||
confirmationLabel="Enter the resource name to confirm metadata-only deletion"
|
||||
shortConfirmationLabel="Resource name" />
|
||||
@endif
|
||||
@else
|
||||
<x-forms.button isError disabled tooltip="You do not have permission to delete this resource.">
|
||||
Delete {{ $resourceLabel }}
|
||||
|
||||
@@ -312,13 +312,20 @@ describe('DELETE resource endpoints', function () {
|
||||
'server_id' => $this->server->id,
|
||||
]);
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
$response = $this->withHeaders($this->headers)
|
||||
->deleteJson("/api/v1/services/{$service->uuid}")
|
||||
->assertOk();
|
||||
|
||||
$response->assertJson([
|
||||
'message' => 'Server is not reachable. The service will be removed from Coolify only; Docker resources may remain.',
|
||||
]);
|
||||
|
||||
expect(Service::find($service->id))->toBeNull()
|
||||
->and(Service::withTrashed()->find($service->id)?->trashed())->toBeTrue();
|
||||
Queue::assertPushed(DeleteResourceJob::class);
|
||||
Queue::assertPushed(
|
||||
DeleteResourceJob::class,
|
||||
fn (DeleteResourceJob $job): bool => $job->deleteFromCoolifyOnly
|
||||
);
|
||||
});
|
||||
|
||||
test('soft deletes a database before queuing cleanup', function () {
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
<?php
|
||||
|
||||
use App\Actions\Service\DeleteService;
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\ScheduledVolumeBackup;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Notifications\Internal\GeneralNotification;
|
||||
use Illuminate\Foundation\Console\QueuedCommand;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
@@ -43,7 +49,7 @@ beforeEach(function () {
|
||||
Queue::fake();
|
||||
});
|
||||
|
||||
it('deletes the Coolify resource when remote cleanup fails', function () {
|
||||
it('deletes a non-service Coolify resource when remote cleanup fails', function () {
|
||||
Process::fake(['*' => Process::result(errorOutput: 'SSH connection timed out', exitCode: 255)]);
|
||||
|
||||
(new DeleteResourceJob($this->application))->handle();
|
||||
@@ -52,6 +58,130 @@ it('deletes the Coolify resource when remote cleanup fails', function () {
|
||||
Queue::assertNotPushed(QueuedCommand::class);
|
||||
});
|
||||
|
||||
it('keeps a service when remote cleanup fails', function () {
|
||||
Notification::fake();
|
||||
$service = Service::factory()->create([
|
||||
'environment_id' => $this->application->environment_id,
|
||||
'server_id' => $this->application->destination->server_id,
|
||||
'destination_id' => $this->application->destination_id,
|
||||
'destination_type' => $this->application->destination_type,
|
||||
]);
|
||||
ServiceApplication::create([
|
||||
'service_id' => $service->id,
|
||||
'name' => 'web',
|
||||
'image' => 'nginx:alpine',
|
||||
]);
|
||||
$service->server->team->webhookNotificationSettings()->update([
|
||||
'webhook_enabled' => true,
|
||||
'webhook_url' => 'https://example.com/webhook',
|
||||
]);
|
||||
$service->delete();
|
||||
Process::fake();
|
||||
|
||||
expect(fn () => (new DeleteResourceJob($service))->handle())
|
||||
->toThrow(RuntimeException::class, 'Server is not functional.');
|
||||
|
||||
expect(Service::find($service->id))->not->toBeNull()
|
||||
->and(ServiceApplication::where('service_id', $service->id)->exists())->toBeTrue();
|
||||
Notification::assertCount(1);
|
||||
Notification::assertSentTo(
|
||||
$service->team(),
|
||||
GeneralNotification::class,
|
||||
fn (GeneralNotification $notification): bool => ! $notification->success
|
||||
&& str_contains($notification->message, 'Remove from Coolify only')
|
||||
);
|
||||
});
|
||||
|
||||
it('deletes only local service metadata when explicitly requested', function () {
|
||||
$service = Service::factory()->create([
|
||||
'environment_id' => $this->application->environment_id,
|
||||
'server_id' => $this->application->destination->server_id,
|
||||
'destination_id' => $this->application->destination_id,
|
||||
'destination_type' => $this->application->destination_type,
|
||||
]);
|
||||
ServiceApplication::create([
|
||||
'service_id' => $service->id,
|
||||
'name' => 'web',
|
||||
'image' => 'nginx:alpine',
|
||||
]);
|
||||
Process::fake();
|
||||
|
||||
(new DeleteResourceJob($service, deleteFromCoolifyOnly: true))->handle();
|
||||
|
||||
Process::assertNothingRan();
|
||||
expect(Service::withTrashed()->find($service->id))->toBeNull()
|
||||
->and(ServiceApplication::withTrashed()->where('service_id', $service->id)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('removes service containers before its volumes and local metadata', function () {
|
||||
$service = Service::factory()->create([
|
||||
'environment_id' => $this->application->environment_id,
|
||||
'server_id' => $this->application->destination->server_id,
|
||||
'destination_id' => $this->application->destination_id,
|
||||
'destination_type' => $this->application->destination_type,
|
||||
]);
|
||||
$application = ServiceApplication::create([
|
||||
'service_id' => $service->id,
|
||||
'name' => 'web',
|
||||
'image' => 'nginx:alpine',
|
||||
]);
|
||||
$application->persistentStorages()->create([
|
||||
'name' => "{$service->uuid}_web-data",
|
||||
'mount_path' => '/data',
|
||||
'host_path' => null,
|
||||
]);
|
||||
$privateKey = PrivateKey::factory()->create(['team_id' => $service->server->team_id]);
|
||||
$service->server->update(['private_key_id' => $privateKey->id]);
|
||||
$service->server->settings()->update(['is_reachable' => true, 'is_usable' => true]);
|
||||
$commands = collect();
|
||||
Process::fake(function ($process) use ($commands) {
|
||||
$commands->push($process->command);
|
||||
|
||||
return Process::result(output: '');
|
||||
});
|
||||
|
||||
(new DeleteResourceJob($service))->handle();
|
||||
|
||||
$commandList = $commands->implode("\n");
|
||||
expect($commandList)
|
||||
->toContain("label=coolify.serviceId={$service->id}")
|
||||
->toContain('docker rm -f $container_ids')
|
||||
->toContain("docker volume rm -f '{$service->uuid}_web-data'")
|
||||
->and(strpos($commandList, 'docker rm -f $container_ids'))
|
||||
->toBeLessThan(strpos($commandList, 'docker volume rm -f'));
|
||||
expect(Service::withTrashed()->find($service->id))->toBeNull();
|
||||
});
|
||||
|
||||
it('targets a service subresource container by its Docker labels', function () {
|
||||
$service = Service::factory()->create([
|
||||
'environment_id' => $this->application->environment_id,
|
||||
'server_id' => $this->application->destination->server_id,
|
||||
'destination_id' => $this->application->destination_id,
|
||||
'destination_type' => $this->application->destination_type,
|
||||
]);
|
||||
$application = ServiceApplication::create([
|
||||
'service_id' => $service->id,
|
||||
'name' => 'web',
|
||||
'image' => 'nginx:alpine',
|
||||
]);
|
||||
$privateKey = PrivateKey::factory()->create(['team_id' => $service->server->team_id]);
|
||||
$service->server->update(['private_key_id' => $privateKey->id]);
|
||||
$service->server->settings()->update(['is_reachable' => true, 'is_usable' => true]);
|
||||
$commands = collect();
|
||||
Process::fake(function ($process) use ($commands) {
|
||||
$commands->push($process->command);
|
||||
|
||||
return Process::result(output: '');
|
||||
});
|
||||
|
||||
app(DeleteService::class)->removeSubresourceContainer($application);
|
||||
|
||||
expect($commands->implode("\n"))
|
||||
->toContain("label=coolify.serviceId={$service->id}")
|
||||
->toContain("label=coolify.service.subId={$application->id}")
|
||||
->toContain('docker rm -f $container_ids');
|
||||
});
|
||||
|
||||
it('rolls back local metadata deletion when deleting the resource fails', function () {
|
||||
Process::fake(['*' => Process::result(output: '')]);
|
||||
$applicationUuid = $this->application->uuid;
|
||||
|
||||
@@ -100,7 +100,8 @@ test('delete succeeds without password for an oauth user', function () {
|
||||
->call('delete', '')
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect(Application::find($this->application->id))->toBeNull();
|
||||
expect(Application::find($this->application->id))->not->toBeNull();
|
||||
Queue::assertPushed(DeleteResourceJob::class, fn (DeleteResourceJob $job) => $job->resource->is($this->application));
|
||||
});
|
||||
|
||||
test('delete applies selectedActions from checkbox state', function () {
|
||||
@@ -112,3 +113,29 @@ test('delete applies selectedActions from checkbox state', function () {
|
||||
expect($component->get('delete_configurations'))->toBeTrue();
|
||||
expect($component->get('docker_cleanup'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('service can be removed from Coolify without remote cleanup', function () {
|
||||
$service = Service::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'server_id' => $this->server->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
|
||||
Livewire::test(Danger::class, ['resource' => $service])
|
||||
->set('projectUuid', $this->project->uuid)
|
||||
->set('environmentUuid', $this->environment->uuid)
|
||||
->assertSee('Server is not reachable')
|
||||
->assertSee('Remove from Coolify only')
|
||||
->call('deleteFromCoolifyOnly', 'test-password')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirectToRoute('project.resource.index', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
]);
|
||||
|
||||
Queue::assertPushed(
|
||||
DeleteResourceJob::class,
|
||||
fn (DeleteResourceJob $job): bool => $job->resource->is($service) && $job->deleteFromCoolifyOnly
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user