From df796dffa2598354b7edcf3965dd00d45787eebf Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Thu, 8 Aug 2024 01:02:48 +0200 Subject: [PATCH 001/371] fix delte networks and unused images of services when deleted --- app/Actions/Service/DeleteService.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index 194cf4db9..b043082ac 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -3,6 +3,7 @@ namespace App\Actions\Service; use App\Models\Service; +use App\Actions\Server\CleanupDocker; use Lorisleiva\Actions\Concerns\AsAction; class DeleteService @@ -33,6 +34,11 @@ class DeleteService foreach ($storagesToDelete as $storage) { $commands[] = "docker volume rm -f $storage->name"; } + + $uuid = $service->uuid; + instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false); + instant_remote_process(["docker network rm {$uuid}"], $server, false); + $commands[] = "docker rm -f $service->uuid"; instant_remote_process($commands, $server, false); @@ -50,6 +56,9 @@ class DeleteService $task->delete(); } $service->tags()->detach(); + $service->forceDelete(); + + CleanupDocker::run($server, true); } } } From 070daee28e8f5c91401c37c778b919f1270042a0 Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Thu, 8 Aug 2024 01:19:17 +0200 Subject: [PATCH 002/371] remove networks and cleanup unused images when stoping dockercompose build pack containers --- app/Actions/Application/StopApplication.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index 1f05e29ac..70575c821 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -3,6 +3,7 @@ namespace App\Actions\Application; use App\Models\Application; +use App\Actions\Server\CleanupDocker; use Lorisleiva\Actions\Concerns\AsAction; class StopApplication @@ -13,7 +14,6 @@ class StopApplication { if ($application->destination->server->isSwarm()) { instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server); - return; } @@ -23,7 +23,7 @@ class StopApplication $servers->push($server); }); foreach ($servers as $server) { - if (! $server->isFunctional()) { + if (!$server->isFunctional()) { return 'Server is not functional'; } if ($previewDeployments) { @@ -44,10 +44,11 @@ class StopApplication } } 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); + + CleanupDocker::run($server, true); } } } From 4d0acee95cf32173411523429598bb21774e1ca8 Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Thu, 8 Aug 2024 12:31:37 +0200 Subject: [PATCH 003/371] UI options for deletion WIP --- app/Actions/Service/DeleteService.php | 4 +-- app/Jobs/DeleteResourceJob.php | 31 +++++++++++++++---- .../livewire/project/shared/danger.blade.php | 4 ++- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index b043082ac..7c8eaf75d 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -10,7 +10,7 @@ class DeleteService { use AsAction; - public function handle(Service $service) + public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $deleteImages, bool $deleteNetworks) { try { $server = data_get($service, 'server'); @@ -61,4 +61,4 @@ class DeleteService CleanupDocker::run($server, true); } } -} +} \ No newline at end of file diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index dbf44dd5d..36f673986 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -6,6 +6,7 @@ use App\Actions\Application\StopApplication; use App\Actions\Database\StopDatabase; use App\Actions\Service\DeleteService; use App\Actions\Service\StopService; +use App\Actions\Server\CleanupDocker; use App\Models\Application; use App\Models\Service; use App\Models\StandaloneClickhouse; @@ -31,7 +32,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 $deleteVolumes = false, + public bool $deleteImages = false, + public bool $deleteNetworks = false + ) { + } public function handle() { @@ -59,19 +64,33 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue break; } - if ($this->deleteVolumes && $this->resource->type() !== 'service') { - $this->resource?->delete_volumes($persistentStorages); - } if ($this->deleteConfigurations) { $this->resource?->delete_configurations(); } + + if ($this->deleteVolumes && $this->resource->type() !== 'service') { + $this->resource?->delete_volumes($persistentStorages); + } + + if ($this->deleteImages) { + // Logic to delete images + } + + if ($this->deleteNetworks) { + // Logic to delete networks + } + + $server = data_get($this->resource, 'server'); + if ($server) { + CleanupDocker::run($server, true); + } } catch (\Throwable $e) { ray($e->getMessage()); - send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage()); + send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage()); throw $e; } finally { $this->resource->forceDelete(); Artisan::queue('cleanup:stucked-resources'); } } -} +} \ No newline at end of file diff --git a/resources/views/livewire/project/shared/danger.blade.php b/resources/views/livewire/project/shared/danger.blade.php index 276061a8e..20bd7310b 100644 --- a/resources/views/livewire/project/shared/danger.blade.php +++ b/resources/views/livewire/project/shared/danger.blade.php @@ -12,5 +12,7 @@ + + - + \ No newline at end of file From 0135e2b5c02095aa43e67c370c70747a96cf7f83 Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 00:30:11 +0200 Subject: [PATCH 004/371] add logic --- app/Actions/Application/StopApplication.php | 3 +- app/Actions/Service/DeleteService.php | 34 +++++++--- app/Jobs/DeleteResourceJob.php | 28 ++++---- app/Livewire/Project/Shared/Danger.php | 12 +++- app/Models/Application.php | 68 +++++++++++-------- app/Models/ServiceApplication.php | 4 +- .../livewire/project/shared/danger.blade.php | 9 ++- 7 files changed, 94 insertions(+), 64 deletions(-) diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index c81e90518..73abeba7a 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -43,8 +43,7 @@ class StopApplication } if ($application->build_pack === 'dockercompose') { $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($uuid); CleanupDocker::run($server, true); } diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index 7c8eaf75d..f32a44262 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -10,14 +10,16 @@ class DeleteService { use AsAction; - public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $deleteImages, bool $deleteNetworks) + public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $deleteImages, bool $deleteConnectedNetworks) { try { $server = data_get($service, 'server'); + if ($server->isFunctional()) { $storagesToDelete = collect([]); $service->environment_variables()->delete(); + $commands = []; foreach ($service->applications()->get() as $application) { $storages = $application->persistentStorages()->get(); @@ -31,21 +33,34 @@ class DeleteService $storagesToDelete->push($storage); } } - foreach ($storagesToDelete as $storage) { - $commands[] = "docker volume rm -f $storage->name"; + + // Delete volumes if the flag is set + if ($deleteVolumes) { + foreach ($service->applications()->get() as $application) { + $persistentStorages = $application->persistentStorages()->get(); + $application->delete_volumes($persistentStorages); + } } - $uuid = $service->uuid; - instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false); - instant_remote_process(["docker network rm {$uuid}"], $server, false); + // Delete networks if the flag is set + if ($deleteConnectedNetworks) { + $uuid = $service->uuid; + $service->delete_connected_networks($uuid); + } + // Command to remove the service itself $commands[] = "docker rm -f $service->uuid"; + // Execute all commands instant_remote_process($commands, $server, false); } } catch (\Exception $e) { throw new \Exception($e->getMessage()); } finally { + // Delete configurations if the flag is set + if ($deleteConfigurations) { + $service->delete_configurations(); + } foreach ($service->applications()->get() as $application) { $application->forceDelete(); } @@ -58,7 +73,10 @@ class DeleteService $service->tags()->detach(); $service->forceDelete(); - CleanupDocker::run($server, true); + // Run cleanup if images need to be deleted + if ($deleteImages) { + CleanupDocker::run($server, true); + } } } -} \ No newline at end of file +} diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index 36f673986..68036ee4a 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -31,10 +31,10 @@ 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 $deleteImages = false, - public bool $deleteNetworks = false + public bool $deleteConfigurations, + public bool $deleteVolumes, + public bool $deleteImages, + public bool $deleteConnectedNetworks ) { } @@ -60,7 +60,7 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue break; case 'service': StopService::run($this->resource); - DeleteService::run($this->resource); + DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->deleteImages, $this->deleteConnectedNetworks); break; } @@ -72,20 +72,16 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue $this->resource?->delete_volumes($persistentStorages); } - if ($this->deleteImages) { - // Logic to delete images - } - - if ($this->deleteNetworks) { - // Logic to delete networks - } - $server = data_get($this->resource, 'server'); - if ($server) { + if ($this->deleteImages && $server) { CleanupDocker::run($server, true); } + + if ($this->deleteConnectedNetworks) { + $uuid = $this->resource->uuid; // Get the UUID from the resource + $this->resource?->delete_connected_networks($uuid); // Pass the UUID to the method + } } catch (\Throwable $e) { - ray($e->getMessage()); send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage()); throw $e; } finally { @@ -93,4 +89,4 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue Artisan::queue('cleanup:stucked-resources'); } } -} \ No newline at end of file +} diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php index 5f0178be4..cff1d453a 100644 --- a/app/Livewire/Project/Shared/Danger.php +++ b/app/Livewire/Project/Shared/Danger.php @@ -18,6 +18,10 @@ class Danger extends Component public bool $delete_volumes = true; + public bool $delete_images = true; + + public bool $delete_connected_networks = true; + public ?string $modalId = null; public function mount() @@ -33,7 +37,13 @@ class Danger extends Component 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->delete_images, + $this->delete_connected_networks + ); return redirect()->route('project.resource.index', [ 'project_uuid' => $this->projectUuid, diff --git a/app/Models/Application.php b/app/Models/Application.php index e2871da4b..324713bbf 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -152,7 +152,7 @@ class Application extends BaseModel $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); } } @@ -173,6 +173,15 @@ class Application extends BaseModel } } + public function delete_connected_networks($uuid) + { + $server = data_get($this, 'destination.server'); + ray($uuid); + 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') @@ -280,7 +289,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, ); } @@ -288,7 +297,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}"; } @@ -315,7 +324,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 @@ -334,7 +343,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 @@ -351,7 +360,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}"; } @@ -362,7 +371,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(); } @@ -432,7 +441,7 @@ class Application extends BaseModel public function baseDirectory(): Attribute { return Attribute::make( - set: fn ($value) => '/'.ltrim($value, '/'), + set: fn ($value) => '/' . ltrim($value, '/'), ); } @@ -775,7 +784,7 @@ class Application extends BaseModel public function workdir() { - return application_configuration_dir()."/{$this->uuid}"; + return application_configuration_dir() . "/{$this->uuid}"; } public function isLogDrainEnabled() @@ -785,7 +794,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 { @@ -839,7 +848,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) @@ -885,7 +894,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) { @@ -902,7 +911,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) { @@ -963,7 +972,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) { @@ -971,14 +980,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); } } @@ -1007,7 +1016,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) { @@ -1015,14 +1024,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); } } @@ -1074,20 +1083,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()); @@ -1161,7 +1170,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); @@ -1178,13 +1187,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) { @@ -1267,10 +1275,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/ServiceApplication.php b/app/Models/ServiceApplication.php index 6690f254e..9df825869 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); } @@ -59,7 +59,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/resources/views/livewire/project/shared/danger.blade.php b/resources/views/livewire/project/shared/danger.blade.php index 20bd7310b..f9eaec30f 100644 --- a/resources/views/livewire/project/shared/danger.blade.php +++ b/resources/views/livewire/project/shared/danger.blade.php @@ -9,10 +9,9 @@
This resource will be deleted. It is not reversible. Please think again.

Actions

- - - - + + + + \ No newline at end of file From 70aa05bde96a7a9a26d40903fc3dc81d3ba6e074 Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 00:42:23 +0200 Subject: [PATCH 005/371] order in importance --- resources/views/livewire/project/shared/danger.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/livewire/project/shared/danger.blade.php b/resources/views/livewire/project/shared/danger.blade.php index f9eaec30f..6d00f357c 100644 --- a/resources/views/livewire/project/shared/danger.blade.php +++ b/resources/views/livewire/project/shared/danger.blade.php @@ -9,9 +9,9 @@
This resource will be deleted. It is not reversible. Please think again.

Actions

- - + + \ No newline at end of file From 51071da7006c7b15cc8f6828a8586d6678a072a4 Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 01:00:07 +0200 Subject: [PATCH 006/371] fix order --- app/Jobs/DeleteResourceJob.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index 68036ee4a..50e0e646e 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -64,13 +64,12 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue break; } - if ($this->deleteConfigurations) { - $this->resource?->delete_configurations(); - } - if ($this->deleteVolumes && $this->resource->type() !== 'service') { $this->resource?->delete_volumes($persistentStorages); } + if ($this->deleteConfigurations) { + $this->resource?->delete_configurations(); + } $server = data_get($this->resource, 'server'); if ($this->deleteImages && $server) { From 86a087056e289873b474dacf0dd063b2b5e285cd Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 02:11:42 +0200 Subject: [PATCH 007/371] fix volume deletion for services --- app/Actions/Service/DeleteService.php | 46 ++++++++++--------- .../livewire/project/shared/danger.blade.php | 2 +- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index f32a44262..13b5cc50b 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -14,12 +14,10 @@ class DeleteService { try { $server = data_get($service, 'server'); - - if ($server->isFunctional()) { + if ($deleteVolumes && $server->isFunctional()) { $storagesToDelete = collect([]); $service->environment_variables()->delete(); - $commands = []; foreach ($service->applications()->get() as $application) { $storages = $application->persistentStorages()->get(); @@ -33,27 +31,33 @@ class DeleteService $storagesToDelete->push($storage); } } - - // Delete volumes if the flag is set - if ($deleteVolumes) { - foreach ($service->applications()->get() as $application) { - $persistentStorages = $application->persistentStorages()->get(); - $application->delete_volumes($persistentStorages); - } + foreach ($storagesToDelete as $storage) { + $commands[] = "docker volume rm -f $storage->name"; } - // Delete networks if the flag is set - if ($deleteConnectedNetworks) { - $uuid = $service->uuid; - $service->delete_connected_networks($uuid); - } - - // Command to remove the service itself - $commands[] = "docker rm -f $service->uuid"; - // Execute all commands - instant_remote_process($commands, $server, false); + if (!empty($commands)) { + foreach ($commands as $command) { + $result = instant_remote_process([$command], $server, false); + if ($result !== 0) { + ray("Failed to execute: $command"); + } + } + } } + + // Delete networks if the flag is set + if ($deleteConnectedNetworks) { + $uuid = $service->uuid; + $service->delete_connected_networks($uuid); + } + + // Command to remove the service itself + $commands[] = "docker rm -f $service->uuid"; + + // Execute all commands + instant_remote_process($commands, $server, false); + } catch (\Exception $e) { throw new \Exception($e->getMessage()); } finally { @@ -79,4 +83,4 @@ class DeleteService } } } -} +} \ No newline at end of file diff --git a/resources/views/livewire/project/shared/danger.blade.php b/resources/views/livewire/project/shared/danger.blade.php index 6d00f357c..174731eea 100644 --- a/resources/views/livewire/project/shared/danger.blade.php +++ b/resources/views/livewire/project/shared/danger.blade.php @@ -10,7 +10,7 @@ again.

Actions

- + From e67e03f73f2c8405b9bb96d6c1adb3f27c1b7e48 Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 02:15:40 +0200 Subject: [PATCH 008/371] added comments and removed temp ones --- app/Actions/Service/DeleteService.php | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index 13b5cc50b..521a70e83 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -35,7 +35,7 @@ class DeleteService $commands[] = "docker volume rm -f $storage->name"; } - // Execute all commands + // 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); @@ -46,22 +46,18 @@ class DeleteService } } - // Delete networks if the flag is set if ($deleteConnectedNetworks) { $uuid = $service->uuid; $service->delete_connected_networks($uuid); } - // Command to remove the service itself $commands[] = "docker rm -f $service->uuid"; - // Execute all commands + // Executing remaining commands instant_remote_process($commands, $server, false); - } catch (\Exception $e) { throw new \Exception($e->getMessage()); } finally { - // Delete configurations if the flag is set if ($deleteConfigurations) { $service->delete_configurations(); } @@ -77,10 +73,9 @@ class DeleteService $service->tags()->detach(); $service->forceDelete(); - // Run cleanup if images need to be deleted if ($deleteImages) { CleanupDocker::run($server, true); } } } -} \ No newline at end of file +} From 7722809c52a04f052bfd1d2632daaf88ca0527a7 Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 02:20:32 +0200 Subject: [PATCH 009/371] typo --- resources/views/livewire/project/shared/danger.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/project/shared/danger.blade.php b/resources/views/livewire/project/shared/danger.blade.php index 174731eea..1e1365749 100644 --- a/resources/views/livewire/project/shared/danger.blade.php +++ b/resources/views/livewire/project/shared/danger.blade.php @@ -10,7 +10,7 @@ again.

Actions

- + From 53dff4ca4f6f34f2d47de2db4e9c960275da1a34 Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 02:58:59 +0200 Subject: [PATCH 010/371] simplify uuid variabel --- app/Actions/Service/DeleteService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index 521a70e83..186200993 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -47,14 +47,14 @@ class DeleteService } if ($deleteConnectedNetworks) { - $uuid = $service->uuid; - $service->delete_connected_networks($uuid); + $service->delete_connected_networks($service->uuid); } $commands[] = "docker rm -f $service->uuid"; // Executing remaining commands instant_remote_process($commands, $server, false); + } catch (\Exception $e) { throw new \Exception($e->getMessage()); } finally { From d980c7a4254900d4c98fefd84f2cc257e2128356 Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 02:59:41 +0200 Subject: [PATCH 011/371] only run network removal on stop service if it is not a deletion operation --- app/Actions/Service/StopService.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index 3dd91b4e2..e7d108528 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -9,14 +9,14 @@ class StopService { use AsAction; - public function handle(Service $service) + public function handle(Service $service, bool $isDeleteOperation = false) { try { $server = $service->destination->server; - if (! $server->isFunctional()) { + if (!$server->isFunctional()) { return 'Server is not functional'; } - ray('Stopping service: '.$service->name); + ray('Stopping service: ' . $service->name); $applications = $service->applications()->get(); foreach ($applications as $application) { instant_remote_process(command: ["docker stop --time=30 {$application->name}-{$service->uuid}"], server: $server, throwError: false); @@ -31,13 +31,15 @@ class StopService 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); + + if (!$isDeleteOperation) { + // Only run this if not a delete operation + $service->delete_connected_networks($service->uuid); + } } catch (\Exception $e) { ray($e->getMessage()); return $e->getMessage(); } - } } From 97c2bedda27f9078c794ac952332681f1b2e5024 Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 03:00:30 +0200 Subject: [PATCH 012/371] add delete_connected_networks function to services.php --- app/Models/Application.php | 2 -- app/Models/Service.php | 15 +++++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/Models/Application.php b/app/Models/Application.php index 324713bbf..d88e94e19 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -151,7 +151,6 @@ class Application extends BaseModel $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); } } @@ -176,7 +175,6 @@ class Application extends BaseModel public function delete_connected_networks($uuid) { $server = data_get($this, 'destination.server'); - ray($uuid); instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false); instant_remote_process(["docker network rm {$uuid}"], $server, false); } diff --git a/app/Models/Service.php b/app/Models/Service.php index 33238281e..f56d05af1 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -56,7 +56,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'); @@ -121,13 +121,20 @@ class Service extends BaseModel 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; @@ -907,7 +914,7 @@ class Service extends BaseModel public function workdir() { - return service_configuration_dir()."/{$this->uuid}"; + return service_configuration_dir() . "/{$this->uuid}"; } public function saveComposeConfigs() From 5595853379307589cf0b50dc4c573028ded651df Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 03:03:40 +0200 Subject: [PATCH 013/371] WIP database network, image removal --- app/Jobs/DeleteResourceJob.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index 50e0e646e..6e102266a 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -57,9 +57,19 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue case 'standalone-clickhouse': $persistentStorages = $this->resource?->persistentStorages()?->get(); StopDatabase::run($this->resource); + // TODO + // DBs do not have a network normally? + //if ($this->deleteConnectedNetworks) { + // $this->resource?->delete_connected_networks($this->resource->uuid); + // } + // } + // $server = data_get($this->resource, 'server'); + // if ($this->deleteImages && $server) { + // CleanupDocker::run($server, true); + // } break; case 'service': - StopService::run($this->resource); + StopService::run($this->resource, true); DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->deleteImages, $this->deleteConnectedNetworks); break; } @@ -77,8 +87,7 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue } if ($this->deleteConnectedNetworks) { - $uuid = $this->resource->uuid; // Get the UUID from the resource - $this->resource?->delete_connected_networks($uuid); // Pass the UUID to the method + $this->resource?->delete_connected_networks($this->resource->uuid); } } catch (\Throwable $e) { send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage()); From d177e49e62abfee19f4da56f8c01b6b29ab3974f Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 18:43:13 +0200 Subject: [PATCH 014/371] updated warning message and formating --- .../livewire/project/service/navbar.blade.php | 208 +++++++++--------- 1 file changed, 100 insertions(+), 108 deletions(-) diff --git a/resources/views/livewire/project/service/navbar.blade.php b/resources/views/livewire/project/service/navbar.blade.php index 125f9121a..67d6a78ca 100644 --- a/resources/views/livewire/project/service/navbar.blade.php +++ b/resources/views/livewire/project/service/navbar.blade.php @@ -10,130 +10,122 @@ @script - + @endscript From 72bcf03cbb942b49891e4683b3479cbf5441382d Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 18:59:41 +0200 Subject: [PATCH 015/371] graceful service container stop --- app/Actions/Service/StopService.php | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index e7d108528..933eca7d4 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -3,6 +3,7 @@ namespace App\Actions\Service; use App\Models\Service; +use App\Actions\Server\CleanupDocker; use Lorisleiva\Actions\Concerns\AsAction; class StopService @@ -19,27 +20,38 @@ class StopService ray('Stopping service: ' . $service->name); $applications = $service->applications()->get(); foreach ($applications as $application) { - instant_remote_process(command: ["docker stop --time=30 {$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); + $this->stopContainer("{$application->name}-{$service->uuid}", $server, 600); $application->update(['status' => 'exited']); } $dbs = $service->databases()->get(); foreach ($dbs as $db) { - instant_remote_process(command: ["docker stop --time=30 {$db->name}-{$service->uuid}"], server: $server, throwError: false); - 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); + $this->stopContainer("{$db->name}-{$service->uuid}", $server, 600); $db->update(['status' => 'exited']); } if (!$isDeleteOperation) { - // Only run this if not a delete operation + // Only run if not a deletion operation as for deletion we can specify if we want to delete networks or not $service->delete_connected_networks($service->uuid); + CleanupDocker::run($server, true); } } catch (\Exception $e) { ray($e->getMessage()); - return $e->getMessage(); } } + + private function stopContainer(string $containerName, $server, int $timeout = 600) + { + try { + instant_remote_process(command: ["docker stop --time=$timeout $containerName"], server: $server, throwError: false); + $isRunning = instant_remote_process(command: ["docker inspect -f '{{.State.Running}}' $containerName"], server: $server, throwError: false); + + if (trim($isRunning) === 'true') { + instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false); + } + } catch (\Exception $error) { + } + + instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false); + } } From 1cfddfd529025e8623cd8ca9b2cdba334fbf8a3f Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 19:17:58 +0200 Subject: [PATCH 016/371] fix stop large amount of containers --- app/Actions/Service/StopService.php | 76 +++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index 933eca7d4..035781885 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -5,6 +5,8 @@ namespace App\Actions\Service; use App\Models\Service; use App\Actions\Server\CleanupDocker; use Lorisleiva\Actions\Concerns\AsAction; +use Illuminate\Support\Facades\Process; +use Illuminate\Process\InvokedProcess; class StopService { @@ -18,19 +20,12 @@ class StopService return 'Server is not functional'; } ray('Stopping service: ' . $service->name); - $applications = $service->applications()->get(); - foreach ($applications as $application) { - $this->stopContainer("{$application->name}-{$service->uuid}", $server, 600); - $application->update(['status' => 'exited']); - } - $dbs = $service->databases()->get(); - foreach ($dbs as $db) { - $this->stopContainer("{$db->name}-{$service->uuid}", $server, 600); - $db->update(['status' => 'exited']); - } + + $containersToStop = $this->getContainersToStop($service); + + $this->stopContainers($containersToStop, $server); if (!$isDeleteOperation) { - // Only run if not a deletion operation as for deletion we can specify if we want to delete networks or not $service->delete_connected_networks($service->uuid); CleanupDocker::run($server, true); } @@ -40,18 +35,61 @@ class StopService } } - private function stopContainer(string $containerName, $server, int $timeout = 600) + private function getContainersToStop(Service $service): array { - try { - instant_remote_process(command: ["docker stop --time=$timeout $containerName"], server: $server, throwError: false); - $isRunning = instant_remote_process(command: ["docker inspect -f '{{.State.Running}}' $containerName"], server: $server, throwError: false); + $containersToStop = []; + $applications = $service->applications()->get(); + foreach ($applications as $application) { + $containersToStop[] = "{$application->name}-{$service->uuid}"; + } + $dbs = $service->databases()->get(); + foreach ($dbs as $db) { + $containersToStop[] = "{$db->name}-{$service->uuid}"; + } + return $containersToStop; + } - if (trim($isRunning) === 'true') { - instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false); - } - } catch (\Exception $error) { + private function stopContainers(array $containerNames, $server, int $timeout = 300) + { + $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); + } + } + + private function stopContainer(string $containerName, $server, int $timeout): InvokedProcess + { + return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); + } + + private function removeContainer(string $containerName, $server) + { instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false); } + + private function forceStopRemainingContainers(array $containerNames, $server) + { + foreach ($containerNames as $containerName) { + instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false); + $this->removeContainer($containerName, $server); + } + } } From a4bb87d13b5ac132c634feec986537aab93b3ad1 Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 19:22:14 +0200 Subject: [PATCH 017/371] simplify DeleteService.php --- app/Actions/Service/DeleteService.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index 186200993..238e6b954 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -50,11 +50,7 @@ class DeleteService $service->delete_connected_networks($service->uuid); } - $commands[] = "docker rm -f $service->uuid"; - - // Executing remaining commands - instant_remote_process($commands, $server, false); - + instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false); } catch (\Exception $e) { throw new \Exception($e->getMessage()); } finally { From 450351921eeae0f98f6b2ba157a16b8cf83e561b Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 19:39:21 +0200 Subject: [PATCH 018/371] added public functions --- app/Models/Application.php | 55 ++++++++++++++++++++++++++++++++++++++ app/Models/Service.php | 55 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/app/Models/Application.php b/app/Models/Application.php index d88e94e19..2873ee7da 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; @@ -146,6 +148,59 @@ 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'); diff --git a/app/Models/Service.php b/app/Models/Service.php index f56d05af1..14213ee9a 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 OpenApi\Attributes as OA; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; @@ -119,6 +121,59 @@ class Service extends BaseModel return $this->morphToMany(Tag::class, 'taggable'); } + 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'); From 41be1f7666c67ce53afdf994427a9489d6d5fe96 Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 19:39:43 +0200 Subject: [PATCH 019/371] use public function --- app/Actions/Application/StopApplication.php | 45 +++++--------- app/Actions/Service/StopService.php | 65 +-------------------- 2 files changed, 18 insertions(+), 92 deletions(-) diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index 73abeba7a..5f5846f55 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -12,41 +12,28 @@ class StopApplication public function handle(Application $application, bool $previewDeployments = false) { - 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) { + 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); - } - } - } - if ($application->build_pack === 'dockercompose') { - $uuid = $application->uuid; - $application->delete_connected_networks($uuid); + 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') { + $application->delete_connected_networks($application->uuid); CleanupDocker::run($server, true); } + } catch (\Exception $e) { + ray($e->getMessage()); + return $e->getMessage(); } } } diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index 035781885..6b348f830 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -5,8 +5,6 @@ namespace App\Actions\Service; use App\Models\Service; use App\Actions\Server\CleanupDocker; use Lorisleiva\Actions\Concerns\AsAction; -use Illuminate\Support\Facades\Process; -use Illuminate\Process\InvokedProcess; class StopService { @@ -21,9 +19,8 @@ class StopService } ray('Stopping service: ' . $service->name); - $containersToStop = $this->getContainersToStop($service); - - $this->stopContainers($containersToStop, $server); + $containersToStop = $service->getContainersToStop(); + $service->stopContainers($containersToStop, $server); if (!$isDeleteOperation) { $service->delete_connected_networks($service->uuid); @@ -34,62 +31,4 @@ class StopService return $e->getMessage(); } } - - private function getContainersToStop(Service $service): array - { - $containersToStop = []; - $applications = $service->applications()->get(); - foreach ($applications as $application) { - $containersToStop[] = "{$application->name}-{$service->uuid}"; - } - $dbs = $service->databases()->get(); - foreach ($dbs as $db) { - $containersToStop[] = "{$db->name}-{$service->uuid}"; - } - return $containersToStop; - } - - private function stopContainers(array $containerNames, $server, int $timeout = 300) - { - $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); - } - } - - private function stopContainer(string $containerName, $server, int $timeout): InvokedProcess - { - return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); - } - - private function removeContainer(string $containerName, $server) - { - instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false); - } - - private function forceStopRemainingContainers(array $containerNames, $server) - { - foreach ($containerNames as $containerName) { - instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false); - $this->removeContainer($containerName, $server); - } - } } From 2ca6ffb84e59d3221fb5a6c4d320fae5c091988d Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 19:57:53 +0200 Subject: [PATCH 020/371] fix public function service.php --- app/Actions/Service/StopService.php | 2 +- app/Models/Service.php | 25 +++++++++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index 6b348f830..85c61dc83 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -31,4 +31,4 @@ class StopService return $e->getMessage(); } } -} +} \ No newline at end of file diff --git a/app/Models/Service.php b/app/Models/Service.php index 14213ee9a..64796283c 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -121,20 +121,25 @@ class Service extends BaseModel return $this->morphToMany(Tag::class, 'taggable'); } - public function getContainersToStop(bool $previewDeployments = false): array + public function getContainersToStop(): array { - $containers = $previewDeployments - ? getCurrentApplicationContainerStatus($this->destination->server, $this->id, includePullrequests: true) - : getCurrentApplicationContainerStatus($this->destination->server, $this->id, 0); - - return $containers->pluck('Names')->toArray(); + $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 = 600) + public function stopContainers(array $containerNames, $server, int $timeout = 300) { $processes = []; foreach ($containerNames as $containerName) { - $processes[$containerName] = $this->stopContainer($containerName, $server, $timeout); + $processes[$containerName] = $this->stopContainer($containerName, $timeout); } $startTime = time(); @@ -142,7 +147,7 @@ class Service extends BaseModel $finishedProcesses = array_filter($processes, function ($process) { return !$process->running(); }); - foreach ($finishedProcesses as $containerName => $process) { + foreach (array_keys($finishedProcesses) as $containerName) { unset($processes[$containerName]); $this->removeContainer($containerName, $server); } @@ -156,7 +161,7 @@ class Service extends BaseModel } } - public function stopContainer(string $containerName, $server, int $timeout): InvokedProcess + public function stopContainer(string $containerName, int $timeout): InvokedProcess { return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); } From c566152f3777a53e4e86dd7713a08599e889f3a5 Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 20:28:57 +0200 Subject: [PATCH 021/371] improve graceful_shutdown_container --- app/Jobs/ApplicationDeploymentJob.php | 45 ++++++++++++++++++++------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 473cbc679..42c1ba43c 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -28,6 +28,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; @@ -894,7 +895,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $envs->push("COOLIFY_FQDN={$this->application->fqdn}"); } if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) { - $url = str($this->application->fqdn)->replace('http://', '')->replace('https://', ''); + $url = str_replace('http://', '', $this->application->fqdn); + $url = str_replace('https://', '', $url); $envs->push("COOLIFY_URL={$url}"); } if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { @@ -2027,24 +2029,43 @@ 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] ); - } private function stop_running_container(bool $force = false) @@ -2057,7 +2078,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) { From 16a5c601e3f93d8a8fc43672958299f6bacbcd7a Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 22:15:45 +0200 Subject: [PATCH 022/371] graceful db stop and db deletion fixes --- app/Actions/Database/StopDatabase.php | 50 ++++++++++++++++++++++++--- app/Actions/Service/StopService.php | 2 +- app/Jobs/DeleteResourceJob.php | 16 ++------- 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index d562ec56f..29ce794cf 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -10,25 +10,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; +use App\Actions\Server\CleanupDocker; 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) { $server = $database->destination->server; - if (! $server->isFunctional()) { + 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) { + $this->deleteConnectedNetworks($database->uuid, $server); //Probably not needed as DBs do not have a network normally + 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/StopService.php b/app/Actions/Service/StopService.php index 85c61dc83..6b348f830 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -31,4 +31,4 @@ class StopService return $e->getMessage(); } } -} \ No newline at end of file +} diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index 6e102266a..1f4e835e9 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -56,17 +56,7 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue case 'standalone-dragonfly': case 'standalone-clickhouse': $persistentStorages = $this->resource?->persistentStorages()?->get(); - StopDatabase::run($this->resource); - // TODO - // DBs do not have a network normally? - //if ($this->deleteConnectedNetworks) { - // $this->resource?->delete_connected_networks($this->resource->uuid); - // } - // } - // $server = data_get($this->resource, 'server'); - // if ($this->deleteImages && $server) { - // CleanupDocker::run($server, true); - // } + StopDatabase::run($this->resource, true); break; case 'service': StopService::run($this->resource, true); @@ -80,10 +70,10 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue if ($this->deleteConfigurations) { $this->resource?->delete_configurations(); } - + $server = data_get($this->resource, 'server'); if ($this->deleteImages && $server) { - CleanupDocker::run($server, true); + CleanupDocker::run($server, true); //this is run for DBs but it does not work for DBs } if ($this->deleteConnectedNetworks) { From 7d1179e7c89bf38d5607c8f52ff1b6eb559e6d70 Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 22:25:39 +0200 Subject: [PATCH 023/371] fix cleanup images for databases --- app/Jobs/DeleteResourceJob.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index 1f4e835e9..b0983bc47 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -69,11 +69,21 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue } if ($this->deleteConfigurations) { $this->resource?->delete_configurations(); + ray('Iam am running now, Configurations should disapear right?'); } - $server = data_get($this->resource, 'server'); - if ($this->deleteImages && $server) { - CleanupDocker::run($server, true); //this is run for DBs but it does not work for DBs + $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->deleteImages || $isDatabase) && $server) { + CleanupDocker::run($server, true); + ray('I am running now, images should disappear right?'); } if ($this->deleteConnectedNetworks) { From 840e225aa86befa895a41a789106fe317e85d66d Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 22:43:18 +0200 Subject: [PATCH 024/371] formatting and waring text --- app/Jobs/DeleteResourceJob.php | 4 +- app/Models/ServiceDatabase.php | 4 +- .../project/database/heading.blade.php | 128 ++++++++---------- 3 files changed, 63 insertions(+), 73 deletions(-) diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index b0983bc47..b8a8756cb 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -69,9 +69,8 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue } if ($this->deleteConfigurations) { $this->resource?->delete_configurations(); - ray('Iam am running now, Configurations should disapear right?'); } - + $isDatabase = $this->resource instanceof StandalonePostgresql || $this->resource instanceof StandaloneRedis || $this->resource instanceof StandaloneMongodb @@ -83,7 +82,6 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue $server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server'); if (($this->deleteImages || $isDatabase) && $server) { CleanupDocker::run($server, true); - ray('I am running now, images should disappear right?'); } if ($this->deleteConnectedNetworks) { diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index 4a749913e..7bbcb4f4e 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); } @@ -78,7 +78,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/resources/views/livewire/project/database/heading.blade.php b/resources/views/livewire/project/database/heading.blade.php index cee1f0520..73824421d 100644 --- a/resources/views/livewire/project/database/heading.blade.php +++ b/resources/views/livewire/project/database/heading.blade.php @@ -7,88 +7,80 @@ - + \ No newline at end of file From b5360e5e7518b3f804301d255f87fe7958eec23e Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 23:12:30 +0200 Subject: [PATCH 025/371] improve CleanupDocker.php --- app/Actions/Server/CleanupDocker.php | 35 ++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php index 0009e001d..82515ab6b 100644 --- a/app/Actions/Server/CleanupDocker.php +++ b/app/Actions/Server/CleanupDocker.php @@ -11,15 +11,36 @@ class CleanupDocker public function handle(Server $server, bool $force = true) { - // cleanup docker images, containers, and builder caches + $commonCommands = [ + 'docker container prune -f --filter "label=coolify.managed=true"', + 'docker image prune -f', + 'docker builder prune -f', + 'docker network prune -f', + ]; + + $forceCommands = [ + 'docker container rm $(docker container ls -aq --filter status=exited --filter status=created)', + 'docker image prune -af', + 'docker builder prune -af', + 'docker system prune -af', + 'docker network prune -f', + ]; + + $additionalCommands = [ + 'docker rmi $(docker images -f "dangling=true" -q)', + 'docker network rm $(docker network ls -q -f "unused=true")', + 'docker system prune -f', + ]; + if ($force) { - instant_remote_process(['docker image prune -af'], $server, false); - instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false); - instant_remote_process(['docker builder prune -af'], $server, false); + $commands = array_merge($forceCommands, $commonCommands, $additionalCommands); + $commands[] = 'docker rm $(docker ps -a -q --filter status=exited --filter status=created)'; } else { - instant_remote_process(['docker image prune -f'], $server, false); - instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false); - instant_remote_process(['docker builder prune -f'], $server, false); + $commands = array_merge($commonCommands, $additionalCommands); + } + + foreach ($commands as $command) { + instant_remote_process([$command], $server, false); } } } From 5b54dc8792f884e28a7a5b7606086fb880c5cf77 Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Fri, 9 Aug 2024 23:25:57 +0200 Subject: [PATCH 026/371] Revert "improve CleanupDocker.php" This reverts commit b5360e5e7518b3f804301d255f87fe7958eec23e. --- app/Actions/Server/CleanupDocker.php | 35 ++++++---------------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php index 82515ab6b..0009e001d 100644 --- a/app/Actions/Server/CleanupDocker.php +++ b/app/Actions/Server/CleanupDocker.php @@ -11,36 +11,15 @@ class CleanupDocker public function handle(Server $server, bool $force = true) { - $commonCommands = [ - 'docker container prune -f --filter "label=coolify.managed=true"', - 'docker image prune -f', - 'docker builder prune -f', - 'docker network prune -f', - ]; - - $forceCommands = [ - 'docker container rm $(docker container ls -aq --filter status=exited --filter status=created)', - 'docker image prune -af', - 'docker builder prune -af', - 'docker system prune -af', - 'docker network prune -f', - ]; - - $additionalCommands = [ - 'docker rmi $(docker images -f "dangling=true" -q)', - 'docker network rm $(docker network ls -q -f "unused=true")', - 'docker system prune -f', - ]; - + // cleanup docker images, containers, and builder caches if ($force) { - $commands = array_merge($forceCommands, $commonCommands, $additionalCommands); - $commands[] = 'docker rm $(docker ps -a -q --filter status=exited --filter status=created)'; + instant_remote_process(['docker image prune -af'], $server, false); + instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false); + instant_remote_process(['docker builder prune -af'], $server, false); } else { - $commands = array_merge($commonCommands, $additionalCommands); - } - - foreach ($commands as $command) { - instant_remote_process([$command], $server, false); + instant_remote_process(['docker image prune -f'], $server, false); + instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false); + instant_remote_process(['docker builder prune -f'], $server, false); } } } From c2ea8996ee2689f54f55703ba7e8c3ea144784fe Mon Sep 17 00:00:00 2001 From: Luan Estradioto Date: Tue, 25 Jun 2024 15:29:33 -0300 Subject: [PATCH 027/371] feat: fully functional terminal for command center --- .../Shared/ExecuteContainerCommand.php | 25 +-- app/Livewire/Project/Shared/Terminal.php | 46 +++++ app/Livewire/RunCommand.php | 69 +++++-- app/Models/Server.php | 29 +++ docker-compose.dev.yml | 14 +- docker-compose.prod.yml | 13 ++ docker-compose.windows.yml | 18 ++ docker-compose.yml | 6 + package-lock.json | 53 ++++- package.json | 6 +- resources/js/app.js | 15 ++ .../execute-container-command.blade.php | 13 +- .../project/shared/terminal.blade.php | 191 ++++++++++++++++++ .../views/livewire/run-command.blade.php | 20 +- terminal-server.js | 163 +++++++++++++++ 15 files changed, 624 insertions(+), 57 deletions(-) create mode 100644 app/Livewire/Project/Shared/Terminal.php create mode 100644 resources/views/livewire/project/shared/terminal.blade.php create mode 100755 terminal-server.js diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index 343915d9c..b560595b3 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -2,17 +2,15 @@ namespace App\Livewire\Project\Shared; -use App\Actions\Server\RunCommand; use App\Models\Application; use App\Models\Server; use App\Models\Service; use Illuminate\Support\Collection; +use Livewire\Attributes\On; use Livewire\Component; class ExecuteContainerCommand extends Component { - public string $command; - public string $container; public Collection $containers; @@ -23,8 +21,6 @@ class ExecuteContainerCommand extends Component public string $type; - public string $workDir = ''; - public Server $server; public Collection $servers; @@ -33,7 +29,6 @@ class ExecuteContainerCommand extends Component 'server' => 'required', 'container' => 'required', 'command' => 'required', - 'workDir' => 'nullable', ]; public function mount() @@ -115,7 +110,8 @@ class ExecuteContainerCommand extends Component } } - public function runCommand() + #[On('connectToContainer')] + public function connectToContainer() { try { if (data_get($this->parameters, 'application_uuid')) { @@ -132,14 +128,13 @@ class ExecuteContainerCommand extends Component if ($server->isForceDisabled()) { throw new \RuntimeException('Server is disabled.'); } - $cmd = "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; ".str_replace("'", "'\''", $this->command)."'"; - if (! empty($this->workDir)) { - $exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}"; - } else { - $exec = "docker exec {$container_name} {$cmd}"; - } - $activity = RunCommand::run(server: $server, command: $exec); - $this->dispatch('activityMonitor', $activity->id); + + $this->dispatch('send-terminal-command', + true, + $container_name, + $server->uuid, + ); + } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php new file mode 100644 index 000000000..331392118 --- /dev/null +++ b/app/Livewire/Project/Shared/Terminal.php @@ -0,0 +1,46 @@ +firstOrFail(); + + if (auth()->user()) { + $teams = auth()->user()->teams->pluck('id'); + if (! $teams->contains($server->team_id) && ! $teams->contains(0)) { + throw new \Exception('User is not part of the team that owns this server'); + } + } + + if ($isContainer) { + $status = getContainerStatus($server, $identifier); + if ($status !== 'running') { + return handleError(new \Exception('Container is not running'), $this); + } + $command = generateSshCommand($server, "docker exec -it {$identifier} sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); + } else { + $command = generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); + } + + // ssh command is sent back to frontend then to websocket + // this is done because the websocket connection is not available here + // a better solution would be to remove websocket on NodeJS and work with something like + // 1. Laravel Pusher/Echo connection (not possible without a sdk) + // 2. Ratchet / Revolt / ReactPHP / Event Loop (possible but hard to implement and huge dependencies) + // 3. Just found out about this https://github.com/sirn-se/websocket-php, perhaps it can be used + $this->dispatch('send-back-command', $command); + } + + public function render() + { + return view('livewire.project.shared.terminal'); + } +} diff --git a/app/Livewire/RunCommand.php b/app/Livewire/RunCommand.php index c2d3adeea..aae02d4e1 100644 --- a/app/Livewire/RunCommand.php +++ b/app/Livewire/RunCommand.php @@ -2,42 +2,67 @@ namespace App\Livewire; -use App\Actions\Server\RunCommand as ServerRunCommand; use App\Models\Server; +use Livewire\Attributes\On; use Livewire\Component; class RunCommand extends Component { - public string $command; - - public $server; + public $selected_uuid; public $servers = []; - protected $rules = [ - 'server' => 'required', - 'command' => 'required', - ]; - - protected $validationAttributes = [ - 'server' => 'server', - 'command' => 'command', - ]; + public $containers = []; public function mount($servers) { $this->servers = $servers; - $this->server = $servers[0]->uuid; + $this->selected_uuid = $servers[0]->uuid; + $this->containers = $this->getAllActiveContainers(); } - public function runCommand() + private function getAllActiveContainers() { - $this->validate(); - try { - $activity = ServerRunCommand::run(server: Server::where('uuid', $this->server)->first(), command: $this->command); - $this->dispatch('activityMonitor', $activity->id); - } catch (\Throwable $e) { - return handleError($e, $this); - } + return Server::all()->flatMap(function ($server) { + if (! $server->isFunctional()) { + return []; + } + + return $server->definedResources() + ->filter(fn ($resource) => str_starts_with($resource->status, 'running:')) + ->map(function ($resource) use ($server) { + $container_name = $resource->uuid; + + if (class_basename($resource) === 'Application' || class_basename($resource) === 'Service') { + if ($server->isSwarm()) { + $container_name = $resource->uuid.'_'.$resource->uuid; + } else { + $current_containers = getCurrentApplicationContainerStatus($server, $resource->id, includePullrequests: true); + $container_name = data_get($current_containers->first(), 'Names'); + } + } + + return [ + 'name' => $resource->name, + 'connection_name' => $container_name, + 'uuid' => $resource->uuid, + 'status' => $resource->status, + 'server' => $server, + 'server_uuid' => $server->uuid, + ]; + }); + }); + } + + #[On('connectToContainer')] + public function connectToContainer() + { + $container = collect($this->containers)->firstWhere('uuid', $this->selected_uuid); + + $this->dispatch('send-terminal-command', + isset($container), + $container['connection_name'] ?? $this->selected_uuid, + $container['server_uuid'] ?? $this->selected_uuid + ); } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 8a7325beb..337d7d7fa 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -295,6 +295,13 @@ respond 404 'service' => 'coolify-realtime', 'rule' => "Host(`{$host}`) && PathPrefix(`/app`)", ], + 'coolify-terminal-ws' => [ + 'entryPoints' => [ + 0 => 'http', + ], + 'service' => 'coolify-terminal', + 'rule' => "Host(`{$host}`) && PathPrefix(`/terminal`)", + ], ], 'services' => [ 'coolify' => [ @@ -315,6 +322,15 @@ respond 404 ], ], ], + 'coolify-terminal' => [ + 'loadBalancer' => [ + 'servers' => [ + 0 => [ + 'url' => 'http://coolify-terminal:6002', + ], + ], + ], + ], ], ], ]; @@ -344,6 +360,16 @@ respond 404 'certresolver' => 'letsencrypt', ], ]; + $traefik_dynamic_conf['http']['routers']['coolify-terminal-wss'] = [ + 'entryPoints' => [ + 0 => 'https', + ], + 'service' => 'coolify-terminal', + 'rule' => "Host(`{$host}`) && PathPrefix(`/terminal`)", + 'tls' => [ + 'certresolver' => 'letsencrypt', + ], + ]; } $yaml = Yaml::dump($traefik_dynamic_conf, 12, 2); $yaml = @@ -377,6 +403,9 @@ $schema://$host { handle /app/* { reverse_proxy coolify-realtime:6001 } + handle /terminal/* { + reverse_proxy coolify-terminal:6002 + } reverse_proxy coolify:80 }"; $base64 = base64_encode($caddy_file); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 7eda14d41..9710e0fae 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -52,8 +52,18 @@ services: SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}" SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}" + terminal: + env_file: + - .env + pull_policy: always + working_dir: /var/www/html + ports: + - "${FORWARD_TERMINAL_PORT:-6002}:6002" + volumes: + - .:/var/www/html:cached + command: sh -c "apk add --no-cache openssh-client && node --watch /var/www/html/terminal-server.js" vite: - image: node:20 + image: node:alpine pull_policy: always working_dir: /var/www/html # environment: @@ -62,7 +72,7 @@ services: - "${VITE_PORT:-5173}:${VITE_PORT:-5173}" volumes: - .:/var/www/html:cached - command: sh -c "npm install && npm run dev" + command: sh -c "apk add --no-cache make g++ python3 && npm install && npm run dev" networks: - coolify testing-host: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b8156cab5..5f7b5e935 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -125,6 +125,19 @@ services: interval: 5s retries: 10 timeout: 2s + terminal: + working_dir: /var/www/html + ports: + - "${TERMINAL_PORT:-6002}:6002" + volumes: + - .:/var/www/html:cached + command: sh -c "apk add --no-cache openssh-client && node /var/www/html/terminal-server.js" + healthcheck: + test: wget -qO- http://localhost:6002/ready || exit 1 + interval: 5s + retries: 10 + timeout: 2s + volumes: coolify-db: name: coolify-db diff --git a/docker-compose.windows.yml b/docker-compose.windows.yml index af5ecc0f7..ab3c7197a 100644 --- a/docker-compose.windows.yml +++ b/docker-compose.windows.yml @@ -121,6 +121,24 @@ services: interval: 5s retries: 10 timeout: 2s + terminal: + image: node:alpine + pull_policy: always + container_name: coolify-terminal + restart: always + env_file: + - .env + working_dir: /var/www/html + ports: + - "${TERMINAL_PORT:-6002}:6002" + volumes: + - .:/var/www/html:cached + command: sh -c "apk add --no-cache openssh-client && node /var/www/html/terminal-server.js" + healthcheck: + test: wget -qO- http://localhost:6002/ready || exit 1 + interval: 5s + retries: 10 + timeout: 2s volumes: coolify-db: name: coolify-db diff --git a/docker-compose.yml b/docker-compose.yml index 8eed44f8c..4f1c03127 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,12 @@ services: restart: always networks: - coolify + terminal: + image: node:alpine + container_name: coolify-terminal + restart: always + networks: + - coolify networks: coolify: name: coolify diff --git a/package-lock.json b/package-lock.json index bec5a7f66..ed15acfe0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,13 @@ "dependencies": { "@tailwindcss/forms": "0.5.7", "@tailwindcss/typography": "0.5.13", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", "alpinejs": "3.14.0", "ioredis": "5.4.1", - "tailwindcss-scrollbar": "0.1.0" + "node-pty": "^1.0.0", + "tailwindcss-scrollbar": "0.1.0", + "ws": "^8.17.0" }, "devDependencies": { "@vitejs/plugin-vue": "4.5.1", @@ -692,6 +696,19 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==" }, + "node_modules/@xterm/addon-fit": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", + "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==" + }, "node_modules/alpinejs": { "version": "3.14.0", "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.0.tgz", @@ -1474,6 +1491,11 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==" + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -1491,6 +1513,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-pty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", + "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "hasInstallScript": true, + "dependencies": { + "nan": "^2.17.0" + } + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -2123,6 +2154,26 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yaml": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", diff --git a/package.json b/package.json index b4609a025..9c2541ecc 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,12 @@ "dependencies": { "@tailwindcss/forms": "0.5.7", "@tailwindcss/typography": "0.5.13", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", "alpinejs": "3.14.0", "ioredis": "5.4.1", - "tailwindcss-scrollbar": "0.1.0" + "node-pty": "^1.0.0", + "tailwindcss-scrollbar": "0.1.0", + "ws": "^8.17.0" } } diff --git a/resources/js/app.js b/resources/js/app.js index befec919e..f450cbe29 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -4,3 +4,18 @@ // const app = createApp({}); // app.component("magic-bar", MagicBar); // app.mount("#vue"); + +import { Terminal } from '@xterm/xterm'; +import '@xterm/xterm/css/xterm.css'; +import { FitAddon } from '@xterm/addon-fit'; + +if (!window.term) { + window.term = new Terminal({ + cols: 80, + rows: 30, + fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"', + cursorBlink: true + }); + window.fitAddon = new FitAddon(); + window.term.loadAddon(window.fitAddon); +} diff --git a/resources/views/livewire/project/shared/execute-container-command.blade.php b/resources/views/livewire/project/shared/execute-container-command.blade.php index 680d6e0e1..167f4178b 100644 --- a/resources/views/livewire/project/shared/execute-container-command.blade.php +++ b/resources/views/livewire/project/shared/execute-container-command.blade.php @@ -22,11 +22,8 @@
@if (count($containers) > 0) -
-
- - -
+ @if (data_get($this->parameters, 'application_uuid')) @@ -47,14 +44,14 @@ @endif - Run + Start Connection
@else
No containers are not running.
@endif
-
- +
+
diff --git a/resources/views/livewire/project/shared/terminal.blade.php b/resources/views/livewire/project/shared/terminal.blade.php new file mode 100644 index 000000000..ecb1a0e50 --- /dev/null +++ b/resources/views/livewire/project/shared/terminal.blade.php @@ -0,0 +1,191 @@ +
+
+
+ (connection closed) +
+
+
+
+ + +
+ + @script + + @endscript +
diff --git a/resources/views/livewire/run-command.blade.php b/resources/views/livewire/run-command.blade.php index 7911f0470..1f0162940 100644 --- a/resources/views/livewire/run-command.blade.php +++ b/resources/views/livewire/run-command.blade.php @@ -1,19 +1,23 @@
-
- - + + @foreach ($servers as $server) @if ($loop->first) @else @endif + @foreach ($containers as $container) + @if ($container['server_uuid'] == $server->uuid) + + @endif + @endforeach @endforeach - Execute Command - + Start Connection -
- -
+
diff --git a/terminal-server.js b/terminal-server.js new file mode 100755 index 000000000..3923c3b5c --- /dev/null +++ b/terminal-server.js @@ -0,0 +1,163 @@ +import { WebSocketServer } from 'ws'; +import http from 'http'; +import pty from 'node-pty'; + +const server = http.createServer((req, res) => { + if (req.url === '/ready') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('OK'); + } else { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found'); + } +}); + +const wss = new WebSocketServer({ server, path: '/terminal' }); +const userSessions = new Map(); + +wss.on('connection', (ws) => { + const userId = generateUserId(); + const userSession = { ws, userId, ptyProcess: null, isActive: false }; + userSessions.set(userId, userSession); + + ws.on('message', (message) => handleMessage(userSession, message)); + ws.on('error', (err) => handleError(err, userId)); + ws.on('close', () => handleClose(userId)); +}); + +const messageHandlers = { + message: (session, data) => session.ptyProcess.write(data), + resize: (session, { cols, rows }) => session.ptyProcess.resize(cols, rows), + pause: (session) => session.ptyProcess.pause(), + resume: (session) => session.ptyProcess.resume(), + checkActive: (session, data) => { + if (data === 'force' && session.isActive) { + killPtyProcess(session.userId); + } else { + session.ws.send(session.isActive); + } + }, + command: (session, data) => handleCommand(session.ws, data, session.userId) +}; + +function handleMessage(userSession, message) { + const parsed = parseMessage(message); + if (!parsed) return; + + Object.entries(parsed).forEach(([key, value]) => { + const handler = messageHandlers[key]; + if (handler && (userSession.isActive || key === 'checkActive' || key === 'command')) { + handler(userSession, value); + } + }); +} + +function parseMessage(message) { + try { + return JSON.parse(message); + } catch (e) { + console.error('Failed to parse message:', e); + return null; + } +} + +async function handleCommand(ws, command, userId) { + const userSession = userSessions.get(userId); + + if (userSession && userSession.isActive) { + await killPtyProcess(userId); + } + + const commandString = command[0].split('\n').join(' '); + const timeout = extractTimeout(commandString); + const sshArgs = extractSshArgs(commandString); + const hereDocContent = extractHereDocContent(commandString); + const options = { + name: 'xterm-color', + cols: 80, + rows: 30, + cwd: process.env.HOME, + env: process.env + }; + + // NOTE: - Initiates a process within the Terminal container + // Establishes an SSH connection to root@coolify with RequestTTY enabled + // Executes the 'docker exec' command to connect to a specific container + // If the user types 'exit', it terminates the container connection and reverts to the server. + const ptyProcess = pty.spawn('ssh', sshArgs.concat(['bash']), options); + userSession.ptyProcess = ptyProcess; + userSession.isActive = true; + ptyProcess.write(hereDocContent + '\n'); + ptyProcess.write('clear\n'); + + ws.send('pty-ready'); + + ptyProcess.onData((data) => ws.send(data)); + + ptyProcess.onExit(({ exitCode, signal }) => { + console.error(`Process exited with code ${exitCode} and signal ${signal}`); + userSession.isActive = false; + }); + + if (timeout) { + setTimeout(async () => { + await killPtyProcess(userId); + }, timeout * 1000); + } +} + +async function handleError(err, userId) { + console.error('WebSocket error:', err); + await killPtyProcess(userId); +} + +async function handleClose(userId) { + await killPtyProcess(userId); + userSessions.delete(userId); +} + +async function killPtyProcess(userId) { + const session = userSessions.get(userId); + if (!session?.ptyProcess) return false; + + return new Promise((resolve) => { + session.ptyProcess.on('exit', () => { + session.isActive = false; + resolve(true); + }); + + session.ptyProcess.kill(); + }); +} + +function generateUserId() { + return Math.random().toString(36).substring(2, 11); +} + +function extractTimeout(commandString) { + const timeoutMatch = commandString.match(/timeout (\d+)/); + return timeoutMatch ? parseInt(timeoutMatch[1], 10) : null; +} + +function extractSshArgs(commandString) { + const sshCommandMatch = commandString.match(/ssh (.+?) 'bash -se'/); + let sshArgs = sshCommandMatch ? sshCommandMatch[1].split(' ') : []; + sshArgs = sshArgs.map(arg => arg === 'RequestTTY=no' ? 'RequestTTY=yes' : arg); + if (!sshArgs.includes('RequestTTY=yes')) { + sshArgs.push('-o', 'RequestTTY=yes'); + } + return sshArgs; +} + +function extractHereDocContent(commandString) { + const delimiterMatch = commandString.match(/<< (\S+)/); + const delimiter = delimiterMatch ? delimiterMatch[1] : null; + const escapedDelimiter = delimiter.slice(1).trim().replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'); + const hereDocRegex = new RegExp(`<< \\\\${escapedDelimiter}([\\s\\S\\.]*?)${escapedDelimiter}`); + const hereDocMatch = commandString.match(hereDocRegex); + return hereDocMatch ? hereDocMatch[1] : ''; +} + +server.listen(6002, () => { + console.log('Server listening on port 6002'); +}); From 548fc21e4008b2723d81f273535b3b0c82a6acc6 Mon Sep 17 00:00:00 2001 From: Luan Estradioto Date: Thu, 15 Aug 2024 02:38:06 -0300 Subject: [PATCH 028/371] added ws authentication --- app/Livewire/RunCommand.php | 5 +- package-lock.json | 32 +- package.json | 4 +- .../project/shared/terminal.blade.php | 309 +++++++++--------- routes/web.php | 6 + terminal-server.js | 43 ++- 6 files changed, 238 insertions(+), 161 deletions(-) diff --git a/app/Livewire/RunCommand.php b/app/Livewire/RunCommand.php index aae02d4e1..2d01cbca0 100644 --- a/app/Livewire/RunCommand.php +++ b/app/Livewire/RunCommand.php @@ -29,7 +29,10 @@ class RunCommand extends Component } return $server->definedResources() - ->filter(fn ($resource) => str_starts_with($resource->status, 'running:')) + ->filter(function ($resource) { + $status = method_exists($resource, 'realStatus') ? $resource->realStatus() : (method_exists($resource, 'status') ? $resource->status() : 'exited'); + return str_starts_with($status, 'running:'); + }) ->map(function ($resource) use ($server) { $container_name = $resource->uuid; diff --git a/package-lock.json b/package-lock.json index ed15acfe0..a9e742135 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "alpinejs": "3.14.0", + "cookie": "^0.6.0", + "dotenv": "^16.4.5", "ioredis": "5.4.1", "node-pty": "^1.0.0", "tailwindcss-scrollbar": "0.1.0", @@ -18,7 +20,7 @@ "devDependencies": { "@vitejs/plugin-vue": "4.5.1", "autoprefixer": "10.4.19", - "axios": "1.7.2", + "axios": "^1.7.4", "laravel-echo": "1.16.1", "laravel-vite-plugin": "0.8.1", "postcss": "8.4.38", @@ -783,10 +785,11 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "dev": true, + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -956,6 +959,15 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1016,6 +1028,18 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.692", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.692.tgz", diff --git a/package.json b/package.json index 9c2541ecc..882ad2e01 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "devDependencies": { "@vitejs/plugin-vue": "4.5.1", "autoprefixer": "10.4.19", - "axios": "1.7.2", + "axios": "^1.7.4", "laravel-echo": "1.16.1", "laravel-vite-plugin": "0.8.1", "postcss": "8.4.38", @@ -23,6 +23,8 @@ "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "alpinejs": "3.14.0", + "cookie": "^0.6.0", + "dotenv": "^16.4.5", "ioredis": "5.4.1", "node-pty": "^1.0.0", "tailwindcss-scrollbar": "0.1.0", diff --git a/resources/views/livewire/project/shared/terminal.blade.php b/resources/views/livewire/project/shared/terminal.blade.php index ecb1a0e50..4e1a086e2 100644 --- a/resources/views/livewire/project/shared/terminal.blade.php +++ b/resources/views/livewire/project/shared/terminal.blade.php @@ -24,168 +24,169 @@ @script - + // Type CTRL + D or exit in the terminal + if (data === '\x04' || (data === '\r' && stripAnsiCommands(commandBuffer).trim() === 'exit')) { + checkIfProcessIsRunningAndKillIt(); + setTimeout(() => { + term.reset(); + term.write('(connection closed)'); + $data.terminalActive = false; + }, 500); + commandBuffer = ''; + } else if (data === '\r') { + commandBuffer = ''; + } else { + commandBuffer += data; + } + }); + + function stripAnsiCommands(input) { + return input.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, ''); + } + + // Copy and paste + // Enables ctrl + c and ctrl + v + // defaults otherwise to ctrl + insert, shift + insert + term.attachCustomKeyEventHandler((arg) => { + if (arg.ctrlKey && arg.code === "KeyV" && arg.type === "keydown") { + navigator.clipboard.readText() + .then(text => { + socket.send(JSON.stringify({ + message: text + })); + }) + }; + + if (arg.ctrlKey && arg.code === "KeyC" && arg.type === "keydown") { + const selection = term.getSelection(); + if (selection) { + navigator.clipboard.writeText(selection); + return false; + } + } + return true; + }); + + $wire.on('send-back-command', function(command) { + socket.send(JSON.stringify({ + command: command + })); + }); + + window.addEventListener('beforeunload', function(e) { + checkIfProcessIsRunningAndKillIt(); + }); + + function checkIfProcessIsRunningAndKillIt() { + socket.send(JSON.stringify({ + checkActive: 'force' + })); + } + + window.onresize = function() { + $data.resizeTerminal() + }; + + Alpine.data('data', () => ({ + fullscreen: false, + terminalActive: false, + init() { + this.$watch('terminalActive', (value) => { + this.$nextTick(() => { + if (value) { + $refs.terminalWrapper.style.display = 'block'; + this.resizeTerminal(); + } else { + $refs.terminalWrapper.style.display = 'none'; + } + }); + }); + }, + makeFullscreen() { + this.fullscreen = !this.fullscreen; + $nextTick(() => { + this.resizeTerminal() + }) + }, + + resizeTerminal() { + if (!this.terminalActive) return; + + fitAddon.fit(); + const height = $refs.terminalWrapper.clientHeight; + const rows = height / term._core._renderService._charSizeService.height - 1; + var termWidth = term.cols; + var termHeight = parseInt(rows.toString(), 10); + term.resize(termWidth, termHeight); + socket.send(JSON.stringify({ + resize: { + cols: termWidth, + rows: termHeight + } + })); + } + })); + + initializeWebSocket(); + @endscript - + \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index e2ccfc704..01cf2762b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -154,6 +154,12 @@ Route::middleware(['auth', 'verified'])->group(function () { }); Route::get('/command-center', CommandCenterIndex::class)->name('command-center'); + Route::post('/terminal/auth', function () { + if (auth()->check()) { + return response()->json(['authenticated' => true], 200); + } + return response()->json(['authenticated' => false], 401); + })->name('terminal.auth'); Route::prefix('invitations')->group(function () { Route::get('/{uuid}', [Controller::class, 'accept_invitation'])->name('team.invitation.accept'); diff --git a/terminal-server.js b/terminal-server.js index 3923c3b5c..a1555bf30 100755 --- a/terminal-server.js +++ b/terminal-server.js @@ -1,6 +1,9 @@ import { WebSocketServer } from 'ws'; import http from 'http'; import pty from 'node-pty'; +import axios from 'axios'; +import cookie from 'cookie'; +import 'dotenv/config' const server = http.createServer((req, res) => { if (req.url === '/ready') { @@ -12,7 +15,45 @@ const server = http.createServer((req, res) => { } }); -const wss = new WebSocketServer({ server, path: '/terminal' }); +const verifyClient = async (info, callback) => { + const cookies = cookie.parse(info.req.headers.cookie || ''); + const origin = new URL(info.origin); + const protocol = origin.protocol; + const xsrfToken = cookies['XSRF-TOKEN']; + + // Generate session cookie name based on APP_NAME + const appName = process.env.APP_NAME || 'laravel'; + const sessionCookieName = `${appName.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}_session`; + const laravelSession = cookies[sessionCookieName]; + + // Verify presence of required tokens + if (!laravelSession || !xsrfToken) { + return callback(false, 401, 'Unauthorized: Missing required tokens'); + } + + try { + // Authenticate with Laravel backend + const response = await axios.post(`${protocol}//coolify/terminal/auth`, null, { + headers: { + 'Cookie': `${sessionCookieName}=${laravelSession}`, + 'X-XSRF-TOKEN': xsrfToken + }, + }); + + if (response.status === 200) { + // Authentication successful + callback(true); + } else { + callback(false, 401, 'Unauthorized: Invalid credentials'); + } + } catch (error) { + console.error('Authentication error:', error.message); + callback(false, 500, 'Internal Server Error'); + } +}; + + +const wss = new WebSocketServer({ server, path: '/terminal', verifyClient: verifyClient }); const userSessions = new Map(); wss.on('connection', (ws) => { From 2b8c9920d85c82756c2e4a1ac9d0ac1443e0295d Mon Sep 17 00:00:00 2001 From: Luan Estradioto Date: Thu, 15 Aug 2024 20:52:50 -0300 Subject: [PATCH 029/371] removed extra container and added new process to soketi container --- app/Models/Server.php | 4 +- docker-compose.dev.yml | 20 +++++----- docker-compose.prod.yml | 22 +++++------ docker-compose.windows.yml | 31 ++++++--------- docker-compose.yml | 6 --- docker/soketi-entrypoint/soketi-entrypoint.sh | 39 +++++++++++++++++++ 6 files changed, 70 insertions(+), 52 deletions(-) create mode 100644 docker/soketi-entrypoint/soketi-entrypoint.sh diff --git a/app/Models/Server.php b/app/Models/Server.php index 337d7d7fa..44746324b 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -326,7 +326,7 @@ respond 404 'loadBalancer' => [ 'servers' => [ 0 => [ - 'url' => 'http://coolify-terminal:6002', + 'url' => 'http://coolify-realtime:6002', ], ], ], @@ -404,7 +404,7 @@ $schema://$host { reverse_proxy coolify-realtime:6001 } handle /terminal/* { - reverse_proxy coolify-terminal:6002 + reverse_proxy coolify-realtime:6002 } reverse_proxy coolify:80 }"; diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 9710e0fae..64eb1d2a4 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -47,21 +47,19 @@ services: - .env ports: - "${FORWARD_SOKETI_PORT:-6001}:6001" + - "6002:6002" + volumes: + - ./docker/soketi-entrypoint/soketi-entrypoint.sh:/soketi-entrypoint.sh + - ./package.json:/terminal/package.json + - ./package-lock.json:/terminal/package-lock.json + - ./terminal-server.js:/terminal/terminal-server.js + - ./storage:/var/www/html/storage + entrypoint: ["/bin/sh", "/soketi-entrypoint.sh"] environment: SOKETI_DEBUG: "false" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}" SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}" - terminal: - env_file: - - .env - pull_policy: always - working_dir: /var/www/html - ports: - - "${FORWARD_TERMINAL_PORT:-6002}:6002" - volumes: - - .:/var/www/html:cached - command: sh -c "apk add --no-cache openssh-client && node --watch /var/www/html/terminal-server.js" vite: image: node:alpine pull_policy: always @@ -72,7 +70,7 @@ services: - "${VITE_PORT:-5173}:${VITE_PORT:-5173}" volumes: - .:/var/www/html:cached - command: sh -c "apk add --no-cache make g++ python3 && npm install && npm run dev" + command: sh -c "npm install && npm run dev" networks: - coolify testing-host: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 5f7b5e935..5fc6a9919 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -115,25 +115,21 @@ services: soketi: ports: - "${SOKETI_PORT:-6001}:6001" + - "6002:6002" + volumes: + - ./docker/soketi-entrypoint/soketi-entrypoint.sh:/soketi-entrypoint.sh + - ./package.json:/terminal/package.json + - ./package-lock.json:/terminal/package-lock.json + - ./terminal-server.js:/terminal/terminal-server.js + - ./storage:/var/www/html/storage + entrypoint: ["/bin/sh", "/soketi-entrypoint.sh"] environment: SOKETI_DEBUG: "${SOKETI_DEBUG:-false}" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}" SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}" healthcheck: - test: wget -qO- http://127.0.0.1:6001/ready || exit 1 - interval: 5s - retries: 10 - timeout: 2s - terminal: - working_dir: /var/www/html - ports: - - "${TERMINAL_PORT:-6002}:6002" - volumes: - - .:/var/www/html:cached - command: sh -c "apk add --no-cache openssh-client && node /var/www/html/terminal-server.js" - healthcheck: - test: wget -qO- http://localhost:6002/ready || exit 1 + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1"] interval: 5s retries: 10 timeout: 2s diff --git a/docker-compose.windows.yml b/docker-compose.windows.yml index ab3c7197a..1b800a5d6 100644 --- a/docker-compose.windows.yml +++ b/docker-compose.windows.yml @@ -102,7 +102,7 @@ services: interval: 5s retries: 10 timeout: 2s - soketi: +soketi: image: 'quay.io/soketi/soketi:1.6-16-alpine' pull_policy: always container_name: coolify-realtime @@ -111,34 +111,25 @@ services: - .env ports: - "${SOKETI_PORT:-6001}:6001" + - "6002:6002" + volumes: + - ./docker/soketi-entrypoint/soketi-entrypoint.sh:/soketi-entrypoint.sh + - ./package.json:/terminal/package.json + - ./package-lock.json:/terminal/package-lock.json + - ./terminal-server.js:/terminal/terminal-server.js + - ./storage:/var/www/html/storage + entrypoint: ["/bin/sh", "/soketi-entrypoint.sh"] environment: SOKETI_DEBUG: "${SOKETI_DEBUG:-false}" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}" SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}" healthcheck: - test: wget -qO- http://localhost:6001/ready || exit 1 - interval: 5s - retries: 10 - timeout: 2s - terminal: - image: node:alpine - pull_policy: always - container_name: coolify-terminal - restart: always - env_file: - - .env - working_dir: /var/www/html - ports: - - "${TERMINAL_PORT:-6002}:6002" - volumes: - - .:/var/www/html:cached - command: sh -c "apk add --no-cache openssh-client && node /var/www/html/terminal-server.js" - healthcheck: - test: wget -qO- http://localhost:6002/ready || exit 1 + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1"] interval: 5s retries: 10 timeout: 2s + volumes: coolify-db: name: coolify-db diff --git a/docker-compose.yml b/docker-compose.yml index 4f1c03127..8eed44f8c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,12 +28,6 @@ services: restart: always networks: - coolify - terminal: - image: node:alpine - container_name: coolify-terminal - restart: always - networks: - - coolify networks: coolify: name: coolify diff --git a/docker/soketi-entrypoint/soketi-entrypoint.sh b/docker/soketi-entrypoint/soketi-entrypoint.sh new file mode 100644 index 000000000..808e306e7 --- /dev/null +++ b/docker/soketi-entrypoint/soketi-entrypoint.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +# Install openssh-client +apk add --no-cache openssh-client make g++ python3 + +cd /terminal + +# Install npm dependencies +npm ci + +# Rebuild node-pty +npm rebuild node-pty --update-binary + +# Function to timestamp logs +timestamp() { + date "+%Y-%m-%d %H:%M:%S" +} + +# Start the terminal server in the background with logging +node --watch /terminal/terminal-server.js > >(while read line; do echo "$(timestamp) [TERMINAL] $line"; done) 2>&1 & +TERMINAL_PID=$! + +# Start the Soketi process in the background with logging +node /app/bin/server.js start > >(while read line; do echo "$(timestamp) [SOKETI] $line"; done) 2>&1 & +SOKETI_PID=$! + +# Function to forward signals to child processes +forward_signal() { + kill -$1 $TERMINAL_PID $SOKETI_PID +} + +# Forward SIGTERM to child processes +trap 'forward_signal TERM' TERM + +# Wait for any process to exit +wait -n + +# Exit with status of process that exited first +exit $? \ No newline at end of file From 2a581147aa72c56aae32c731f0f51c81eb8d1e6e Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Tue, 27 Aug 2024 12:43:59 +0200 Subject: [PATCH 030/371] new confirm delete dialog --- .../components/modal-confirmation.blade.php | 109 ++++-------------- .../livewire/project/shared/danger.blade.php | 72 ++++++++++-- 2 files changed, 86 insertions(+), 95 deletions(-) diff --git a/resources/views/components/modal-confirmation.blade.php b/resources/views/components/modal-confirmation.blade.php index 7910d2eb2..56abba71d 100644 --- a/resources/views/components/modal-confirmation.blade.php +++ b/resources/views/components/modal-confirmation.blade.php @@ -1,62 +1,41 @@ @props([ 'title' => 'Are you sure?', 'isErrorButton' => false, - 'buttonTitle' => 'REWRITE THIS BUTTON TITLE PLEASSSSEEEE', + 'buttonTitle' => 'Confirm Action', 'buttonFullWidth' => false, 'customButton' => null, 'disabled' => false, 'action' => 'delete', 'content' => null, ]) -
@if ($customButton) - @if ($buttonFullWidth) - - {{ $customButton }} - - @else - - {{ $customButton }} - - @endif + + {{ $customButton }} + @else @if ($content)
{{ $content }}
@else - @if ($disabled) - @if ($buttonFullWidth) - - {{ $buttonTitle }} - - @else - - {{ $buttonTitle }} - - @endif - @elseif ($isErrorButton) - @if ($buttonFullWidth) - - {{ $buttonTitle }} - - @else - - {{ $buttonTitle }} - - @endif - @else - @if ($buttonFullWidth) - - {{ $buttonTitle }} - - @else - - {{ $buttonTitle }} - - @endif - @endif + + {{ $buttonTitle }} + @endif @endif diff --git a/resources/views/livewire/project/shared/danger.blade.php b/resources/views/livewire/project/shared/danger.blade.php index 1e1365749..1b24d0593 100644 --- a/resources/views/livewire/project/shared/danger.blade.php +++ b/resources/views/livewire/project/shared/danger.blade.php @@ -5,13 +5,69 @@
This will stop your containers, delete all related data, etc. Beware! There is no coming back!
+ -
This resource will be deleted. It is not reversible. Please think - again.

-

Actions

- - - - +
+ +
+
Select the actions you want to perform:
+ + + + +
+ Cancel + + Continue + +
+
+ + +
+ +
The following actions will be performed:
+
    + +
+
Please type DELETE to confirm this destructive action:
+ +
+ Back + + Permanently Delete + +
+
+
-
\ No newline at end of file + From ac50d8b4d8f4c9e34ff300a080825e9383433854 Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Tue, 27 Aug 2024 12:57:03 +0200 Subject: [PATCH 031/371] fix styling --- resources/views/livewire/project/shared/danger.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/livewire/project/shared/danger.blade.php b/resources/views/livewire/project/shared/danger.blade.php index 1b24d0593..c021208f0 100644 --- a/resources/views/livewire/project/shared/danger.blade.php +++ b/resources/views/livewire/project/shared/danger.blade.php @@ -26,10 +26,10 @@
Cancel Continue @@ -55,7 +55,7 @@
Please type DELETE to confirm this destructive action:
- +
Back Date: Tue, 27 Aug 2024 13:44:12 +0200 Subject: [PATCH 032/371] confirm with password --- app/Livewire/Project/Shared/Danger.php | 9 +- .../livewire/project/shared/danger.blade.php | 152 ++++++++++-------- 2 files changed, 93 insertions(+), 68 deletions(-) diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php index cff1d453a..5a49460da 100644 --- a/app/Livewire/Project/Shared/Danger.php +++ b/app/Livewire/Project/Shared/Danger.php @@ -5,6 +5,8 @@ namespace App\Livewire\Project\Shared; use App\Jobs\DeleteResourceJob; use Livewire\Component; use Visus\Cuid2\Cuid2; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Auth; class Danger extends Component { @@ -32,8 +34,13 @@ class Danger extends Component $this->environmentName = data_get($parameters, 'environment_name'); } - public function delete() + public function delete($selectedActions, $password) { + if (!Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); + return; + } + try { // $this->authorize('delete', $this->resource); $this->resource->delete(); diff --git a/resources/views/livewire/project/shared/danger.blade.php b/resources/views/livewire/project/shared/danger.blade.php index c021208f0..11521a8c6 100644 --- a/resources/views/livewire/project/shared/danger.blade.php +++ b/resources/views/livewire/project/shared/danger.blade.php @@ -1,73 +1,91 @@ -
-

Danger Zone

-
Woah. I hope you know what are you doing.
-

Delete Resource

-
This will stop your containers, delete all related data, etc. Beware! There is no coming - back! -
+
+

Danger Zone

- -
- -
-
Select the actions you want to perform:
- - - - -
- Cancel - - Continue - -
+
+
+
+

Delete Resource

+

Once you delete a resource, there is no going back. Please be certain.

+ +
+ +
+
Select the actions you want to perform:
+ + + + +
+ Cancel + + Continue + +
+
- -
- -
The following actions will be performed:
-
    - -
-
Please type DELETE to confirm this destructive action:
- -
- Back - - Permanently Delete - -
-
+
-
+
From 472667624821eb783db40b7287f54a72015c9fdb Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Tue, 27 Aug 2024 14:19:37 +0200 Subject: [PATCH 033/371] make things more clear --- app/Actions/Service/DeleteService.php | 4 ++-- app/Jobs/DeleteResourceJob.php | 6 ++--- app/Livewire/Project/Shared/Danger.php | 4 ++-- .../components/modal-confirmation.blade.php | 18 ++++++--------- .../livewire/project/shared/danger.blade.php | 22 +++++++++++++------ 5 files changed, 29 insertions(+), 25 deletions(-) diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index 238e6b954..93c79383e 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -10,7 +10,7 @@ class DeleteService { use AsAction; - public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $deleteImages, bool $deleteConnectedNetworks) + public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $dockerCleanup, bool $deleteConnectedNetworks) { try { $server = data_get($service, 'server'); @@ -69,7 +69,7 @@ class DeleteService $service->tags()->detach(); $service->forceDelete(); - if ($deleteImages) { + if ($dockerCleanup) { CleanupDocker::run($server, true); } } diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index b8a8756cb..1ebbf4681 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -33,7 +33,7 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, public bool $deleteConfigurations, public bool $deleteVolumes, - public bool $deleteImages, + public bool $dockerCleanup, public bool $deleteConnectedNetworks ) { } @@ -60,7 +60,7 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue break; case 'service': StopService::run($this->resource, true); - DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->deleteImages, $this->deleteConnectedNetworks); + DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks); break; } @@ -80,7 +80,7 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue || $this->resource instanceof StandaloneDragonfly || $this->resource instanceof StandaloneClickhouse; $server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server'); - if (($this->deleteImages || $isDatabase) && $server) { + if (($this->dockerCleanup || $isDatabase) && $server) { CleanupDocker::run($server, true); } diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php index 5a49460da..1fe7ec21a 100644 --- a/app/Livewire/Project/Shared/Danger.php +++ b/app/Livewire/Project/Shared/Danger.php @@ -20,7 +20,7 @@ class Danger extends Component public bool $delete_volumes = true; - public bool $delete_images = true; + public bool $docker_cleanup = true; public bool $delete_connected_networks = true; @@ -48,7 +48,7 @@ class Danger extends Component $this->resource, $this->delete_configurations, $this->delete_volumes, - $this->delete_images, + $this->docker_cleanup, $this->delete_connected_networks ); diff --git a/resources/views/components/modal-confirmation.blade.php b/resources/views/components/modal-confirmation.blade.php index 56abba71d..46910310c 100644 --- a/resources/views/components/modal-confirmation.blade.php +++ b/resources/views/components/modal-confirmation.blade.php @@ -8,15 +8,7 @@ 'action' => 'delete', 'content' => null, ]) -
@if ($customButton) @@ -32,7 +24,6 @@ @click="modalOpen=true" class="{{ $buttonFullWidth ? 'w-full' : '' }} {{ $isErrorButton ? 'bg-red-500 hover:bg-red-600 text-white' : '' }}" :disabled="$disabled" - wire:target > {{ $buttonTitle }} @@ -43,7 +34,7 @@ class="fixed top-0 lg:pt-10 left-0 z-[99] flex items-start justify-center w-screen h-screen" x-cloak>

{{ $title }}

+
{{ $slot }} diff --git a/resources/views/livewire/project/shared/danger.blade.php b/resources/views/livewire/project/shared/danger.blade.php index 11521a8c6..d7360310f 100644 --- a/resources/views/livewire/project/shared/danger.blade.php +++ b/resources/views/livewire/project/shared/danger.blade.php @@ -14,23 +14,25 @@ >
-
Select the actions you want to perform:
+
+
Select the actions you want to perform:
+
- +
Cancel - + Continue
@@ -44,6 +46,12 @@
The following actions will be performed:
    +
  • + + + + All containers of this resource will be stopped and permanently deleted. +
  • From 76cb473db80528056a25b22bb10b2ab50385596b Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Sat, 31 Aug 2024 13:06:55 +0200 Subject: [PATCH 051/371] fix default checkbox state --- .../components/modal-confirmation.blade.php | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/resources/views/components/modal-confirmation.blade.php b/resources/views/components/modal-confirmation.blade.php index acf641203..0896a1bc8 100644 --- a/resources/views/components/modal-confirmation.blade.php +++ b/resources/views/components/modal-confirmation.blade.php @@ -34,7 +34,7 @@ copied: false, submitAction: @js($submitAction), passwordError: '', - selectedActions: [], + selectedActions: @js(collect($checkboxes)->pluck('id')->all()), resetModal() { this.step = this.initialStep; this.deleteText = ''; @@ -73,7 +73,14 @@ this.copied = false; }, 2000); }, - + toggleAction(id) { + const index = this.selectedActions.indexOf(id); + if (index > -1) { + this.selectedActions.splice(index, 1); + } else { + this.selectedActions.push(id); + } + } }" @keydown.escape.window="modalOpen = false; resetModal()" :class="{ 'z-40': modalOpen }" class="relative w-auto h-auto" @password-error.window="passwordError = $event.detail"> @if ($customButton) @if ($buttonFullWidth) @@ -144,7 +151,13 @@
    Select the actions you want to perform:
@foreach($checkboxes as $index => $checkbox) - + @endforeach
@endif @@ -166,12 +179,14 @@ @endforeach @foreach($checkboxes as $checkbox) -
  • - - - - {{ $checkbox['label'] }} -
  • + @endforeach @if($confirmWithText) From bcfca40f3a7c979fed16c26742d797c61553cea6 Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Sat, 31 Aug 2024 13:10:45 +0200 Subject: [PATCH 052/371] rest checkboxes on close --- resources/views/components/modal-confirmation.blade.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/views/components/modal-confirmation.blade.php b/resources/views/components/modal-confirmation.blade.php index 0896a1bc8..02283a1a6 100644 --- a/resources/views/components/modal-confirmation.blade.php +++ b/resources/views/components/modal-confirmation.blade.php @@ -41,6 +41,7 @@ this.password = ''; this.userConfirmText = ''; this.passwordError = ''; + this.selectedActions = @js(collect($checkboxes)->pluck('id')->all()); }, step1ButtonText: @js($step1ButtonText), step2ButtonText: @js($step2ButtonText), @@ -157,6 +158,7 @@ :label="$checkbox['label']" x-on:change="toggleAction('{{ $checkbox['id'] }}')" :checked="true" + x-bind:checked="selectedActions.includes('{{ $checkbox['id'] }}')" > @endforeach
    From b118a627d001caef33a1be4bf918b817edb75b4f Mon Sep 17 00:00:00 2001 From: ayntk-ai <122374094+ayntk-ai@users.noreply.github.com> Date: Sat, 31 Aug 2024 13:41:08 +0200 Subject: [PATCH 053/371] fix password validation and password error --- .../components/modal-confirmation.blade.php | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/resources/views/components/modal-confirmation.blade.php b/resources/views/components/modal-confirmation.blade.php index 02283a1a6..f8b0c4a26 100644 --- a/resources/views/components/modal-confirmation.blade.php +++ b/resources/views/components/modal-confirmation.blade.php @@ -40,32 +40,34 @@ this.deleteText = ''; this.password = ''; this.userConfirmText = ''; - this.passwordError = ''; this.selectedActions = @js(collect($checkboxes)->pluck('id')->all()); + $wire.$refresh(); }, step1ButtonText: @js($step1ButtonText), step2ButtonText: @js($step2ButtonText), step3ButtonText: @js($step3ButtonText), validatePassword() { - this.passwordError = ''; if (this.confirmWithPassword && !this.password) { - this.passwordError = 'Password is required.'; - return false; + return 'Password is required.'; } - return true; + return ''; }, submitForm() { - if (this.validatePassword()) { - $wire.call(this.submitAction, this.password, this.selectedActions) - .then(result => { - if (result === true) { - this.modalOpen = false; - this.resetModal(); - } else if (typeof result === 'string') { - this.passwordError = result; - } - }); + if (this.confirmWithPassword) { + this.passwordError = this.validatePassword(); + if (this.passwordError) { + return; + } } + $wire.call(this.submitAction, this.password, this.selectedActions) + .then(result => { + if (result === true) { + this.modalOpen = false; + this.resetModal(); + } else if (typeof result === 'string') { + this.passwordError = result; + } + }); }, copyConfirmText() { navigator.clipboard.writeText(this.confirmText); @@ -82,7 +84,7 @@ this.selectedActions.push(id); } } -}" @keydown.escape.window="modalOpen = false; resetModal()" :class="{ 'z-40': modalOpen }" class="relative w-auto h-auto" @password-error.window="passwordError = $event.detail"> +}" @keydown.escape.window="modalOpen = false; resetModal()" :class="{ 'z-40': modalOpen }" class="relative w-auto h-auto"> @if ($customButton) @if ($buttonFullWidth) @@ -134,7 +136,7 @@ @endif -