mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
73b1ad1587
commit
6c270a42db
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)'],
|
||||
|
||||
+3
-10
@@ -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": [
|
||||
|
||||
+2
-8
@@ -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: []
|
||||
|
||||
@@ -54,19 +54,6 @@
|
||||
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 }}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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],
|
||||
]);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user