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 - ); -});