From 6c270a42dbb1739de2bf9e1a1a69fcb7a2690a7f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 24 Oct 2026 10:18:51 +0200 Subject: [PATCH] fix(service): remove the "Remove from Coolify only" delete option A normal delete now removes a resource from Coolify also when its server is not reachable, so the separate option only left untracked containers running. The danger zone button is gone, and the services API rejects delete_from_coolify_only with 422 instead of ignoring it, so old clients do not delete Docker resources they asked to keep. Co-Authored-By: Claude Opus 5.5 --- .../Controllers/Api/ServicesController.php | 22 +++++------- app/Livewire/Project/Shared/Danger.php | 26 -------------- app/Mcp/Tools/ManageService.php | 2 +- openapi.json | 13 ++----- openapi.yaml | 10 ++---- .../livewire/project/shared/danger.blade.php | 13 ------- tests/Feature/Api/LifecycleApisTest.php | 4 +-- .../Api/ServiceDeleteMessageApiTest.php | 35 ++++++------------- tests/v4/Feature/DangerDeleteResourceTest.php | 26 -------------- 9 files changed, 28 insertions(+), 123 deletions(-) diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 57ea276897..da1255eb08 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -1023,7 +1023,6 @@ 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 when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.', 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( @@ -1054,6 +1053,7 @@ class ServicesController extends Controller ref: '#/components/responses/404', ), new OA\Response(response: 403, description: 'The token needs the write and deploy permissions.'), + new OA\Response(response: 422, ref: '#/components/responses/422'), ] )] public function delete_by_uuid(Request $request) @@ -1072,10 +1072,14 @@ class ServicesController extends Controller $this->authorize('delete', $service); - $service->delete(); + // The parameter was removed. Reject it instead of ignoring it, so old clients do not delete Docker resources they asked to keep. + if ($request->boolean('delete_from_coolify_only')) { + return response()->json([ + 'message' => 'The delete_from_coolify_only parameter is no longer supported. Nothing was deleted. Send the request without it: Coolify removes the service also when the server is not reachable.', + ], 422); + } - $deleteFromCoolifyOnlyRequested = $request->boolean('delete_from_coolify_only'); - $deleteFromCoolifyOnly = $deleteFromCoolifyOnlyRequested || ! $service->server?->isFunctional(); + $service->delete(); DeleteResourceJob::dispatch( resource: $service, @@ -1083,23 +1087,15 @@ class ServicesController extends Controller deleteConnectedNetworks: $request->boolean('delete_connected_networks', true), deleteConfigurations: $request->boolean('delete_configurations', 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, ]); - $message = match (true) { - $deleteFromCoolifyOnlyRequested => 'The service will be removed from Coolify only; Docker resources will remain.', - $deleteFromCoolifyOnly => 'Server is not reachable. The service will be removed from Coolify only; Docker resources may remain.', - default => 'Service deletion request queued.', - }; - - return response()->json(['message' => $message]); + return response()->json(['message' => 'Service deletion request queued.']); } #[OA\Patch( diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php index 0865974828..d79532e6e8 100644 --- a/app/Livewire/Project/Shared/Danger.php +++ b/app/Livewire/Project/Shared/Danger.php @@ -132,32 +132,6 @@ class Danger extends Component } } - 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', [ - 'project_uuid' => $this->projectUuid, - 'environment_uuid' => $this->environmentUuid, - ]); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - public function flush($password) { try { diff --git a/app/Mcp/Tools/ManageService.php b/app/Mcp/Tools/ManageService.php index efa6d99fdd..cfd5b38c75 100644 --- a/app/Mcp/Tools/ManageService.php +++ b/app/Mcp/Tools/ManageService.php @@ -17,7 +17,7 @@ class ManageService extends RestActionTool { return [ 'update' => ['method' => 'PATCH', 'path' => 'services/{uuid}', 'ability' => 'write', 'description' => 'update the service (data: name, description, docker_compose_raw base64, connect_to_docker_network, urls, force_domain_override, is_container_label_escape_enabled, redact_all_env_values_in_logs, instant_deploy; instant_deploy needs deploy ability)'], - 'delete' => ['method' => 'DELETE', 'path' => 'services/{uuid}', 'ability' => 'deploy', 'confirm' => true, 'description' => 'delete the service (data: delete_volumes, delete_connected_networks, delete_configurations, docker_cleanup, delete_from_coolify_only)'], + 'delete' => ['method' => 'DELETE', 'path' => 'services/{uuid}', 'ability' => 'deploy', 'confirm' => true, 'description' => 'delete the service (data: delete_volumes, delete_connected_networks, delete_configurations, docker_cleanup)'], 'clone' => ['method' => 'POST', 'path' => 'services/{uuid}/clone', 'ability' => 'write', 'confirm' => true, 'description' => 'clone the service to a destination of the team (data: destination_uuid, name, clone_volumes; clone_volumes stops the source service while volumes are copied and needs deploy ability)'], 'move' => ['method' => 'POST', 'path' => 'services/{uuid}/move', 'ability' => 'write', 'confirm' => true, 'description' => 'move the service to another environment of the team (data: environment_uuid)'], 'update_application' => ['method' => 'PATCH', 'path' => 'services/{uuid}/applications/{app_uuid}', 'ability' => 'write', 'description' => 'update a service application (data: url, noindex_domains, human_name, description, image, exclude_from_status, is_log_drain_enabled, is_gzip_enabled, traefik_certificate_resolver, is_stripprefix_enabled, is_force_https_enabled, max_restart_count)'], diff --git a/openapi.json b/openapi.json index 5978a99124..7bd46ddba5 100644 --- a/openapi.json +++ b/openapi.json @@ -20486,16 +20486,6 @@ "type": "boolean", "default": true } - }, - { - "name": "delete_from_coolify_only", - "in": "query", - "description": "Remove only Coolify metadata without deleting Docker resources.", - "required": false, - "schema": { - "type": "boolean", - "default": false - } } ], "responses": { @@ -20526,6 +20516,9 @@ }, "403": { "description": "The token needs the write and deploy permissions." + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ diff --git a/openapi.yaml b/openapi.yaml index ad222762e4..5cfd3c35c8 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -12946,14 +12946,6 @@ paths: schema: type: boolean default: true - - - name: delete_from_coolify_only - in: query - description: 'Remove only Coolify metadata without deleting Docker resources.' - required: false - schema: - type: boolean - default: false responses: '200': description: 'Delete a service by UUID' @@ -12971,6 +12963,8 @@ paths: $ref: '#/components/responses/404' '403': description: 'The token needs the write and deploy permissions.' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] diff --git a/resources/views/livewire/project/shared/danger.blade.php b/resources/views/livewire/project/shared/danger.blade.php index 5aaf304ef1..73eaaac738 100644 --- a/resources/views/livewire/project/shared/danger.blade.php +++ b/resources/views/livewire/project/shared/danger.blade.php @@ -54,19 +54,6 @@ confirmationText="{{ $resourceName }}" confirmationLabel="Enter the resource name to confirm permanent deletion" shortConfirmationLabel="Resource name" /> - @if ($resource instanceof \App\Models\Service) - - @endif @else Delete {{ $resourceLabel }} diff --git a/tests/Feature/Api/LifecycleApisTest.php b/tests/Feature/Api/LifecycleApisTest.php index f761d0d218..0776a20612 100644 --- a/tests/Feature/Api/LifecycleApisTest.php +++ b/tests/Feature/Api/LifecycleApisTest.php @@ -332,14 +332,14 @@ describe('DELETE resource endpoints', function () { ->assertOk(); $response->assertJson([ - 'message' => 'Server is not reachable. The service will be removed from Coolify only; Docker resources may remain.', + 'message' => 'Service deletion request queued.', ]); expect(Service::find($service->id))->toBeNull() ->and(Service::withTrashed()->find($service->id)?->trashed())->toBeTrue(); Queue::assertPushed( DeleteResourceJob::class, - fn (DeleteResourceJob $job): bool => $job->deleteFromCoolifyOnly + fn (DeleteResourceJob $job): bool => $job->resource->is($service) && ! $job->deleteFromCoolifyOnly ); }); diff --git a/tests/Feature/Api/ServiceDeleteMessageApiTest.php b/tests/Feature/Api/ServiceDeleteMessageApiTest.php index cdee77e6c6..e655156f30 100644 --- a/tests/Feature/Api/ServiceDeleteMessageApiTest.php +++ b/tests/Feature/Api/ServiceDeleteMessageApiTest.php @@ -50,22 +50,17 @@ function setServiceDeleteServerReachable(Server $server, bool $reachable): void Server::flushIdentityMap(); } -test('explicit delete from coolify only does not claim the server is unreachable', function (bool $serverReachable) { - setServiceDeleteServerReachable($this->server, $serverReachable); - +test('delete from coolify only is rejected and deletes nothing', function () { $this->withToken($this->bearerToken) ->deleteJson("/api/v1/services/{$this->service->uuid}?delete_from_coolify_only=true") - ->assertOk() - ->assertJsonPath('message', 'The service will be removed from Coolify only; Docker resources will remain.'); + ->assertUnprocessable(); - Queue::assertPushed(DeleteResourceJob::class, fn (DeleteResourceJob $job): bool => $job->deleteFromCoolifyOnly); -})->with([ - 'reachable server' => [true], - 'unreachable server' => [false], -]); + expect(Service::find($this->service->id))->not->toBeNull(); + Queue::assertNotPushed(DeleteResourceJob::class); +}); -test('delete on a reachable server queues a full deletion', function () { - setServiceDeleteServerReachable($this->server, true); +test('delete queues a full deletion whether the server is reachable or not', function (bool $serverReachable) { + setServiceDeleteServerReachable($this->server, $serverReachable); $this->withToken($this->bearerToken) ->deleteJson("/api/v1/services/{$this->service->uuid}") @@ -73,15 +68,7 @@ test('delete on a reachable server queues a full deletion', function () { ->assertJsonPath('message', 'Service deletion request queued.'); Queue::assertPushed(DeleteResourceJob::class, fn (DeleteResourceJob $job): bool => ! $job->deleteFromCoolifyOnly); -}); - -test('delete on an unreachable server falls back to removing from coolify only', function () { - setServiceDeleteServerReachable($this->server, false); - - $this->withToken($this->bearerToken) - ->deleteJson("/api/v1/services/{$this->service->uuid}") - ->assertOk() - ->assertJsonPath('message', 'Server is not reachable. The service will be removed from Coolify only; Docker resources may remain.'); - - Queue::assertPushed(DeleteResourceJob::class, fn (DeleteResourceJob $job): bool => $job->deleteFromCoolifyOnly); -}); +})->with([ + 'reachable server' => [true], + 'unreachable server' => [false], +]); diff --git a/tests/v4/Feature/DangerDeleteResourceTest.php b/tests/v4/Feature/DangerDeleteResourceTest.php index 25cdf671a2..64dd56ede9 100644 --- a/tests/v4/Feature/DangerDeleteResourceTest.php +++ b/tests/v4/Feature/DangerDeleteResourceTest.php @@ -149,29 +149,3 @@ 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 - ); -});