diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php
index 7155f9a0a..3ed435049 100644
--- a/app/Actions/Application/StopApplication.php
+++ b/app/Actions/Application/StopApplication.php
@@ -3,50 +3,40 @@
namespace App\Actions\Application;
use App\Models\Application;
+use App\Actions\Server\CleanupDocker;
use Lorisleiva\Actions\Concerns\AsAction;
class StopApplication
{
use AsAction;
- public function handle(Application $application, bool $previewDeployments = false)
+ public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true)
{
- if ($application->destination->server->isSwarm()) {
- instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server);
-
- return;
- }
-
- $servers = collect([]);
- $servers->push($application->destination->server);
- $application->additional_servers->map(function ($server) use ($servers) {
- $servers->push($server);
- });
- foreach ($servers as $server) {
- if (! $server->isFunctional()) {
+ try {
+ $server = $application->destination->server;
+ if (!$server->isFunctional()) {
return 'Server is not functional';
}
- if ($previewDeployments) {
- $containers = getCurrentApplicationContainerStatus($server, $application->id, includePullrequests: true);
- } else {
- $containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
- }
- if ($containers->count() > 0) {
- foreach ($containers as $container) {
- $containerName = data_get($container, 'Names');
- if ($containerName) {
- instant_remote_process(command: ["docker stop --time=30 $containerName"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm $containerName"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm -f {$containerName}"], server: $server, throwError: false);
- }
- }
+ ray('Stopping application: ' . $application->name);
+
+ if ($server->isSwarm()) {
+ instant_remote_process(["docker stack rm {$application->uuid}"], $server);
+ return;
}
+
+ $containersToStop = $application->getContainersToStop($previewDeployments);
+ $application->stopContainers($containersToStop, $server);
+
if ($application->build_pack === 'dockercompose') {
- // remove network
- $uuid = $application->uuid;
- instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
- instant_remote_process(["docker network rm {$uuid}"], $server, false);
+ $application->delete_connected_networks($application->uuid);
}
+
+ if ($dockerCleanup) {
+ CleanupDocker::run($server, true);
+ }
+ } catch (\Exception $e) {
+ ray($e->getMessage());
+ return $e->getMessage();
}
}
}
diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php
index d562ec56f..0f074bea9 100644
--- a/app/Actions/Database/StopDatabase.php
+++ b/app/Actions/Database/StopDatabase.php
@@ -2,6 +2,7 @@
namespace App\Actions\Database;
+use App\Actions\Server\CleanupDocker;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
@@ -10,25 +11,65 @@ use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
+use Illuminate\Support\Facades\Process;
use Lorisleiva\Actions\Concerns\AsAction;
class StopDatabase
{
use AsAction;
- public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
+ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $isDeleteOperation = false, bool $dockerCleanup = true)
{
$server = $database->destination->server;
if (! $server->isFunctional()) {
return 'Server is not functional';
}
- instant_remote_process(command: ["docker stop --time=30 $database->uuid"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm $database->uuid"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm -f $database->uuid"], server: $server, throwError: false);
+ $this->stopContainer($database, $database->uuid, 300);
+ if (! $isDeleteOperation) {
+ if ($dockerCleanup) {
+ CleanupDocker::run($server, true);
+ }
+ }
if ($database->is_public) {
StopDatabaseProxy::run($database);
}
+
+ return 'Database stopped successfully';
+ }
+
+ private function stopContainer($database, string $containerName, int $timeout = 300): void
+ {
+ $server = $database->destination->server;
+
+ $process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+
+ $startTime = time();
+ while ($process->running()) {
+ if (time() - $startTime >= $timeout) {
+ $this->forceStopContainer($containerName, $server);
+ break;
+ }
+ usleep(100000);
+ }
+
+ $this->removeContainer($containerName, $server);
+ }
+
+ private function forceStopContainer(string $containerName, $server): void
+ {
+ instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
+ }
+
+ private function removeContainer(string $containerName, $server): void
+ {
+ instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
+ }
+
+ private function deleteConnectedNetworks($uuid, $server)
+ {
+ instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
+ instant_remote_process(["docker network rm {$uuid}"], $server, false);
}
}
diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php
index 194cf4db9..93c79383e 100644
--- a/app/Actions/Service/DeleteService.php
+++ b/app/Actions/Service/DeleteService.php
@@ -3,17 +3,18 @@
namespace App\Actions\Service;
use App\Models\Service;
+use App\Actions\Server\CleanupDocker;
use Lorisleiva\Actions\Concerns\AsAction;
class DeleteService
{
use AsAction;
- public function handle(Service $service)
+ public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $dockerCleanup, bool $deleteConnectedNetworks)
{
try {
$server = data_get($service, 'server');
- if ($server->isFunctional()) {
+ if ($deleteVolumes && $server->isFunctional()) {
$storagesToDelete = collect([]);
$service->environment_variables()->delete();
@@ -33,13 +34,29 @@ class DeleteService
foreach ($storagesToDelete as $storage) {
$commands[] = "docker volume rm -f $storage->name";
}
- $commands[] = "docker rm -f $service->uuid";
- instant_remote_process($commands, $server, false);
+ // Execute volume deletion first, this must be done first otherwise volumes will not be deleted.
+ if (!empty($commands)) {
+ foreach ($commands as $command) {
+ $result = instant_remote_process([$command], $server, false);
+ if ($result !== 0) {
+ ray("Failed to execute: $command");
+ }
+ }
+ }
}
+
+ if ($deleteConnectedNetworks) {
+ $service->delete_connected_networks($service->uuid);
+ }
+
+ instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false);
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
} finally {
+ if ($deleteConfigurations) {
+ $service->delete_configurations();
+ }
foreach ($service->applications()->get() as $application) {
$application->forceDelete();
}
@@ -50,6 +67,11 @@ class DeleteService
$task->delete();
}
$service->tags()->detach();
+ $service->forceDelete();
+
+ if ($dockerCleanup) {
+ CleanupDocker::run($server, true);
+ }
}
}
}
diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php
index 82b0b3ece..d69898a58 100644
--- a/app/Actions/Service/StopService.php
+++ b/app/Actions/Service/StopService.php
@@ -3,46 +3,33 @@
namespace App\Actions\Service;
use App\Models\Service;
+use App\Actions\Server\CleanupDocker;
use Lorisleiva\Actions\Concerns\AsAction;
class StopService
{
use AsAction;
- public function handle(Service $service)
+ public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true)
{
try {
$server = $service->destination->server;
- if (! $server->isFunctional()) {
+ if (!$server->isFunctional()) {
return 'Server is not functional';
}
- ray('Stopping service: '.$service->name);
- $applications = $service->applications()->get();
- foreach ($applications as $application) {
- if ($applications->count() < 6) {
- instant_remote_process(command: ["docker stop --time=10 {$application->name}-{$service->uuid}"], server: $server, throwError: false);
- }
- instant_remote_process(command: ["docker rm {$application->name}-{$service->uuid}"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm -f {$application->name}-{$service->uuid}"], server: $server, throwError: false);
- $application->update(['status' => 'exited']);
- }
- $dbs = $service->databases()->get();
- foreach ($dbs as $db) {
- if ($dbs->count() < 6) {
- instant_remote_process(command: ["docker stop --time=10 {$db->name}-{$service->uuid}"], server: $server, throwError: false);
+ $containersToStop = $service->getContainersToStop();
+ $service->stopContainers($containersToStop, $server);
+
+ if (!$isDeleteOperation) {
+ $service->delete_connected_networks($service->uuid);
+ if ($dockerCleanup) {
+ CleanupDocker::run($server, true);
}
- instant_remote_process(command: ["docker rm {$db->name}-{$service->uuid}"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm -f {$db->name}-{$service->uuid}"], server: $server, throwError: false);
- $db->update(['status' => 'exited']);
}
- instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy"], $service->server);
- instant_remote_process(["docker network rm {$service->uuid}"], $service->server);
} catch (\Exception $e) {
ray($e->getMessage());
-
return $e->getMessage();
}
-
}
}
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index df7daa3b4..5ee0ef189 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -29,6 +29,7 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Illuminate\Support\Sleep;
use Illuminate\Support\Str;
+use Illuminate\Support\Facades\Process;
use RuntimeException;
use Symfony\Component\Yaml\Yaml;
use Throwable;
@@ -2200,20 +2201,40 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->application_deployment_queue->addLogEntry('Building docker image completed.');
}
- /**
- * @param int $timeout in seconds
- */
- private function graceful_shutdown_container(string $containerName, int $timeout = 30)
+ private function graceful_shutdown_container(string $containerName, int $timeout = 300)
{
try {
- $this->execute_remote_command(
- ["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
- ["docker rm $containerName", 'hidden' => true, 'ignore_errors' => true]
- );
+ $process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+
+ $startTime = time();
+ while ($process->running()) {
+ if (time() - $startTime >= $timeout) {
+ $this->execute_remote_command(
+ ["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
+ );
+ break;
+ }
+ usleep(100000);
+ }
+
+ $isRunning = $this->execute_remote_command(
+ ["docker inspect -f '{{.State.Running}}' $containerName", 'hidden' => true, 'ignore_errors' => true]
+ ) === 'true';
+
+ if ($isRunning) {
+ $this->execute_remote_command(
+ ["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
+ );
+ }
} catch (\Exception $error) {
- // report error if needed
+ $this->application_deployment_queue->addLogEntry("Error stopping container $containerName: " . $error->getMessage(), 'stderr');
}
+ $this->remove_container($containerName);
+ }
+
+ private function remove_container(string $containerName)
+ {
$this->execute_remote_command(
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
);
@@ -2229,7 +2250,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
if ($this->pull_request_id === 0) {
$containers = $containers->filter(function ($container) {
- return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name.'-pr-'.$this->pull_request_id;
+ return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name . '-pr-' . $this->pull_request_id;
});
}
$containers->each(function ($container) {
diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php
index dbf44dd5d..37300593e 100644
--- a/app/Jobs/DeleteResourceJob.php
+++ b/app/Jobs/DeleteResourceJob.php
@@ -4,6 +4,7 @@ namespace App\Jobs;
use App\Actions\Application\StopApplication;
use App\Actions\Database\StopDatabase;
+use App\Actions\Server\CleanupDocker;
use App\Actions\Service\DeleteService;
use App\Actions\Service\StopService;
use App\Models\Application;
@@ -30,8 +31,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource,
- public bool $deleteConfigurations = false,
- public bool $deleteVolumes = false) {}
+ public bool $deleteConfigurations,
+ public bool $deleteVolumes,
+ public bool $dockerCleanup,
+ public bool $deleteConnectedNetworks
+ ) {}
public function handle()
{
@@ -51,11 +55,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
case 'standalone-dragonfly':
case 'standalone-clickhouse':
$persistentStorages = $this->resource?->persistentStorages()?->get();
- StopDatabase::run($this->resource);
+ StopDatabase::run($this->resource, true);
break;
case 'service':
- StopService::run($this->resource);
- DeleteService::run($this->resource);
+ StopService::run($this->resource, true);
+ DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks);
break;
}
@@ -65,12 +69,31 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
if ($this->deleteConfigurations) {
$this->resource?->delete_configurations();
}
+
+ $isDatabase = $this->resource instanceof StandalonePostgresql
+ || $this->resource instanceof StandaloneRedis
+ || $this->resource instanceof StandaloneMongodb
+ || $this->resource instanceof StandaloneMysql
+ || $this->resource instanceof StandaloneMariadb
+ || $this->resource instanceof StandaloneKeydb
+ || $this->resource instanceof StandaloneDragonfly
+ || $this->resource instanceof StandaloneClickhouse;
+ $server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
+ if (($this->dockerCleanup || $isDatabase) && $server) {
+ CleanupDocker::run($server, true);
+ }
+
+ if ($this->deleteConnectedNetworks && ! $isDatabase) {
+ $this->resource?->delete_connected_networks($this->resource->uuid);
+ }
} catch (\Throwable $e) {
- ray($e->getMessage());
send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
throw $e;
} finally {
$this->resource->forceDelete();
+ if ($this->dockerCleanup) {
+ CleanupDocker::run($server, true);
+ }
Artisan::queue('cleanup:stucked-resources');
}
}
diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php
index 68555d26c..1f0b68dd3 100644
--- a/app/Livewire/Dashboard.php
+++ b/app/Livewire/Dashboard.php
@@ -30,7 +30,6 @@ class Dashboard extends Component
public function cleanup_queue()
{
- $this->dispatch('success', 'Cleanup started.');
Artisan::queue('cleanup:application-deployment-queue', [
'--team-id' => currentTeam()->id,
]);
diff --git a/app/Livewire/Destination/Form.php b/app/Livewire/Destination/Form.php
index 7125f2120..5b3115dea 100644
--- a/app/Livewire/Destination/Form.php
+++ b/app/Livewire/Destination/Form.php
@@ -1,7 +1,6 @@
destination->delete();
- return redirect()->route('dashboard');
+ return redirect()->route('destination.all');
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/NavbarDeleteTeam.php b/app/Livewire/NavbarDeleteTeam.php
index ec196c154..6b3ea3fd4 100644
--- a/app/Livewire/NavbarDeleteTeam.php
+++ b/app/Livewire/NavbarDeleteTeam.php
@@ -4,11 +4,25 @@ namespace App\Livewire;
use Illuminate\Support\Facades\DB;
use Livewire\Component;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
class NavbarDeleteTeam extends Component
{
- public function delete()
+ public $team;
+
+ public function mount()
{
+ $this->team = currentTeam()->name;
+ }
+
+ public function delete($password)
+ {
+ if (!Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+ return;
+ }
+
$currentTeam = currentTeam();
$currentTeam->delete();
diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php
index c02949e17..1082b48cd 100644
--- a/app/Livewire/Project/Application/Heading.php
+++ b/app/Livewire/Project/Application/Heading.php
@@ -21,6 +21,8 @@ class Heading extends Component
protected string $deploymentUuid;
+ public bool $docker_cleanup = true;
+
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
@@ -102,7 +104,7 @@ class Heading extends Component
public function stop()
{
- StopApplication::run($this->application);
+ StopApplication::run($this->application, false, $this->docker_cleanup);
$this->application->status = 'exited';
$this->application->save();
if ($this->application->additional_servers->count() > 0) {
@@ -135,4 +137,13 @@ class Heading extends Component
'environment_name' => $this->parameters['environment_name'],
]);
}
+
+ public function render()
+ {
+ return view('livewire.project.application.heading', [
+ 'checkboxes' => [
+ ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
+ ],
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php
index 317a2ae51..397e159ad 100644
--- a/app/Livewire/Project/Application/Previews.php
+++ b/app/Livewire/Project/Application/Previews.php
@@ -9,6 +9,8 @@ use Illuminate\Support\Collection;
use Livewire\Component;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
+use Illuminate\Process\InvokedProcess;
+use Illuminate\Support\Facades\Process;
class Previews extends Component
{
@@ -184,17 +186,20 @@ class Previews extends Component
public function stop(int $pull_request_id)
{
try {
+ $server = $this->application->destination->server;
+ $timeout = 300;
+
if ($this->application->destination->server->isSwarm()) {
- instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
+ instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else {
- $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
- foreach ($containers as $container) {
- $name = str_replace('/', '', $container['Names']);
- instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
- }
+ $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
+ $this->stopContainers($containers, $server, $timeout);
}
- GetContainersStatus::dispatchSync($this->application->destination->server)->onQueue('high');
- $this->dispatch('reloadWindow');
+
+ GetContainersStatus::run($server);
+ $this->application->refresh();
+ $this->dispatch('containerStatusUpdated');
+ $this->dispatch('success', 'Preview Deployment stopped.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -203,16 +208,21 @@ class Previews extends Component
public function delete(int $pull_request_id)
{
try {
+ $server = $this->application->destination->server;
+ $timeout = 300;
+
if ($this->application->destination->server->isSwarm()) {
- instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
+ instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else {
- $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
- foreach ($containers as $container) {
- $name = str_replace('/', '', $container['Names']);
- instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
- }
+ $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
+ $this->stopContainers($containers, $server, $timeout);
}
- ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete();
+
+ ApplicationPreview::where('application_id', $this->application->id)
+ ->where('pull_request_id', $pull_request_id)
+ ->first()
+ ->delete();
+
$this->application->refresh();
$this->dispatch('update_links');
$this->dispatch('success', 'Preview deleted.');
@@ -220,4 +230,49 @@ class Previews extends Component
return handleError($e, $this);
}
}
+
+ private function stopContainers(array $containers, $server, int $timeout)
+ {
+ $processes = [];
+ foreach ($containers as $container) {
+ $containerName = str_replace('/', '', $container['Names']);
+ $processes[$containerName] = $this->stopContainer($containerName, $timeout);
+ }
+
+ $startTime = time();
+ while (count($processes) > 0) {
+ $finishedProcesses = array_filter($processes, function ($process) {
+ return !$process->running();
+ });
+ foreach (array_keys($finishedProcesses) as $containerName) {
+ unset($processes[$containerName]);
+ $this->removeContainer($containerName, $server);
+ }
+
+ if (time() - $startTime >= $timeout) {
+ $this->forceStopRemainingContainers(array_keys($processes), $server);
+ break;
+ }
+
+ usleep(100000);
+ }
+ }
+
+ private function stopContainer(string $containerName, int $timeout): InvokedProcess
+ {
+ return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+ }
+
+ private function removeContainer(string $containerName, $server)
+ {
+ instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
+ }
+
+ private function forceStopRemainingContainers(array $containerNames, $server)
+ {
+ foreach ($containerNames as $containerName) {
+ instant_remote_process(["docker kill $containerName"], $server, throwError: false);
+ $this->removeContainer($containerName, $server);
+ }
+ }
}
diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php
index 59f2f9a39..9efb2d9fc 100644
--- a/app/Livewire/Project/Database/BackupEdit.php
+++ b/app/Livewire/Project/Database/BackupEdit.php
@@ -5,6 +5,8 @@ namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
use Livewire\Component;
use Spatie\Url\Url;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\Auth;
class BackupEdit extends Component
{
@@ -12,6 +14,10 @@ class BackupEdit extends Component
public $s3s;
+ public bool $delete_associated_backups_locally = false;
+ public bool $delete_associated_backups_s3 = false;
+ public bool $delete_associated_backups_sftp = false;
+
public ?string $status = null;
public array $parameters;
@@ -46,16 +52,29 @@ class BackupEdit extends Component
}
}
- public function delete()
+ public function delete($password)
{
+ if (!Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+ return;
+ }
+
try {
+ if ($this->delete_associated_backups_locally) {
+ $this->deleteAssociatedBackupsLocally();
+ }
+ if ($this->delete_associated_backups_s3) {
+ $this->deleteAssociatedBackupsS3();
+ }
+
$this->backup->delete();
+
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$previousUrl = url()->previous();
$url = Url::fromString($previousUrl);
$url = $url->withoutQueryParameter('selectedBackupId');
$url = $url->withFragment('backups');
- $url = $url->getPath()."#{$url->getFragment()}";
+ $url = $url->getPath() . "#{$url->getFragment()}";
return redirect($url);
} else {
@@ -104,4 +123,66 @@ class BackupEdit extends Component
$this->dispatch('error', $e->getMessage());
}
}
+
+ public function deleteAssociatedBackupsLocally()
+ {
+ $executions = $this->backup->executions;
+ $backupFolder = null;
+
+ foreach ($executions as $execution) {
+ if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
+ $server = $this->backup->database->service->destination->server;
+ } else {
+ $server = $this->backup->database->destination->server;
+ }
+
+ if (!$backupFolder) {
+ $backupFolder = dirname($execution->filename);
+ }
+
+ delete_backup_locally($execution->filename, $server);
+ $execution->delete();
+ }
+
+ if ($backupFolder) {
+ $this->deleteEmptyBackupFolder($backupFolder, $server);
+ }
+ }
+
+ public function deleteAssociatedBackupsS3()
+ {
+ //Add function to delete backups from S3
+ }
+
+ public function deleteAssociatedBackupsSftp()
+ {
+ //Add function to delete backups from SFTP
+ }
+
+ private function deleteEmptyBackupFolder($folderPath, $server)
+ {
+ $checkEmpty = instant_remote_process(["[ -z \"$(ls -A '$folderPath')\" ] && echo 'empty' || echo 'not empty'"], $server);
+
+ if (trim($checkEmpty) === 'empty') {
+ instant_remote_process(["rmdir '$folderPath'"], $server);
+
+ $parentFolder = dirname($folderPath);
+ $checkParentEmpty = instant_remote_process(["[ -z \"$(ls -A '$parentFolder')\" ] && echo 'empty' || echo 'not empty'"], $server);
+
+ if (trim($checkParentEmpty) === 'empty') {
+ instant_remote_process(["rmdir '$parentFolder'"], $server);
+ }
+ }
+ }
+
+ public function render()
+ {
+ return view('livewire.project.database.backup-edit', [
+ 'checkboxes' => [
+ ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from local storage.'],
+ // ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected S3 Storage.']
+ // ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.']
+ ]
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php
index 5d56ea53d..e16397652 100644
--- a/app/Livewire/Project/Database/BackupExecutions.php
+++ b/app/Livewire/Project/Database/BackupExecutions.php
@@ -4,6 +4,9 @@ namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
use Livewire\Component;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\Auth;
+use Livewire\Attributes\On;
class BackupExecutions extends Component
{
@@ -12,9 +15,12 @@ class BackupExecutions extends Component
public $executions = [];
public $setDeletableBackup;
+ public $delete_backup_s3 = true;
+ public $delete_backup_sftp = true;
+
public function getListeners()
{
- $userId = auth()->user()->id;
+ $userId = Auth::id();
return [
"echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions',
@@ -31,19 +37,34 @@ class BackupExecutions extends Component
}
}
- public function deleteBackup($exeuctionId)
+ #[On('deleteBackup')]
+ public function deleteBackup($executionId, $password)
{
- $execution = $this->backup->executions()->where('id', $exeuctionId)->first();
- if (is_null($execution)) {
- $this->dispatch('error', 'Backup execution not found.');
-
+ if (!Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
return;
}
+
+ $execution = $this->backup->executions()->where('id', $executionId)->first();
+ if (is_null($execution)) {
+ $this->dispatch('error', 'Backup execution not found.');
+ return;
+ }
+
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server);
} else {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
}
+
+ if ($this->delete_backup_s3) {
+ // Add logic to delete from S3
+ }
+
+ if ($this->delete_backup_sftp) {
+ // Add logic to delete from SFTP
+ }
+
$execution->delete();
$this->dispatch('success', 'Backup deleted.');
$this->refreshBackupExecutions();
@@ -106,4 +127,14 @@ class BackupExecutions extends Component
}
return $dateObj->format('Y-m-d H:i:s T');
}
+
+ public function render()
+ {
+ return view('livewire.project.database.backup-executions', [
+ 'checkboxes' => [
+ ['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'],
+ ['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'],
+ ]
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php
index 6435f6781..765213f60 100644
--- a/app/Livewire/Project/Database/Heading.php
+++ b/app/Livewire/Project/Database/Heading.php
@@ -14,6 +14,8 @@ class Heading extends Component
public array $parameters;
+ public $docker_cleanup = true;
+
public function getListeners()
{
$userId = auth()->user()->id;
@@ -54,7 +56,7 @@ class Heading extends Component
public function stop()
{
- StopDatabase::run($this->database);
+ StopDatabase::run($this->database, false, $this->docker_cleanup);
$this->database->status = 'exited';
$this->database->save();
$this->check_status();
@@ -71,4 +73,13 @@ class Heading extends Component
$activity = StartDatabase::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
}
+
+ public function render()
+ {
+ return view('livewire.project.database.heading', [
+ 'checkboxes' => [
+ ['id' => 'docker_cleanup', 'label' => 'Cleanup docker build cache and unused images (next deployment could take longer).'],
+ ],
+ ]);
+ }
}
diff --git a/app/Livewire/Project/DeleteEnvironment.php b/app/Livewire/Project/DeleteEnvironment.php
index 22478916f..e01741770 100644
--- a/app/Livewire/Project/DeleteEnvironment.php
+++ b/app/Livewire/Project/DeleteEnvironment.php
@@ -13,9 +13,12 @@ class DeleteEnvironment extends Component
public bool $disabled = false;
+ public string $environmentName = '';
+
public function mount()
{
$this->parameters = get_route_parameters();
+ $this->environmentName = Environment::findOrFail($this->environment_id)->name;
}
public function delete()
diff --git a/app/Livewire/Project/DeleteProject.php b/app/Livewire/Project/DeleteProject.php
index 499b86e3e..360fad10a 100644
--- a/app/Livewire/Project/DeleteProject.php
+++ b/app/Livewire/Project/DeleteProject.php
@@ -13,9 +13,12 @@ class DeleteProject extends Component
public bool $disabled = false;
+ public string $projectName = '';
+
public function mount()
{
$this->parameters = get_route_parameters();
+ $this->projectName = Project::findOrFail($this->project_id)->name;
}
public function delete()
diff --git a/app/Livewire/Project/Service/Configuration.php b/app/Livewire/Project/Service/Configuration.php
index fa44fdfbf..a2e48fee7 100644
--- a/app/Livewire/Project/Service/Configuration.php
+++ b/app/Livewire/Project/Service/Configuration.php
@@ -52,7 +52,7 @@ class Configuration extends Component
$application = $this->service->applications->find($id);
if ($application) {
$application->restart();
- $this->dispatch('success', 'Application restarted successfully.');
+ $this->dispatch('success', 'Service application restarted successfully.');
}
} catch (\Exception $e) {
return handleError($e, $this);
@@ -65,7 +65,7 @@ class Configuration extends Component
$database = $this->service->databases->find($id);
if ($database) {
$database->restart();
- $this->dispatch('success', 'Database restarted successfully.');
+ $this->dispatch('success', 'Service database restarted successfully.');
}
} catch (\Exception $e) {
return handleError($e, $this);
diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php
index 6cd54883e..9fac897bf 100644
--- a/app/Livewire/Project/Service/FileStorage.php
+++ b/app/Livewire/Project/Service/FileStorage.php
@@ -15,6 +15,8 @@ use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Livewire\Component;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\Auth;
class FileStorage extends Component
{
@@ -83,8 +85,13 @@ class FileStorage extends Component
}
}
- public function delete()
+ public function delete($password)
{
+ if (!Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+ return;
+ }
+
try {
$message = 'File deleted.';
if ($this->fileStorage->is_directory) {
@@ -127,8 +134,15 @@ class FileStorage extends Component
$this->submit();
}
- public function render()
+ public function render()
{
- return view('livewire.project.service.file-storage');
+ return view('livewire.project.service.file-storage', [
+ 'directoryDeletionCheckboxes' => [
+ ['id' => 'permanently_delete', 'label' => 'The selected directory and all its contents will be permantely deleted form the server.'],
+ ],
+ 'fileDeletionCheckboxes' => [
+ ['id' => 'permanently_delete', 'label' => 'The selected file will be permanently deleted form the server.'],
+ ]
+ ]);
}
}
diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php
index e6bb6d9bf..42c9357fd 100644
--- a/app/Livewire/Project/Service/Navbar.php
+++ b/app/Livewire/Project/Service/Navbar.php
@@ -20,6 +20,8 @@ class Navbar extends Component
public $isDeploymentProgress = false;
+ public $docker_cleanup = true;
+
public $title = 'Configuration';
public function mount()
@@ -42,7 +44,7 @@ class Navbar extends Component
public function serviceStarted()
{
- $this->dispatch('success', 'Service status changed.');
+ // $this->dispatch('success', 'Service status changed.');
if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) {
$this->service->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
@@ -62,11 +64,6 @@ class Navbar extends Component
$this->dispatch('success', 'Service status updated.');
}
- public function render()
- {
- return view('livewire.project.service.navbar');
- }
-
public function checkDeployments()
{
try {
@@ -97,14 +94,9 @@ class Navbar extends Component
$this->dispatch('activityMonitor', $activity->id);
}
- public function stop(bool $forceCleanup = false)
+ public function stop()
{
- StopService::run($this->service);
- if ($forceCleanup) {
- $this->dispatch('success', 'Containers cleaned up.');
- } else {
- $this->dispatch('success', 'Service stopped.');
- }
+ StopService::run($this->service, false, $this->docker_cleanup);
ServiceStatusChanged::dispatch();
}
@@ -123,4 +115,13 @@ class Navbar extends Component
$activity = StartService::run($this->service);
$this->dispatch('activityMonitor', $activity->id);
}
+
+ public function render()
+ {
+ return view('livewire.project.service.navbar', [
+ 'checkboxes' => [
+ ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
+ ],
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php
index e7d00c3dd..56b506043 100644
--- a/app/Livewire/Project/Service/ServiceApplicationView.php
+++ b/app/Livewire/Project/Service/ServiceApplicationView.php
@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Service;
use App\Models\ServiceApplication;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class ServiceApplicationView extends Component
@@ -11,6 +13,10 @@ class ServiceApplicationView extends Component
public $parameters;
+ public $docker_cleanup = true;
+
+ public $delete_volumes = true;
+
protected $rules = [
'application.human_name' => 'nullable',
'application.description' => 'nullable',
@@ -23,11 +29,6 @@ class ServiceApplicationView extends Component
'application.is_stripprefix_enabled' => 'nullable|boolean',
];
- public function render()
- {
- return view('livewire.project.service.service-application-view');
- }
-
public function updatedApplicationFqdn()
{
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
@@ -56,8 +57,14 @@ class ServiceApplicationView extends Component
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
}
- public function delete()
+ public function delete($password)
{
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+
try {
$this->application->delete();
$this->dispatch('success', 'Application deleted.');
@@ -91,4 +98,17 @@ class ServiceApplicationView extends Component
$this->dispatch('generateDockerCompose');
}
}
+
+ public function render()
+ {
+ return view('livewire.project.service.service-application-view', [
+ 'checkboxes' => [
+ ['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
+ ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
+ // ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
+ // ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
+ // ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
+ ],
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php
index 5f0178be4..543e64539 100644
--- a/app/Livewire/Project/Shared/Danger.php
+++ b/app/Livewire/Project/Shared/Danger.php
@@ -3,6 +3,11 @@
namespace App\Livewire\Project\Shared;
use App\Jobs\DeleteResourceJob;
+use App\Models\Service;
+use App\Models\ServiceApplication;
+use App\Models\ServiceDatabase;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -10,6 +15,8 @@ class Danger extends Component
{
public $resource;
+ public $resourceName;
+
public $projectUuid;
public $environmentName;
@@ -18,22 +25,93 @@ class Danger extends Component
public bool $delete_volumes = true;
+ public bool $docker_cleanup = true;
+
+ public bool $delete_connected_networks = true;
+
public ?string $modalId = null;
+ public string $resourceDomain = '';
+
public function mount()
{
- $this->modalId = new Cuid2;
$parameters = get_route_parameters();
+ $this->modalId = new Cuid2;
$this->projectUuid = data_get($parameters, 'project_uuid');
$this->environmentName = data_get($parameters, 'environment_name');
+
+ if ($this->resource === null) {
+ if (isset($parameters['service_uuid'])) {
+ $this->resource = Service::where('uuid', $parameters['service_uuid'])->first();
+ } elseif (isset($parameters['stack_service_uuid'])) {
+ $this->resource = ServiceApplication::where('uuid', $parameters['stack_service_uuid'])->first()
+ ?? ServiceDatabase::where('uuid', $parameters['stack_service_uuid'])->first();
+ }
+ }
+
+ if ($this->resource === null) {
+ $this->resourceName = 'Unknown Resource';
+
+ return;
+ }
+
+ if (! method_exists($this->resource, 'type')) {
+ $this->resourceName = 'Unknown Resource';
+
+ return;
+ }
+
+ switch ($this->resource->type()) {
+ case 'application':
+ $this->resourceName = $this->resource->name ?? 'Application';
+ break;
+ case 'standalone-postgresql':
+ case 'standalone-redis':
+ case 'standalone-mongodb':
+ case 'standalone-mysql':
+ case 'standalone-mariadb':
+ case 'standalone-keydb':
+ case 'standalone-dragonfly':
+ case 'standalone-clickhouse':
+ $this->resourceName = $this->resource->name ?? 'Database';
+ break;
+ case 'service':
+ $this->resourceName = $this->resource->name ?? 'Service';
+ break;
+ case 'service-application':
+ $this->resourceName = $this->resource->name ?? 'Service Application';
+ break;
+ case 'service-database':
+ $this->resourceName = $this->resource->name ?? 'Service Database';
+ break;
+ default:
+ $this->resourceName = 'Unknown Resource';
+ }
}
- public function delete()
+ public function delete($password)
{
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+
+ if (! $this->resource) {
+ $this->addError('resource', 'Resource not found.');
+
+ return;
+ }
+
try {
- // $this->authorize('delete', $this->resource);
$this->resource->delete();
- DeleteResourceJob::dispatch($this->resource, $this->delete_configurations, $this->delete_volumes);
+ DeleteResourceJob::dispatch(
+ $this->resource,
+ $this->delete_configurations,
+ $this->delete_volumes,
+ $this->docker_cleanup,
+ $this->delete_connected_networks
+ );
return redirect()->route('project.resource.index', [
'project_uuid' => $this->projectUuid,
@@ -43,4 +121,19 @@ class Danger extends Component
return handleError($e, $this);
}
}
+
+ public function render()
+ {
+ return view('livewire.project.shared.danger', [
+ 'checkboxes' => [
+ ['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
+ ['id' => 'delete_connected_networks', 'label' => __('resource.delete_connected_networks')],
+ ['id' => 'delete_configurations', 'label' => __('resource.delete_configurations')],
+ ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
+ // ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
+ // ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
+ // ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
+ ],
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php
index a2c018beb..1be724568 100644
--- a/app/Livewire/Project/Shared/Destination.php
+++ b/app/Livewire/Project/Shared/Destination.php
@@ -10,6 +10,8 @@ use App\Models\Server;
use App\Models\StandaloneDocker;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\Auth;
class Destination extends Component
{
@@ -115,8 +117,13 @@ class Destination extends Component
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
}
- public function removeServer(int $network_id, int $server_id)
+ public function removeServer(int $network_id, int $server_id, $password)
{
+ if (!Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+ return;
+ }
+
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
$this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');
diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php
index 8be4ff643..37f50dd32 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/Show.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php
@@ -20,6 +20,8 @@ class Show extends Component
public string $type;
+ public string $scheduledTaskName;
+
protected $rules = [
'task.enabled' => 'required|boolean',
'task.name' => 'required|string',
@@ -49,6 +51,7 @@ class Show extends Component
$this->modalId = new Cuid2;
$this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first();
+ $this->scheduledTaskName = $this->task->name;
}
public function instantSave()
@@ -75,9 +78,9 @@ class Show extends Component
$this->task->delete();
if ($this->type == 'application') {
- return redirect()->route('project.application.configuration', $this->parameters);
+ return redirect()->route('project.application.configuration', $this->parameters, $this->scheduledTaskName);
} else {
- return redirect()->route('project.service.configuration', $this->parameters);
+ return redirect()->route('project.service.configuration', $this->parameters, $this->scheduledTaskName);
}
} catch (\Exception $e) {
return handleError($e);
diff --git a/app/Livewire/Project/Shared/Storages/Show.php b/app/Livewire/Project/Shared/Storages/Show.php
index 08f51ce08..a0cb54aa0 100644
--- a/app/Livewire/Project/Shared/Storages/Show.php
+++ b/app/Livewire/Project/Shared/Storages/Show.php
@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Shared\Storages;
use App\Models\LocalPersistentVolume;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class Show extends Component
@@ -36,8 +38,13 @@ class Show extends Component
$this->dispatch('success', 'Storage updated successfully');
}
- public function delete()
+ public function delete($password)
{
+ if (!Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+ return;
+ }
+
$this->storage->delete();
$this->dispatch('refreshStorages');
}
diff --git a/app/Livewire/Server/Delete.php b/app/Livewire/Server/Delete.php
index 3beec0c91..08e91a4c7 100644
--- a/app/Livewire/Server/Delete.php
+++ b/app/Livewire/Server/Delete.php
@@ -4,6 +4,8 @@ namespace App\Livewire\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\Auth;
class Delete extends Component
{
@@ -11,8 +13,12 @@ class Delete extends Component
public $server;
- public function delete()
+ public function delete($password)
{
+ if (!Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+ return;
+ }
try {
$this->authorize('delete', $this->server);
if ($this->server->hasDefinedResources()) {
diff --git a/app/Livewire/Server/Proxy/Deploy.php b/app/Livewire/Server/Proxy/Deploy.php
index 2279951ee..f7318c8f4 100644
--- a/app/Livewire/Server/Proxy/Deploy.php
+++ b/app/Livewire/Server/Proxy/Deploy.php
@@ -7,6 +7,8 @@ use App\Actions\Proxy\StartProxy;
use App\Events\ProxyStatusChanged;
use App\Models\Server;
use Livewire\Component;
+use Illuminate\Support\Facades\Process;
+use Illuminate\Process\InvokedProcess;
class Deploy extends Component
{
@@ -94,21 +96,43 @@ class Deploy extends Component
public function stop(bool $forceStop = true)
{
try {
- if ($this->server->isSwarm()) {
- instant_remote_process([
- 'docker service rm coolify-proxy_traefik',
- ], $this->server);
- } else {
- instant_remote_process([
- 'docker rm -f coolify-proxy',
- ], $this->server);
+ $containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
+ $timeout = 30;
+
+ $process = $this->stopContainer($containerName, $timeout);
+
+ $startTime = time();
+ while ($process->running()) {
+ if (time() - $startTime >= $timeout) {
+ $this->forceStopContainer($containerName);
+ break;
+ }
+ usleep(100000);
}
- $this->server->proxy->status = 'exited';
- $this->server->proxy->force_stop = $forceStop;
- $this->server->save();
- $this->dispatch('proxyStatusUpdated');
+
+ $this->removeContainer($containerName);
} catch (\Throwable $e) {
return handleError($e, $this);
+ } finally {
+ $this->server->proxy->force_stop = $forceStop;
+ $this->server->proxy->status = 'exited';
+ $this->server->save();
+ $this->dispatch('proxyStatusUpdated');
}
}
+
+ private function stopContainer(string $containerName, int $timeout): InvokedProcess
+ {
+ return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+ }
+
+ private function forceStopContainer(string $containerName)
+ {
+ instant_remote_process(["docker kill $containerName"], $this->server, throwError: false);
+ }
+
+ private function removeContainer(string $containerName)
+ {
+ instant_remote_process(["docker rm -f $containerName"], $this->server, throwError: false);
+ }
}
diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php
index 97d4fcdbf..ee5e673a5 100644
--- a/app/Livewire/Team/AdminView.php
+++ b/app/Livewire/Team/AdminView.php
@@ -5,6 +5,8 @@ namespace App\Livewire\Team;
use App\Models\Team;
use App\Models\User;
use Livewire\Component;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
class AdminView extends Component
{
@@ -73,8 +75,12 @@ class AdminView extends Component
$team->delete();
}
- public function delete($id)
+ public function delete($id, $password)
{
+ if (!Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+ return;
+ }
if (! auth()->user()->isInstanceAdmin()) {
return $this->dispatch('error', 'You are not authorized to delete users');
}
diff --git a/app/Models/Application.php b/app/Models/Application.php
index d0cc34a06..8c7520eaf 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -8,6 +8,8 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
+use Illuminate\Support\Facades\Process;
+use Illuminate\Process\InvokedProcess;
use OpenApi\Attributes as OA;
use RuntimeException;
use Spatie\Activitylog\Models\Activity;
@@ -149,13 +151,65 @@ class Application extends BaseModel
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
}
+ public function getContainersToStop(bool $previewDeployments = false): array
+ {
+ $containers = $previewDeployments
+ ? getCurrentApplicationContainerStatus($this->destination->server, $this->id, includePullrequests: true)
+ : getCurrentApplicationContainerStatus($this->destination->server, $this->id, 0);
+
+ return $containers->pluck('Names')->toArray();
+ }
+
+ public function stopContainers(array $containerNames, $server, int $timeout = 600)
+ {
+ $processes = [];
+ foreach ($containerNames as $containerName) {
+ $processes[$containerName] = $this->stopContainer($containerName, $server, $timeout);
+ }
+
+ $startTime = time();
+ while (count($processes) > 0) {
+ $finishedProcesses = array_filter($processes, function ($process) {
+ return !$process->running();
+ });
+ foreach ($finishedProcesses as $containerName => $process) {
+ unset($processes[$containerName]);
+ $this->removeContainer($containerName, $server);
+ }
+
+ if (time() - $startTime >= $timeout) {
+ $this->forceStopRemainingContainers(array_keys($processes), $server);
+ break;
+ }
+
+ usleep(100000);
+ }
+ }
+
+ public function stopContainer(string $containerName, $server, int $timeout): InvokedProcess
+ {
+ return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+ }
+
+ public function removeContainer(string $containerName, $server)
+ {
+ instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
+ }
+
+ public function forceStopRemainingContainers(array $containerNames, $server)
+ {
+ foreach ($containerNames as $containerName) {
+ instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
+ $this->removeContainer($containerName, $server);
+ }
+ }
+
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
- ray('Deleting workdir');
- instant_remote_process(['rm -rf '.$this->workdir()], $server, false);
+ instant_remote_process(['rm -rf ' . $this->workdir()], $server, false);
}
}
@@ -176,6 +230,14 @@ class Application extends BaseModel
}
}
+ public function delete_connected_networks($uuid)
+ {
+ $server = data_get($this, 'destination.server');
+ instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
+ instant_remote_process(["docker network rm {$uuid}"], $server, false);
+ }
+
+
public function additional_servers()
{
return $this->belongsToMany(Server::class, 'additional_destinations')
@@ -283,7 +345,7 @@ class Application extends BaseModel
public function publishDirectory(): Attribute
{
return Attribute::make(
- set: fn ($value) => $value ? '/'.ltrim($value, '/') : null,
+ set: fn ($value) => $value ? '/' . ltrim($value, '/') : null,
);
}
@@ -291,7 +353,7 @@ class Application extends BaseModel
{
return Attribute::make(
get: function () {
- if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) {
+ if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
if (str($this->git_repository)->contains('bitbucket')) {
return "{$this->source->html_url}/{$this->git_repository}/src/{$this->git_branch}";
}
@@ -318,7 +380,7 @@ class Application extends BaseModel
{
return Attribute::make(
get: function () {
- if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) {
+ if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
return "{$this->source->html_url}/{$this->git_repository}/settings/hooks";
}
// Convert the SSH URL to HTTPS URL
@@ -337,7 +399,7 @@ class Application extends BaseModel
{
return Attribute::make(
get: function () {
- if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) {
+ if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
return "{$this->source->html_url}/{$this->git_repository}/commits/{$this->git_branch}";
}
// Convert the SSH URL to HTTPS URL
@@ -354,7 +416,7 @@ class Application extends BaseModel
public function gitCommitLink($link): string
{
- if (! is_null(data_get($this, 'source.html_url')) && ! is_null(data_get($this, 'git_repository')) && ! is_null(data_get($this, 'git_branch'))) {
+ if (!is_null(data_get($this, 'source.html_url')) && !is_null(data_get($this, 'git_repository')) && !is_null(data_get($this, 'git_branch'))) {
if (str($this->source->html_url)->contains('bitbucket')) {
return "{$this->source->html_url}/{$this->git_repository}/commits/{$link}";
}
@@ -365,7 +427,7 @@ class Application extends BaseModel
$git_repository = str_replace('.git', '', $this->git_repository);
$url = Url::fromString($git_repository);
$url = $url->withUserInfo('');
- $url = $url->withPath($url->getPath().'/commits/'.$link);
+ $url = $url->withPath($url->getPath() . '/commits/' . $link);
return $url->__toString();
}
@@ -418,7 +480,7 @@ class Application extends BaseModel
public function baseDirectory(): Attribute
{
return Attribute::make(
- set: fn ($value) => '/'.ltrim($value, '/'),
+ set: fn ($value) => '/' . ltrim($value, '/'),
);
}
@@ -761,7 +823,7 @@ class Application extends BaseModel
public function workdir()
{
- return application_configuration_dir()."/{$this->uuid}";
+ return application_configuration_dir() . "/{$this->uuid}";
}
public function isLogDrainEnabled()
@@ -771,7 +833,7 @@ class Application extends BaseModel
public function isConfigurationChanged(bool $save = false)
{
- $newConfigHash = $this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect;
+ $newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->ports_exposes . $this->ports_mappings . $this->base_directory . $this->publish_directory . $this->dockerfile . $this->dockerfile_location . $this->custom_labels . $this->custom_docker_run_options . $this->dockerfile_target_build . $this->redirect;
if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
} else {
@@ -825,7 +887,7 @@ class Application extends BaseModel
public function dirOnServer()
{
- return application_configuration_dir()."/{$this->uuid}";
+ return application_configuration_dir() . "/{$this->uuid}";
}
public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false)
@@ -871,7 +933,7 @@ class Application extends BaseModel
if ($this->source->is_public) {
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}";
- if (! $only_checkout) {
+ if (!$only_checkout) {
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true);
}
if ($exec_in_docker) {
@@ -888,7 +950,7 @@ class Application extends BaseModel
$git_clone_command = "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository} {$baseDir}";
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
}
- if (! $only_checkout) {
+ if (!$only_checkout) {
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: false);
}
if ($exec_in_docker) {
@@ -949,7 +1011,7 @@ class Application extends BaseModel
} else {
$commands->push("echo 'Checking out $branch'");
}
- $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
+ $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} elseif ($git_type === 'github' || $git_type === 'gitea') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
@@ -957,14 +1019,14 @@ class Application extends BaseModel
} else {
$commands->push("echo 'Checking out $branch'");
}
- $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
+ $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} elseif ($git_type === 'bitbucket') {
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
- $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit);
+ $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit);
}
}
@@ -993,7 +1055,7 @@ class Application extends BaseModel
} else {
$commands->push("echo 'Checking out $branch'");
}
- $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
+ $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} elseif ($git_type === 'github' || $git_type === 'gitea') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
@@ -1001,14 +1063,14 @@ class Application extends BaseModel
} else {
$commands->push("echo 'Checking out $branch'");
}
- $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
+ $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} elseif ($git_type === 'bitbucket') {
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
- $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit);
+ $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit);
}
}
@@ -1060,20 +1122,20 @@ class Application extends BaseModel
}
if ($source->startsWith('.')) {
$source = $source->after('.');
- $source = $workdir.$source;
+ $source = $workdir . $source;
}
$commands->push("mkdir -p $source > /dev/null 2>&1 || true");
}
}
}
$labels = collect(data_get($service, 'labels', []));
- if (! $labels->contains('coolify.managed')) {
+ if (!$labels->contains('coolify.managed')) {
$labels->push('coolify.managed=true');
}
- if (! $labels->contains('coolify.applicationId')) {
- $labels->push('coolify.applicationId='.$this->id);
+ if (!$labels->contains('coolify.applicationId')) {
+ $labels->push('coolify.applicationId=' . $this->id);
}
- if (! $labels->contains('coolify.type')) {
+ if (!$labels->contains('coolify.type')) {
$labels->push('coolify.type=application');
}
data_set($service, 'labels', $labels->toArray());
@@ -1149,7 +1211,7 @@ class Application extends BaseModel
$jsonNames = $json->keys()->toArray();
$diff = array_diff($jsonNames, $names);
$json = $json->filter(function ($value, $key) use ($diff) {
- return ! in_array($key, $diff);
+ return !in_array($key, $diff);
});
if ($json) {
$this->docker_compose_domains = json_encode($json);
@@ -1166,13 +1228,12 @@ class Application extends BaseModel
} else {
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile
Check if you used the right extension (.yaml or .yml) in the compose file name.");
}
-
}
public function parseContainerLabels(?ApplicationPreview $preview = null)
{
$customLabels = data_get($this, 'custom_labels');
- if (! $customLabels) {
+ if (!$customLabels) {
return;
}
if (base64_encode(base64_decode($customLabels, true)) !== $customLabels) {
@@ -1255,10 +1316,10 @@ class Application extends BaseModel
continue;
}
if (isset($healthcheckCommand) && str_contains($trimmedLine, '\\')) {
- $healthcheckCommand .= ' '.trim($trimmedLine, '\\ ');
+ $healthcheckCommand .= ' ' . trim($trimmedLine, '\\ ');
}
- if (isset($healthcheckCommand) && ! str_contains($trimmedLine, '\\') && ! empty($healthcheckCommand)) {
- $healthcheckCommand .= ' '.$trimmedLine;
+ if (isset($healthcheckCommand) && !str_contains($trimmedLine, '\\') && !empty($healthcheckCommand)) {
+ $healthcheckCommand .= ' ' . $trimmedLine;
break;
}
}
diff --git a/app/Models/Service.php b/app/Models/Service.php
index d8def6663..80464db7d 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Process;
+use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Facades\Storage;
use OpenApi\Attributes as OA;
use Spatie\Url\Url;
@@ -68,7 +70,7 @@ class Service extends BaseModel
$databaseStorages = $this->databases()->get()->pluck('persistentStorages')->flatten()->sortBy('id');
$storages = $applicationStorages->merge($databaseStorages)->implode('updated_at');
- $newConfigHash = $images.$domains.$images.$storages;
+ $newConfigHash = $images . $domains . $images . $storages;
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
@@ -131,15 +133,80 @@ class Service extends BaseModel
return $this->morphToMany(Tag::class, 'taggable');
}
+ public function getContainersToStop(): array
+ {
+ $containersToStop = [];
+ $applications = $this->applications()->get();
+ foreach ($applications as $application) {
+ $containersToStop[] = "{$application->name}-{$this->uuid}";
+ }
+ $dbs = $this->databases()->get();
+ foreach ($dbs as $db) {
+ $containersToStop[] = "{$db->name}-{$this->uuid}";
+ }
+ return $containersToStop;
+ }
+
+ public function stopContainers(array $containerNames, $server, int $timeout = 300)
+ {
+ $processes = [];
+ foreach ($containerNames as $containerName) {
+ $processes[$containerName] = $this->stopContainer($containerName, $timeout);
+ }
+
+ $startTime = time();
+ while (count($processes) > 0) {
+ $finishedProcesses = array_filter($processes, function ($process) {
+ return !$process->running();
+ });
+ foreach (array_keys($finishedProcesses) as $containerName) {
+ unset($processes[$containerName]);
+ $this->removeContainer($containerName, $server);
+ }
+
+ if (time() - $startTime >= $timeout) {
+ $this->forceStopRemainingContainers(array_keys($processes), $server);
+ break;
+ }
+
+ usleep(100000);
+ }
+ }
+
+ public function stopContainer(string $containerName, int $timeout): InvokedProcess
+ {
+ return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+ }
+
+ public function removeContainer(string $containerName, $server)
+ {
+ instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
+ }
+
+ public function forceStopRemainingContainers(array $containerNames, $server)
+ {
+ foreach ($containerNames as $containerName) {
+ instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
+ $this->removeContainer($containerName, $server);
+ }
+ }
+
public function delete_configurations()
{
- $server = data_get($this, 'server');
+ $server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
- instant_remote_process(['rm -rf '.$this->workdir()], $server, false);
+ instant_remote_process(['rm -rf ' . $this->workdir()], $server, false);
}
}
+ public function delete_connected_networks($uuid)
+ {
+ $server = data_get($this, 'destination.server');
+ instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
+ instant_remote_process(["docker network rm {$uuid}"], $server, false);
+ }
+
public function status()
{
$applications = $this->applications;
@@ -994,7 +1061,7 @@ class Service extends BaseModel
public function workdir()
{
- return service_configuration_dir()."/{$this->uuid}";
+ return service_configuration_dir() . "/{$this->uuid}";
}
public function saveComposeConfigs()
diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php
index d312fab96..f0c78cc08 100644
--- a/app/Models/ServiceApplication.php
+++ b/app/Models/ServiceApplication.php
@@ -23,7 +23,7 @@ class ServiceApplication extends BaseModel
public function restart()
{
- $container_id = $this->name.'-'.$this->service->uuid;
+ $container_id = $this->name . '-' . $this->service->uuid;
instant_remote_process(["docker restart {$container_id}"], $this->service->server);
}
@@ -69,7 +69,7 @@ class ServiceApplication extends BaseModel
public function workdir()
{
- return service_configuration_dir()."/{$this->service->uuid}";
+ return service_configuration_dir() . "/{$this->service->uuid}";
}
public function serviceType()
diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php
index 6b96738e8..2e25f4402 100644
--- a/app/Models/ServiceDatabase.php
+++ b/app/Models/ServiceDatabase.php
@@ -21,7 +21,7 @@ class ServiceDatabase extends BaseModel
public function restart()
{
- $container_id = $this->name.'-'.$this->service->uuid;
+ $container_id = $this->name . '-' . $this->service->uuid;
remote_process(["docker restart {$container_id}"], $this->service->server);
}
@@ -88,7 +88,7 @@ class ServiceDatabase extends BaseModel
public function workdir()
{
- return service_configuration_dir()."/{$this->service->uuid}";
+ return service_configuration_dir() . "/{$this->service->uuid}";
}
public function service()
diff --git a/config/sentry.php b/config/sentry.php
index 471a1e0fc..c65d3d1ff 100644
--- a/config/sentry.php
+++ b/config/sentry.php
@@ -7,7 +7,7 @@ return [
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
- 'release' => '4.0.0-beta.341',
+ 'release' => '4.0.0-beta.342',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),
diff --git a/config/version.php b/config/version.php
index 32eb01cd0..633c71d60 100644
--- a/config/version.php
+++ b/config/version.php
@@ -1,3 +1,3 @@
Examples
For Public repositories, use https://....
For Private repositories, use git@....
https://github.com/coollabsio/coolify-examples main branch will be selected
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify branch will be selected.
https://gitea.com/sedlav/expressjs.git main branch will be selected.
https://gitlab.com/andrasbacsai/nodejs-example.git main branch will be selected."
+ "repository.url": "Examples
For Public repositories, use https://....
For Private repositories, use git@....
https://github.com/coollabsio/coolify-examples main branch will be selected
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify branch will be selected.
https://gitea.com/sedlav/expressjs.git main branch will be selected.
https://gitlab.com/andrasbacsai/nodejs-example.git main branch will be selected.",
+ "service.stop": "This service will be stopped.",
+ "resource.docker_cleanup": "Run Docker Cleanup (remove unused images and builder cache).",
+ "resource.non_persistent": "All non-persistent data will be deleted.",
+ "resource.delete_volumes": "All volumes associated with this resource will be permanently deleted.",
+ "resource.delete_connected_networks": "All non-predefined networks associated with this resource will be permanently deleted.",
+ "resource.delete_configurations": "All configuration files will be permanently deleted from the server."
}
diff --git a/resources/views/components/forms/checkbox.blade.php b/resources/views/components/forms/checkbox.blade.php
index f3e3d5c9e..84c6b7e32 100644
--- a/resources/views/components/forms/checkbox.blade.php
+++ b/resources/views/components/forms/checkbox.blade.php
@@ -1,4 +1,15 @@
+@props([
+ 'id',
+ 'label' => null,
+ 'helper' => null,
+ 'disabled' => false,
+ 'instantSave' => false,
+ 'value' => null,
+ 'hideLabel' => false,
+])
+
Warning
+This operation is permanent and cannot be undone. Please think again before proceeding! +
+{{ $confirmationLabel }}
+Final Confirmation
+Please enter your password to confirm this destructive action.
+{{ $message }}
+ @enderror +