fix/feat: better volume cleanups

This commit is contained in:
Andras Bacsai 2024-07-11 12:38:54 +02:00
parent 36c4be1d17
commit 2b805f869a
16 changed files with 188 additions and 90 deletions

View File

@ -43,9 +43,6 @@ public function handle(Application $application)
$uuid = $application->uuid; $uuid = $application->uuid;
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false); instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
instant_remote_process(["docker network rm {$uuid}"], $server, false); instant_remote_process(["docker network rm {$uuid}"], $server, false);
// remove volumes
instant_remote_process(["cd {$application->dirOnServer()} && docker compose down -v"], $server, false);
} }
} }
} }

View File

@ -1240,6 +1240,16 @@ public function application_by_uuid(Request $request)
format: 'uuid', format: 'uuid',
) )
), ),
new OA\Parameter(
name: 'cleanup',
in: 'query',
description: 'Delete configurations and volumes.',
required: false,
schema: new OA\Schema(
type: 'boolean',
default: true,
)
),
], ],
responses: [ responses: [
new OA\Response( new OA\Response(
@ -1273,7 +1283,7 @@ public function application_by_uuid(Request $request)
public function delete_by_uuid(Request $request) public function delete_by_uuid(Request $request)
{ {
$teamId = getTeamIdFromToken(); $teamId = getTeamIdFromToken();
$cleanup = $request->query->get('cleanup') ?? false; $cleanup = $request->query->get('cleanup') ?? true;
if (is_null($teamId)) { if (is_null($teamId)) {
return invalidTokenResponse(); return invalidTokenResponse();
} }
@ -1287,7 +1297,7 @@ public function delete_by_uuid(Request $request)
'message' => 'Application not found', 'message' => 'Application not found',
], 404); ], 404);
} }
DeleteResourceJob::dispatch($application, $cleanup); DeleteResourceJob::dispatch($application, deleteConfigurations: $cleanup, deleteVolumes: $cleanup);
return response()->json([ return response()->json([
'message' => 'Application deletion request queued.', 'message' => 'Application deletion request queued.',

View File

@ -9,6 +9,7 @@
use App\Actions\Database\StopDatabaseProxy; use App\Actions\Database\StopDatabaseProxy;
use App\Enums\NewDatabaseTypes; use App\Enums\NewDatabaseTypes;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Jobs\DeleteResourceJob;
use App\Models\Project; use App\Models\Project;
use App\Models\Server; use App\Models\Server;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -1528,6 +1529,16 @@ public function create_database(Request $request, NewDatabaseTypes $type)
format: 'uuid', format: 'uuid',
) )
), ),
new OA\Parameter(
name: 'cleanup',
in: 'query',
description: 'Delete configurations and volumes.',
required: false,
schema: new OA\Schema(
type: 'boolean',
default: true,
)
),
], ],
responses: [ responses: [
new OA\Response( new OA\Response(
@ -1561,6 +1572,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
public function delete_by_uuid(Request $request) public function delete_by_uuid(Request $request)
{ {
$teamId = getTeamIdFromToken(); $teamId = getTeamIdFromToken();
$cleanup = $request->query->get('cleanup') ?? true;
if (is_null($teamId)) { if (is_null($teamId)) {
return invalidTokenResponse(); return invalidTokenResponse();
} }
@ -1571,8 +1583,7 @@ public function delete_by_uuid(Request $request)
if (! $database) { if (! $database) {
return response()->json(['message' => 'Database not found.'], 404); return response()->json(['message' => 'Database not found.'], 404);
} }
StopDatabase::dispatch($database); DeleteResourceJob::dispatch($database, deleteConfigurations: $cleanup, deleteVolumes: $cleanup);
$database->forceDelete();
return response()->json([ return response()->json([
'message' => 'Database deletion request queued.', 'message' => 'Database deletion request queued.',

View File

@ -28,14 +28,15 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, public bool $deleteConfigurations = false) {} public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, public bool $deleteConfigurations = false, public bool $deleteVolumes = false) {}
public function handle() public function handle()
{ {
try { try {
$this->resource->forceDelete(); $persistentStorages = collect();
switch ($this->resource->type()) { switch ($this->resource->type()) {
case 'application': case 'application':
$persistentStorages = $this->resource?->persistentStorages()?->get();
StopApplication::run($this->resource); StopApplication::run($this->resource);
break; break;
case 'standalone-postgresql': case 'standalone-postgresql':
@ -46,6 +47,7 @@ public function handle()
case 'standalone-keydb': case 'standalone-keydb':
case 'standalone-dragonfly': case 'standalone-dragonfly':
case 'standalone-clickhouse': case 'standalone-clickhouse':
$persistentStorages = $this->resource?->persistentStorages()?->get();
StopDatabase::run($this->resource); StopDatabase::run($this->resource);
break; break;
case 'service': case 'service':
@ -53,6 +55,10 @@ public function handle()
DeleteService::run($this->resource); DeleteService::run($this->resource);
break; break;
} }
if ($this->deleteVolumes && $this->resource->type() !== 'service') {
$this->resource?->delete_volumes($persistentStorages);
}
if ($this->deleteConfigurations) { if ($this->deleteConfigurations) {
$this->resource?->delete_configurations(); $this->resource?->delete_configurations();
} }
@ -61,6 +67,7 @@ public function handle()
send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage()); send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
throw $e; throw $e;
} finally { } finally {
$this->resource->forceDelete();
Artisan::queue('cleanup:stucked-resources'); Artisan::queue('cleanup:stucked-resources');
} }
} }

View File

@ -16,6 +16,8 @@ class Danger extends Component
public bool $delete_configurations = true; public bool $delete_configurations = true;
public bool $delete_volumes = true;
public ?string $modalId = null; public ?string $modalId = null;
public function mount() public function mount()
@ -31,7 +33,7 @@ public function delete()
try { try {
// $this->authorize('delete', $this->resource); // $this->authorize('delete', $this->resource);
$this->resource->delete(); $this->resource->delete();
DeleteResourceJob::dispatch($this->resource, $this->delete_configurations); DeleteResourceJob::dispatch($this->resource, $this->delete_configurations, $this->delete_volumes);
return redirect()->route('project.resource.index', [ return redirect()->route('project.resource.index', [
'project_uuid' => $this->projectUuid, 'project_uuid' => $this->projectUuid,

View File

@ -126,16 +126,9 @@ protected static function booted()
$application->compose_parsing_version = '2'; $application->compose_parsing_version = '2';
$application->save(); $application->save();
}); });
static::deleting(function ($application) { static::forceDeleting(function ($application) {
$application->update(['fqdn' => null]); $application->update(['fqdn' => null]);
$application->settings()->delete(); $application->settings()->delete();
$storages = $application->persistentStorages()->get();
$server = data_get($application, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$application->persistentStorages()->delete(); $application->persistentStorages()->delete();
$application->environment_variables()->delete(); $application->environment_variables()->delete();
$application->environment_variables_preview()->delete(); $application->environment_variables_preview()->delete();
@ -161,6 +154,23 @@ public function delete_configurations()
} }
} }
public function delete_volumes(?Collection $persistentStorages)
{
if ($this->build_pack === 'dockercompose') {
$server = data_get($this, 'destination.server');
ray('Deleting volumes');
instant_remote_process(["cd {$this->dirOnServer()} && docker compose down -v"], $server, false);
} else {
if ($persistentStorages->count() === 0) {
return;
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
}
public function additional_servers() public function additional_servers()
{ {
return $this->belongsToMany(Server::class, 'additional_destinations') return $this->belongsToMany(Server::class, 'additional_destinations')

View File

@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@ -31,16 +32,9 @@ protected static function booted()
'is_readonly' => true, 'is_readonly' => true,
]); ]);
}); });
static::deleting(function ($database) { static::forceDeleting(function ($database) {
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete(); $database->persistentStorages()->delete();
$database->scheduledBackups()->delete();
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach(); $database->tags()->detach();
}); });
@ -91,6 +85,17 @@ public function delete_configurations()
} }
} }
public function delete_volumes(Collection $persistentStorages)
{
if ($persistentStorages->count() === 0) {
return;
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
public function realStatus() public function realStatus()
{ {
return $this->getRawOriginal('status'); return $this->getRawOriginal('status');

View File

@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@ -31,16 +32,9 @@ protected static function booted()
'is_readonly' => true, 'is_readonly' => true,
]); ]);
}); });
static::deleting(function ($database) { static::forceDeleting(function ($database) {
$database->scheduledBackups()->delete();
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->persistentStorages()->delete(); $database->persistentStorages()->delete();
$database->scheduledBackups()->delete();
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach(); $database->tags()->detach();
}); });
@ -91,6 +85,17 @@ public function delete_configurations()
} }
} }
public function delete_volumes(Collection $persistentStorages)
{
if ($persistentStorages->count() === 0) {
return;
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
public function realStatus() public function realStatus()
{ {
return $this->getRawOriginal('status'); return $this->getRawOriginal('status');

View File

@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@ -31,16 +32,9 @@ protected static function booted()
'is_readonly' => true, 'is_readonly' => true,
]); ]);
}); });
static::deleting(function ($database) { static::forceDeleting(function ($database) {
$database->scheduledBackups()->delete();
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->persistentStorages()->delete(); $database->persistentStorages()->delete();
$database->scheduledBackups()->delete();
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach(); $database->tags()->detach();
}); });
@ -91,6 +85,17 @@ public function delete_configurations()
} }
} }
public function delete_volumes(Collection $persistentStorages)
{
if ($persistentStorages->count() === 0) {
return;
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
public function realStatus() public function realStatus()
{ {
return $this->getRawOriginal('status'); return $this->getRawOriginal('status');

View File

@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@ -31,16 +32,9 @@ protected static function booted()
'is_readonly' => true, 'is_readonly' => true,
]); ]);
}); });
static::deleting(function ($database) { static::forceDeleting(function ($database) {
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete(); $database->persistentStorages()->delete();
$database->scheduledBackups()->delete();
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach(); $database->tags()->detach();
}); });
@ -91,6 +85,17 @@ public function delete_configurations()
} }
} }
public function delete_volumes(Collection $persistentStorages)
{
if ($persistentStorages->count() === 0) {
return;
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
public function realStatus() public function realStatus()
{ {
return $this->getRawOriginal('status'); return $this->getRawOriginal('status');

View File

@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@ -35,16 +36,9 @@ protected static function booted()
'is_readonly' => true, 'is_readonly' => true,
]); ]);
}); });
static::deleting(function ($database) { static::forceDeleting(function ($database) {
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete(); $database->persistentStorages()->delete();
$database->scheduledBackups()->delete();
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach(); $database->tags()->detach();
}); });
@ -95,6 +89,17 @@ public function delete_configurations()
} }
} }
public function delete_volumes(Collection $persistentStorages)
{
if ($persistentStorages->count() === 0) {
return;
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
public function realStatus() public function realStatus()
{ {
return $this->getRawOriginal('status'); return $this->getRawOriginal('status');

View File

@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@ -32,16 +33,9 @@ protected static function booted()
'is_readonly' => true, 'is_readonly' => true,
]); ]);
}); });
static::deleting(function ($database) { static::forceDeleting(function ($database) {
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete(); $database->persistentStorages()->delete();
$database->scheduledBackups()->delete();
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach(); $database->tags()->detach();
}); });
@ -92,6 +86,17 @@ public function delete_configurations()
} }
} }
public function delete_volumes(Collection $persistentStorages)
{
if ($persistentStorages->count() === 0) {
return;
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
public function realStatus() public function realStatus()
{ {
return $this->getRawOriginal('status'); return $this->getRawOriginal('status');

View File

@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@ -32,16 +33,9 @@ protected static function booted()
'is_readonly' => true, 'is_readonly' => true,
]); ]);
}); });
static::deleting(function ($database) { static::forceDeleting(function ($database) {
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete(); $database->persistentStorages()->delete();
$database->scheduledBackups()->delete();
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach(); $database->tags()->detach();
}); });
@ -61,6 +55,18 @@ public function delete_configurations()
} }
} }
public function delete_volumes(Collection $persistentStorages)
{
if ($persistentStorages->count() === 0) {
return;
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
ray('Deleting volume: '.$storage->name);
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
public function isConfigurationChanged(bool $save = false) public function isConfigurationChanged(bool $save = false)
{ {
$newConfigHash = $this->image.$this->ports_mappings.$this->postgres_initdb_args.$this->postgres_host_auth_method; $newConfigHash = $this->image.$this->ports_mappings.$this->postgres_initdb_args.$this->postgres_host_auth_method;

View File

@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@ -27,16 +28,9 @@ protected static function booted()
'is_readonly' => true, 'is_readonly' => true,
]); ]);
}); });
static::deleting(function ($database) { static::forceDeleting(function ($database) {
$database->scheduledBackups()->delete();
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->persistentStorages()->delete(); $database->persistentStorages()->delete();
$database->scheduledBackups()->delete();
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach(); $database->tags()->detach();
}); });
@ -87,6 +81,17 @@ public function delete_configurations()
} }
} }
public function delete_volumes(Collection $persistentStorages)
{
if ($persistentStorages->count() === 0) {
return;
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
public function realStatus() public function realStatus()
{ {
return $this->getRawOriginal('status'); return $this->getRawOriginal('status');

View File

@ -1343,6 +1343,14 @@ paths:
schema: schema:
type: string type: string
format: uuid format: uuid
-
name: cleanup
in: query
description: 'Delete configurations and volumes.'
required: false
schema:
type: boolean
default: true
responses: responses:
'200': '200':
description: 'Application deleted.' description: 'Application deleted.'
@ -1799,6 +1807,14 @@ paths:
schema: schema:
type: string type: string
format: uuid format: uuid
-
name: cleanup
in: query
description: 'Delete configurations and volumes.'
required: false
schema:
type: boolean
default: true
responses: responses:
'200': '200':
description: 'Database deleted.' description: 'Database deleted.'
@ -4026,6 +4042,9 @@ components:
format: date-time format: date-time
nullable: true nullable: true
description: 'The date and time when the application was deleted.' description: 'The date and time when the application was deleted.'
compose_parsing_version:
type: string
description: 'How Coolify parse the compose file.'
type: object type: object
ApplicationDeploymentQueue: ApplicationDeploymentQueue:
description: 'Project model' description: 'Project model'

View File

@ -11,5 +11,6 @@
<h4>Actions</h4> <h4>Actions</h4>
<x-forms.checkbox id="delete_configurations" <x-forms.checkbox id="delete_configurations"
label="Permanently delete configuration files from the server?"></x-forms.checkbox> label="Permanently delete configuration files from the server?"></x-forms.checkbox>
<x-forms.checkbox id="delete_volumes" label="Permanently delete associated volumes?"></x-forms.checkbox>
</x-modal-confirmation> </x-modal-confirmation>
</div> </div>