From 8f2c24d7e90fe041bc8e3d07ba0b908bb281126e Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Sat, 18 Nov 2023 17:50:44 +0100 Subject: [PATCH 01/16] fix: reset password --- config/sentry.php | 2 +- config/version.php | 2 +- resources/views/auth/reset-password.blade.php | 2 +- versions.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/sentry.php b/config/sentry.php index 0c8ab1b30..2d3302d48 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.144', + 'release' => '4.0.0-beta.145', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index a4a00caab..9bbb31a9f 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@
@csrf - +
diff --git a/versions.json b/versions.json index ae88d1617..d2bd0be29 100644 --- a/versions.json +++ b/versions.json @@ -4,7 +4,7 @@ "version": "3.12.36" }, "v4": { - "version": "4.0.0-beta.144" + "version": "4.0.0-beta.145" } } } From 8f963adbd4727919f5d60a5ab3540c19b2c1bde2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 20 Nov 2023 10:32:06 +0100 Subject: [PATCH 02/16] fix: only report nonruntime errors --- app/Exceptions/Handler.php | 6 +- app/Http/Livewire/Dashboard.php | 1 - app/Jobs/ApplicationDeploymentJob.php | 1 - bootstrap/helpers/shared.php | 61 ++++--------------- config/sentry.php | 4 +- config/version.php | 2 +- .../views/livewire/server/form.blade.php | 2 +- versions.json | 2 +- 8 files changed, 22 insertions(+), 57 deletions(-) diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index b4f63661c..e78849a07 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -6,6 +6,7 @@ use App\Models\User; use Illuminate\Auth\AuthenticationException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; +use RuntimeException; use Sentry\Laravel\Integration; use Sentry\State\Scope; use Throwable; @@ -55,7 +56,9 @@ public function register(): void { $this->reportable(function (Throwable $e) { if (isDev()) { - ray($e); + // return; + } + if ($e instanceof RuntimeException) { return; } $this->settings = InstanceSettings::get(); @@ -74,6 +77,7 @@ function (Scope $scope) { ); } ); + ray('reporting to sentry'); Integration::captureUnhandledException($e); }); } diff --git a/app/Http/Livewire/Dashboard.php b/app/Http/Livewire/Dashboard.php index 723b00f7f..b7219864d 100644 --- a/app/Http/Livewire/Dashboard.php +++ b/app/Http/Livewire/Dashboard.php @@ -3,7 +3,6 @@ namespace App\Http\Livewire; use App\Models\Project; -use App\Models\S3Storage; use App\Models\Server; use Livewire\Component; diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index dacdbf89b..32f92aefd 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -218,7 +218,6 @@ public function handle(): void $this->next(ApplicationDeploymentStatus::FINISHED->value); $this->application->isConfigurationChanged(true); } catch (Exception $e) { - ray($e); $this->fail($e); throw $e; } finally { diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 6e3c6ca4f..1953f554d 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -93,65 +93,28 @@ function refreshSession(?Team $team = null): void } function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null) { - ray('handleError'); - ray($error); - if ($error instanceof Throwable) { - $message = $error->getMessage(); - } else { - $message = null; - } - if ($customErrorMessage) { - $message = $customErrorMessage . ' ' . $message; - } if ($error instanceof TooManyRequestsException) { if (isset($livewire)) { return $livewire->emit('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds."); } return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds."; } + + if ($error instanceof Throwable) { + $message = $error->getMessage(); + } else { + $message = null; + } + if ($customErrorMessage) { + $error->message = $customErrorMessage . ' ' . $message; + $message = $customErrorMessage . ' ' . $message; + } + if (isset($livewire)) { return $livewire->emit('error', $message); } - - throw new RuntimeException($message); + throw $error; } -function general_error_handler(Throwable $err, Livewire\Component $that = null, $isJson = false, $customErrorMessage = null): mixed -{ - try { - ray($err); - ray('ERROR OCCURRED: ' . $err->getMessage()); - if ($err instanceof QueryException) { - if ($err->errorInfo[0] === '23505') { - throw new Exception($customErrorMessage ?? 'Duplicate entry found.', '23505'); - } else if (count($err->errorInfo) === 4) { - throw new Exception($customErrorMessage ?? $err->errorInfo[3]); - } else { - throw new Exception($customErrorMessage ?? $err->errorInfo[2]); - } - } elseif ($err instanceof TooManyRequestsException) { - throw new Exception($customErrorMessage ?? "Too many requests. Please try again in {$err->secondsUntilAvailable} seconds."); - } else { - if ($err->getMessage() === 'This action is unauthorized.') { - return redirect()->route('dashboard')->with('error', $customErrorMessage ?? $err->getMessage()); - } - throw new Exception($customErrorMessage ?? $err->getMessage()); - } - } catch (\Throwable $e) { - if ($that) { - return $that->emit('error', $customErrorMessage ?? $e->getMessage()); - } elseif ($isJson) { - return response()->json([ - 'code' => $e->getCode(), - 'error' => $e->getMessage(), - ]); - } else { - ray($customErrorMessage); - ray($e); - return $customErrorMessage ?? $e->getMessage(); - } - } -} - function get_route_parameters(): array { return Route::current()->parameters(); diff --git a/config/sentry.php b/config/sentry.php index 2d3302d48..38c7355e9 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -3,11 +3,11 @@ return [ // @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/ - 'dsn' => 'https://c35fe90ee56e18b220bb55e8217d4839@o1082494.ingest.sentry.io/4505347448045568', + 'dsn' => 'https://396748153b19c469f5ceff50f1664323@o1082494.ingest.sentry.io/4505347448045568', // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.145', + 'release' => '4.0.0-beta.146', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index 9bbb31a9f..8c257643a 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ +

You could lost a lot of functionalities if you change the server details of the server where Coolify is diff --git a/versions.json b/versions.json index d2bd0be29..905876d7a 100644 --- a/versions.json +++ b/versions.json @@ -4,7 +4,7 @@ "version": "3.12.36" }, "v4": { - "version": "4.0.0-beta.145" + "version": "4.0.0-beta.146" } } } From 912b0a263e4e1ca36115bb97e91c38fd6ae90b98 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 20 Nov 2023 11:35:31 +0100 Subject: [PATCH 03/16] feat: gpu enabled containers feat: move advanced settings to different view --- .../Livewire/Project/Application/Advanced.php | 54 ++++++++++++++++ .../Livewire/Project/Application/General.php | 61 +++---------------- app/Jobs/ApplicationDeploymentJob.php | 22 +++++++ app/View/Components/Forms/Textarea.php | 2 +- .../2023_11_20_094628_add_gpu_settings.php | 36 +++++++++++ .../views/components/forms/checkbox.blade.php | 2 +- .../project/application/advanced.blade.php | 55 +++++++++++++++++ .../project/application/general.blade.php | 26 +------- .../application/configuration.blade.php | 5 ++ 9 files changed, 185 insertions(+), 78 deletions(-) create mode 100644 app/Http/Livewire/Project/Application/Advanced.php create mode 100644 database/migrations/2023_11_20_094628_add_gpu_settings.php create mode 100644 resources/views/livewire/project/application/advanced.blade.php diff --git a/app/Http/Livewire/Project/Application/Advanced.php b/app/Http/Livewire/Project/Application/Advanced.php new file mode 100644 index 000000000..552a3873a --- /dev/null +++ b/app/Http/Livewire/Project/Application/Advanced.php @@ -0,0 +1,54 @@ + 'boolean|required', + 'application.settings.is_git_lfs_enabled' => 'boolean|required', + 'application.settings.is_preview_deployments_enabled' => 'boolean|required', + 'application.settings.is_auto_deploy_enabled' => 'boolean|required', + 'application.settings.is_force_https_enabled' => 'boolean|required', + 'application.settings.is_log_drain_enabled' => 'boolean|required', + 'application.settings.is_gpu_enabled' => 'boolean|required', + 'application.settings.gpu_driver' => 'string|required', + 'application.settings.gpu_count' => 'string|required', + 'application.settings.gpu_device_ids' => 'string|required', + 'application.settings.gpu_options' => 'string|required', + ]; + public function instantSave() + { + if ($this->application->settings->is_log_drain_enabled) { + if (!$this->application->destination->server->isLogDrainEnabled()) { + $this->application->settings->is_log_drain_enabled = false; + $this->emit('error', 'Log drain is not enabled on this server.'); + return; + } + } + if ($this->application->settings->is_force_https_enabled) { + $this->emit('resetDefaultLabels', false); + } + $this->application->settings->save(); + $this->emit('success', 'Settings saved.'); + } + public function submit() { + if ($this->application->settings->gpu_count && $this->application->settings->gpu_device_ids) { + $this->emit('error', 'You cannot set both GPU count and GPU device IDs.'); + $this->application->settings->gpu_count = null; + $this->application->settings->gpu_device_ids = null; + $this->application->settings->save(); + return; + } + $this->application->settings->save(); + $this->emit('success', 'Settings saved.'); + } + public function render() + { + return view('livewire.project.application.advanced'); + } +} diff --git a/app/Http/Livewire/Project/Application/General.php b/app/Http/Livewire/Project/Application/General.php index 63dbeba23..27c5023c3 100644 --- a/app/Http/Livewire/Project/Application/General.php +++ b/app/Http/Livewire/Project/Application/General.php @@ -26,14 +26,10 @@ class General extends Component public bool $isConfigurationChanged = false; public bool $is_static; - public bool $is_git_submodules_enabled; - public bool $is_git_lfs_enabled; - public bool $is_debug_enabled; - public bool $is_preview_deployments_enabled; - public bool $is_auto_deploy_enabled; - public bool $is_force_https_enabled; - public bool $is_log_drain_enabled; + protected $listeners = [ + 'resetDefaultLabels' + ]; protected $rules = [ 'application.name' => 'required', 'application.description' => 'nullable', @@ -56,6 +52,7 @@ class General extends Component 'application.dockerfile_location' => 'nullable', 'application.custom_labels' => 'nullable', 'application.dockerfile_target_build' => 'nullable', + 'application.settings.is_static' => 'boolean|required', ]; protected $validationAttributes = [ 'application.name' => 'name', @@ -79,6 +76,7 @@ class General extends Component 'application.dockerfile_location' => 'Dockerfile location', 'application.custom_labels' => 'Custom labels', 'application.dockerfile_target_build' => 'Dockerfile target build', + 'application.settings.is_static' => 'Is static', ]; public function mount() @@ -93,18 +91,13 @@ public function mount() } else { $this->customLabels = str($this->application->custom_labels)->replace(',', "\n"); } - if (data_get($this->application, 'settings')) { - $this->is_static = $this->application->settings->is_static; - $this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled; - $this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled; - $this->is_debug_enabled = $this->application->settings->is_debug_enabled; - $this->is_preview_deployments_enabled = $this->application->settings->is_preview_deployments_enabled; - $this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled; - $this->is_force_https_enabled = $this->application->settings->is_force_https_enabled; - $this->is_log_drain_enabled = $this->application->settings->is_log_drain_enabled; - } $this->checkLabelUpdates(); } + public function instantSave() + { + $this->application->settings->save(); + $this->emit('success', 'Settings saved.'); + } public function updatedApplicationBuildPack() { if ($this->application->build_pack !== 'nixpacks') { @@ -121,40 +114,6 @@ public function checkLabelUpdates() $this->labelsChanged = false; } } - public function instantSave() - { - // @TODO: find another way - if possible - $force_https = $this->application->settings->is_force_https_enabled; - $this->application->settings->is_static = $this->is_static; - if ($this->is_static) { - $this->application->ports_exposes = 80; - } else { - $this->application->ports_exposes = 3000; - } - $this->application->settings->is_git_submodules_enabled = $this->is_git_submodules_enabled; - $this->application->settings->is_git_lfs_enabled = $this->is_git_lfs_enabled; - $this->application->settings->is_debug_enabled = $this->is_debug_enabled; - $this->application->settings->is_preview_deployments_enabled = $this->is_preview_deployments_enabled; - $this->application->settings->is_auto_deploy_enabled = $this->is_auto_deploy_enabled; - $this->application->settings->is_force_https_enabled = $this->is_force_https_enabled; - $this->application->settings->is_log_drain_enabled = $this->is_log_drain_enabled; - if ($this->is_log_drain_enabled) { - if (!$this->application->destination->server->isLogDrainEnabled()) { - $this->application->settings->is_log_drain_enabled = $this->is_log_drain_enabled = false; - $this->emit('error', 'Log drain is not enabled on the server. Please enable it first.'); - return; - } - } - $this->application->settings->save(); - $this->application->save(); - $this->application->refresh(); - $this->emit('success', 'Application settings updated!'); - $this->checkLabelUpdates(); - $this->isConfigurationChanged = $this->application->isConfigurationChanged(); - if ($force_https !== $this->is_force_https_enabled) { - $this->resetDefaultLabels(false); - } - } public function getWildcardDomain() { diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 32f92aefd..29f298918 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -873,6 +873,27 @@ private function generate_compose_file() ] ]; } + if ($this->application->settings->is_gpu_enabled) { + ray('asd'); + $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [ + [ + 'driver' => data_get($this->application, 'settings.gpu_driver', 'nvidia'), + 'capabilities' => ['gpu'], + 'options' => data_get($this->application, 'settings.gpu_options', []) + ] + ]; + if (data_get($this->application, 'settings.gpu_count')) { + $count = data_get($this->application, 'settings.gpu_count'); + ray($count); + if ($count === 'all') { + $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = $count; + } else { + $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count; + } + } else if (data_get($this->application, 'settings.gpu_device_ids')) { + $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($this->application, 'settings.gpu_device_ids'); + } + } if ($this->application->isHealthcheckDisabled()) { data_forget($docker_compose, 'services.' . $this->container_name . '.healthcheck'); } @@ -891,6 +912,7 @@ private function generate_compose_file() // 'dockerfile' => $this->workdir . $this->dockerfile_location, // ]; // } + ray($docker_compose); $this->docker_compose = Yaml::dump($docker_compose, 10); $this->docker_compose_base64 = base64_encode($this->docker_compose); $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]); diff --git a/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php index 69c618fb4..50ffe77f7 100644 --- a/app/View/Components/Forms/Textarea.php +++ b/app/View/Components/Forms/Textarea.php @@ -38,7 +38,7 @@ public function render(): View|Closure|string if (is_null($this->id)) $this->id = new Cuid2(7); if (is_null($this->name)) $this->name = $this->id; - $this->label = Str::title($this->label); + // $this->label = Str::title($this->label); return view('components.forms.textarea'); } } diff --git a/database/migrations/2023_11_20_094628_add_gpu_settings.php b/database/migrations/2023_11_20_094628_add_gpu_settings.php new file mode 100644 index 000000000..9aeb255d9 --- /dev/null +++ b/database/migrations/2023_11_20_094628_add_gpu_settings.php @@ -0,0 +1,36 @@ +boolean('is_gpu_enabled')->default(false); + $table->string('gpu_driver')->default('nvidia'); + $table->string('gpu_count')->nullable(); + $table->string('gpu_device_ids')->nullable(); + $table->longText('gpu_options')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_settings', function (Blueprint $table) { + $table->dropColumn('is_gpu_enabled'); + $table->dropColumn('gpu_driver'); + $table->dropColumn('gpu_count'); + $table->dropColumn('gpu_device_ids'); + $table->dropColumn('gpu_options'); + }); + } +}; diff --git a/resources/views/components/forms/checkbox.blade.php b/resources/views/components/forms/checkbox.blade.php index de4abb87a..63c340d72 100644 --- a/resources/views/components/forms/checkbox.blade.php +++ b/resources/views/components/forms/checkbox.blade.php @@ -1,4 +1,4 @@ -

+
@if ($application->could_set_build_commands())
-
@endif @@ -112,29 +112,5 @@ Reset to Coolify Generated Labels
-

Advanced

-
- - - @if ($application->git_based()) - - - - - - @endif - - {{-- - - --}} -
diff --git a/resources/views/project/application/configuration.blade.php b/resources/views/project/application/configuration.blade.php index c8a85e82b..502f436db 100644 --- a/resources/views/project/application/configuration.blade.php +++ b/resources/views/project/application/configuration.blade.php @@ -5,6 +5,8 @@
General + Advanced @if ($application->build_pack !== 'static')
+
+ +
From e33fec0e1af484062b7556ecd25cae112963317f Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 20 Nov 2023 11:37:09 +0100 Subject: [PATCH 04/16] Refactor checkbox component and update GPU settings helper links --- .../views/components/forms/checkbox.blade.php | 15 +-------------- .../project/application/advanced.blade.php | 4 ++-- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/resources/views/components/forms/checkbox.blade.php b/resources/views/components/forms/checkbox.blade.php index 63c340d72..6041eb793 100644 --- a/resources/views/components/forms/checkbox.blade.php +++ b/resources/views/components/forms/checkbox.blade.php @@ -7,20 +7,7 @@ {{ $id }} @endif @if ($helper) -
-
- - - -
- -
+ @endif merge(['class' => $defaultClass]) }} diff --git a/resources/views/livewire/project/application/advanced.blade.php b/resources/views/livewire/project/application/advanced.blade.php index 2a4a87ae9..1193456b7 100644 --- a/resources/views/livewire/project/application/advanced.blade.php +++ b/resources/views/livewire/project/application/advanced.blade.php @@ -25,7 +25,7 @@ @endif
- @if ($application->settings->is_gpu_enabled) Save @@ -37,7 +37,7 @@
From f88e3c5b292e9c789dd4bffb6519aaf805a83d2d Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 20 Nov 2023 13:49:10 +0100 Subject: [PATCH 05/16] feat: push locally built image to docker registry ui: fixes here and there --- .../Livewire/Project/Application/Rollback.php | 5 +- app/Jobs/ApplicationDeploymentJob.php | 148 ++++++++++++------ app/Traits/ExecuteRemoteCommand.php | 12 +- bootstrap/helpers/constants.php | 1 + bootstrap/helpers/remoteProcess.php | 7 +- .../application/deployment-logs.blade.php | 3 +- .../project/application/general.blade.php | 30 +++- .../project/application/rollback.blade.php | 12 +- .../application/configuration.blade.php | 2 +- 9 files changed, 152 insertions(+), 68 deletions(-) diff --git a/app/Http/Livewire/Project/Application/Rollback.php b/app/Http/Livewire/Project/Application/Rollback.php index dcebd4d93..4c363421d 100644 --- a/app/Http/Livewire/Project/Application/Rollback.php +++ b/app/Http/Livewire/Project/Application/Rollback.php @@ -38,10 +38,10 @@ public function rollbackImage($commit) ]); } - public function loadImages() + public function loadImages($showToast = false) { try { - $image = $this->application->uuid; + $image = $this->application->docker_registry_image_name ?? $this->application->uuid; if ($this->application->destination->server->isFunctional()) { $output = instant_remote_process([ "docker inspect --format='{{.Config.Image}}' {$this->application->uuid}", @@ -66,6 +66,7 @@ public function loadImages() ]; })->toArray(); } + $showToast && $this->emit('success', 'Images loaded.'); return []; } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 29f298918..06b065961 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -24,6 +24,7 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Collection; use Illuminate\Support\Str; +use RuntimeException; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; use Throwable; @@ -197,6 +198,12 @@ public function handle(): void try { if ($this->restart_only && $this->application->build_pack !== 'dockerimage') { $this->just_restart(); + if ($this->server->isProxyShouldRun()) { + dispatch(new ContainerStatusJob($this->server)); + } + $this->next(ApplicationDeploymentStatus::FINISHED->value); + $this->application->isConfigurationChanged(true); + return; } else if ($this->application->dockerfile) { $this->deploy_simple_dockerfile(); } else if ($this->application->build_pack === 'dockerimage') { @@ -215,6 +222,9 @@ public function handle(): void if ($this->server->isProxyShouldRun()) { dispatch(new ContainerStatusJob($this->server)); } + if ($this->application->docker_registry_image_name) { + $this->push_to_docker_registry(); + } $this->next(ApplicationDeploymentStatus::FINISHED->value); $this->application->isConfigurationChanged(true); } catch (Exception $e) { @@ -255,7 +265,38 @@ public function handle(): void ); } } - + private function push_to_docker_registry() + { + try { + instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server); + $this->execute_remote_command( + [ + "echo '\n----------------------------------------'", + ], + ["echo -n 'Pushing image to docker registry ({$this->production_image_name}).'"], + [ + executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'ignore_errors' => true, 'hidden' => true + ], + ); + if ($this->application->docker_registry_image_tag) { + // Tag image with latest + $this->execute_remote_command( + ['echo -n "Tagging and pushing image with latest tag."'], + [ + executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true + ], + [ + executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true + ], + ); + } + $this->execute_remote_command([ + "echo -n 'Image pushed to docker registry.'" + ]); + } catch (Exception $e) { + ray($e); + } + } // private function deploy_docker_compose() // { // $dockercompose_base64 = base64_encode($this->application->dockercompose); @@ -303,12 +344,14 @@ private function generate_image_names() $this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build"); $this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}"); } else { - $tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}"); - if (strlen($tag) > 128) { - $tag = $tag->substr(0, 128); + $this->dockerImageTag = str($this->commit)->substr(0, 128); + if ($this->application->docker_registry_image_name) { + $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build"); + $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}"); + } else { + $this->build_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}-build"); + $this->production_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}"); } - $this->build_image_name = Str::lower("{$this->application->uuid}:{$tag}-build"); - $this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}"); } } private function just_restart() @@ -322,17 +365,29 @@ private function just_restart() $this->check_git_if_build_needed(); $this->set_base_dir(); $this->generate_image_names(); - $this->execute_remote_command([ - "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" - ]); + $this->check_image_locally_or_remotely(); if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) { $this->generate_compose_file(); $this->rolling_update(); return; } + throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.'); + } + private function check_image_locally_or_remotely() + { $this->execute_remote_command([ - "echo 'Cannot find image {$this->production_image_name} locally. Please redeploy the application.'", + "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" ]); + if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) { + $this->execute_remote_command([ + "echo 'Cannot find image locally. Pulling from docker registry.'", 'type' => 'err' + ], [ + "docker pull {$this->production_image_name} 2>/dev/null", "ignore_errors" => true, "hidden" => true + ]); + $this->execute_remote_command([ + "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" + ]); + } } private function save_environment_variables() { @@ -419,12 +474,10 @@ private function deploy_nixpacks_buildpack() $this->set_base_dir(); $this->generate_image_names(); if (!$this->force_rebuild) { - $this->execute_remote_command([ - "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" - ]); - if (Str::of($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) { + $this->check_image_locally_or_remotely(); + if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) { $this->execute_remote_command([ - "echo 'No configuration changed & Docker Image found locally with the same Git Commit SHA {$this->application->uuid}:{$this->commit}. Build step skipped.'", + "echo 'No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.'", ]); $this->generate_compose_file(); $this->rolling_update(); @@ -467,12 +520,18 @@ private function rolling_update() { if (count($this->application->ports_mappings_array) > 0) { $this->execute_remote_command( + [ + "echo '\n----------------------------------------'", + ], ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"], ); $this->stop_running_container(force: true); $this->start_by_compose_file(); } else { $this->execute_remote_command( + [ + "echo '\n----------------------------------------'", + ], ["echo -n 'Rolling update started.'"], ); $this->start_by_compose_file(); @@ -488,10 +547,10 @@ private function health_check() } // ray('New container name: ', $this->container_name); if ($this->container_name) { - $counter = 0; + $counter = 1; $this->execute_remote_command( [ - "echo 'Waiting for healthcheck to pass on the new version of your application.'" + "echo 'Waiting for healthcheck to pass on the new container.'" ] ); if ($this->full_healthcheck_url) { @@ -503,9 +562,6 @@ private function health_check() } while ($counter < $this->application->health_check_retries) { $this->execute_remote_command( - [ - "echo 'Attempt {$counter} of {$this->application->health_check_retries}'" - ], [ "docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}", "hidden" => true, @@ -515,17 +571,17 @@ private function health_check() ); $this->execute_remote_command( [ - "echo 'New version healthcheck status: {$this->saved_outputs->get('health_check')}'" + "echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'" ], ); if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) { $this->newVersionIsHealthy = true; + $this->application->update(['status' => 'running']); $this->execute_remote_command( [ - "echo 'Rolling update completed.'" + "echo 'New container is healthy.'" ], ); - $this->application->update(['status' => 'running']); break; } $counter++; @@ -588,6 +644,7 @@ private function set_base_dir() [ "echo -n 'Setting base directory to {$this->workdir}.'" ], + ["echo '\n----------------------------------------'"] ); } private function check_git_if_build_needed() @@ -630,6 +687,9 @@ private function clone_repository() { $importCommands = $this->generate_git_import_commands(); $this->execute_remote_command( + [ + "echo '\n----------------------------------------'", + ], [ "echo -n 'Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '" ], @@ -677,7 +737,7 @@ private function generate_git_import_commands() $this->fullRepoUrl = $this->customRepository; $private_key = data_get($this->application, 'private_key.private_key'); if (is_null($private_key)) { - throw new Exception('Private key not found. Please add a private key to the application and try again.'); + throw new RuntimeException('Private key not found. Please add a private key to the application and try again.'); } $private_key = base64_encode($private_key); $git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}"; @@ -736,16 +796,10 @@ private function cleanup_git() private function generate_nixpacks_confs() { - - $this->execute_remote_command( - [ - "echo -n 'Generating nixpacks configuration.'", - ] - ); $nixpacks_command = $this->nixpacks_build_cmd(); $this->execute_remote_command( [ - "echo -n Running: $nixpacks_command", + "echo -n 'Generating nixpacks configuration with: $nixpacks_command'", ], [executeInDocker($this->deployment_uuid, $nixpacks_command)], [executeInDocker($this->deployment_uuid, "cp {$this->workdir}/.nixpacks/Dockerfile {$this->workdir}/Dockerfile")], @@ -884,7 +938,6 @@ private function generate_compose_file() ]; if (data_get($this->application, 'settings.gpu_count')) { $count = data_get($this->application, 'settings.gpu_count'); - ray($count); if ($count === 'all') { $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = $count; } else { @@ -912,7 +965,6 @@ private function generate_compose_file() // 'dockerfile' => $this->workdir . $this->dockerfile_location, // ]; // } - ray($docker_compose); $this->docker_compose = Yaml::dump($docker_compose, 10); $this->docker_compose_base64 = base64_encode($this->docker_compose); $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]); @@ -1021,9 +1073,12 @@ private function build_image() "echo -n 'Static deployment. Copying static assets to the image.'", ]); } else { - $this->execute_remote_command([ - "echo -n 'Building docker image for your application. To check the current progress, click on Show Debug Logs.'", - ]); + $this->execute_remote_command( + [ + "echo -n 'Building docker image started.'", + ], + ["echo -n 'To check the current progress, click on Show Debug Logs.'"] + ); } if ($this->application->settings->is_static || $this->application->build_pack === 'static') { @@ -1105,12 +1160,14 @@ private function build_image() ]); } } + $this->execute_remote_command([ + "echo -n 'Building docker image completed.'", + ]); } private function stop_running_container(bool $force = false) { - $this->execute_remote_command(["echo -n 'Removing old version of your application.'"]); - + $this->execute_remote_command(["echo -n 'Removing old container.'"]); if ($this->newVersionIsHealthy || $force) { $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); if ($this->pull_request_id !== 0) { @@ -1128,9 +1185,14 @@ private function stop_running_container(bool $force = false) [executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], ); }); + $this->execute_remote_command( + [ + "echo 'Rolling update completed.'" + ], + ); } else { $this->execute_remote_command( - ["echo -n 'New version is not healthy, rolling back to the old version.'"], + ["echo -n 'New container is not healthy, rolling back to the old container.'"], [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], ); } @@ -1142,12 +1204,10 @@ private function start_by_compose_file() $this->execute_remote_command( ["echo -n 'Pulling latest images from the registry.'"], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true], - ["echo -n 'Starting application (could take a while).'"], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], ); } else { $this->execute_remote_command( - ["echo -n 'Starting application (could take a while).'"], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], ); } @@ -1206,9 +1266,9 @@ private function next(string $status) public function failed(Throwable $exception): void { $this->execute_remote_command( - ["echo 'Oops something is not okay, are you okay? 😢'"], - ["echo '{$exception->getMessage()}'"], - ["echo -n 'Deployment failed. Removing the new version of your application.'"], + ["echo 'Oops something is not okay, are you okay? 😢'", 'type' => 'err'], + ["echo '{$exception->getMessage()}'", 'type' => 'err'], + ["echo -n 'Deployment failed. Removing the new version of your application.'", 'type' => 'err'], [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true] ); diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index 1244fde28..0a53c5bf6 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -32,16 +32,20 @@ public function execute_remote_command(...$commands) throw new \RuntimeException('Command is not set'); } $hidden = data_get($single_command, 'hidden', false); + $customType = data_get($single_command, 'type'); $ignore_errors = data_get($single_command, 'ignore_errors', false); $this->save = data_get($single_command, 'save'); $remote_command = generateSshCommand($this->server, $command); - $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) { + $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType) { $output = Str::of($output)->trim(); + if ($output->startsWith('╔')) { + $output = "\n" . $output; + } $new_log_entry = [ - 'command' => $command, - 'output' => $output, - 'type' => $type === 'err' ? 'stderr' : 'stdout', + 'command' => remove_iip($command), + 'output' => remove_iip($output), + 'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout', 'timestamp' => Carbon::now('UTC'), 'hidden' => $hidden, 'batch' => static::$batch_counter, diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index e844efea9..d26932165 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -1,5 +1,6 @@ '; const DATABASE_TYPES = ['postgresql', 'redis', 'mongodb', 'mysql', 'mariadb']; const VALID_CRON_STRINGS = [ 'every_minute' => '* * * * *', diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index c1ed577b5..948e47329 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -170,10 +170,13 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d $i['timestamp'] = Carbon::parse($i['timestamp'])->format('Y-M-d H:i:s.u'); return $i; }); - return $formatted; } - +function remove_iip($text) +{ + $text = preg_replace('/x-access-token:.*?(?=@)/', "x-access-token:" . REDACTED, $text); + return preg_replace('/\x1b\[[0-9;]*m/', '', $text); +} function refresh_server_connection(?PrivateKey $private_key = null) { if (is_null($private_key)) { diff --git a/resources/views/livewire/project/application/deployment-logs.blade.php b/resources/views/livewire/project/application/deployment-logs.blade.php index a9656556e..09cb8232e 100644 --- a/resources/views/livewire/project/application/deployment-logs.blade.php +++ b/resources/views/livewire/project/application/deployment-logs.blade.php @@ -48,9 +48,8 @@ class="fixed top-4 right-16" x-on:click="toggleScroll"> $line['type'] == 'stdout', - 'text-error' => $line['type'] == 'stderr', 'text-warning' => $line['hidden'], + 'text-error' => $line['type'] == 'stderr', ])>[{{ $line['timestamp'] }}] @if ($line['hidden'])
COMMAND:
{{ $line['command'] }}

OUTPUT: @endif{{ $line['output'] }}@if ($line['hidden']) diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 5fdcdd205..7a498d6b3 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -40,12 +40,33 @@
@if ($application->could_set_build_commands())
-
@endif @endif +

Docker Registry

+ @if ($application->build_pack !== 'dockerimage') +
Push the built image to a docker registry. More info here.
+ @endif +
+ @if ($application->build_pack === 'dockerimage') + + + @else + + + @endif + +
@if ($application->build_pack !== 'dockerimage')

Build

@@ -64,8 +85,6 @@ @endif @endif - -
@@ -88,11 +107,6 @@ @endif @endif
- @else -
- - -
@endif @if ($application->dockerfile) diff --git a/resources/views/livewire/project/application/rollback.blade.php b/resources/views/livewire/project/application/rollback.blade.php index e9c23fdf6..09e69c7c1 100644 --- a/resources/views/livewire/project/application/rollback.blade.php +++ b/resources/views/livewire/project/application/rollback.blade.php @@ -1,12 +1,12 @@

Rollback

- Reload Available Images + Reload Available Images
-
You can easily rollback to a previously built image quickly.
+
You can easily rollback to a previously built (local) images quickly.
- @foreach ($images as $image) + @forelse ($images as $image)
@@ -25,14 +25,16 @@ Rollback @else - + Rollback @endif
- @endforeach + @empty +
No images found locally.
+ @endforelse
diff --git a/resources/views/project/application/configuration.blade.php b/resources/views/project/application/configuration.blade.php index 502f436db..d6eb25c10 100644 --- a/resources/views/project/application/configuration.blade.php +++ b/resources/views/project/application/configuration.blade.php @@ -36,7 +36,7 @@ @endif @if ($application->build_pack !== 'static') Health Checks + @click.prevent="activeTab = 'health'; window.location.hash = 'health'" href="#">Healthchecks @endif Date: Mon, 20 Nov 2023 13:58:31 +0100 Subject: [PATCH 06/16] Update Docker Registry link in general.blade.php --- resources/views/livewire/project/application/general.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 7a498d6b3..d796daee8 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -50,7 +50,7 @@

Docker Registry

@if ($application->build_pack !== 'dockerimage')
Push the built image to a docker registry. More info here.
+ href="https://coolify.io/docs/docker-registries" target="_blank">here. @endif
@if ($application->build_pack === 'dockerimage') From 608f0b7840869395517cc5ddbf08a1ec6a305855 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 20 Nov 2023 14:23:11 +0100 Subject: [PATCH 07/16] Refactor Docker image name generation and push to registry --- app/Jobs/ApplicationDeploymentJob.php | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 06b065961..99aecba86 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -275,7 +275,7 @@ private function push_to_docker_registry() ], ["echo -n 'Pushing image to docker registry ({$this->production_image_name}).'"], [ - executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'ignore_errors' => true, 'hidden' => true + executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true ], ); if ($this->application->docker_registry_image_tag) { @@ -294,6 +294,9 @@ private function push_to_docker_registry() "echo -n 'Image pushed to docker registry.'" ]); } catch (Exception $e) { + $this->execute_remote_command( + ["echo -n 'Failed to push image to docker registry. Please check debug logs for more information.'"], + ); ray($e); } } @@ -336,13 +339,23 @@ private function push_to_docker_registry() private function generate_image_names() { if ($this->application->dockerfile) { - $this->build_image_name = Str::lower("{$this->application->uuid}:build"); - $this->production_image_name = Str::lower("{$this->application->uuid}:latest"); + if ($this->application->docker_registry_image_name) { + $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:build"); + $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:latest"); + } else { + $this->build_image_name = Str::lower("{$this->application->uuid}:build"); + $this->production_image_name = Str::lower("{$this->application->uuid}:latest"); + } } else if ($this->application->build_pack === 'dockerimage') { $this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}"); } else if ($this->pull_request_id !== 0) { - $this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build"); - $this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}"); + if ($this->application->docker_registry_image_name) { + $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build"); + $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}"); + } else { + $this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build"); + $this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}"); + } } else { $this->dockerImageTag = str($this->commit)->substr(0, 128); if ($this->application->docker_registry_image_name) { @@ -380,8 +393,6 @@ private function check_image_locally_or_remotely() ]); if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) { $this->execute_remote_command([ - "echo 'Cannot find image locally. Pulling from docker registry.'", 'type' => 'err' - ], [ "docker pull {$this->production_image_name} 2>/dev/null", "ignore_errors" => true, "hidden" => true ]); $this->execute_remote_command([ @@ -644,7 +655,6 @@ private function set_base_dir() [ "echo -n 'Setting base directory to {$this->workdir}.'" ], - ["echo '\n----------------------------------------'"] ); } private function check_git_if_build_needed() From 30f8e8f232fec6768a29f952cdc037a792b2b4b4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 20 Nov 2023 15:01:35 +0100 Subject: [PATCH 08/16] fix: handle different label formats in services --- app/Models/Service.php | 118 ++++++++++++------ .../project/service/stack-form.blade.php | 6 +- 2 files changed, 85 insertions(+), 39 deletions(-) diff --git a/app/Models/Service.php b/app/Models/Service.php index cf78e8260..8cd195bce 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -53,50 +53,83 @@ public function extraFields() $image = str($application->image)->before(':')->value(); switch ($image) { case str($image)->contains('minio'): + $data = collect([]); $console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first(); $s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first(); $admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_MINIO')->first(); + if (is_null($admin_user)) { + $admin_user = $this->environment_variables()->where('key', 'MINIO_ROOT_USER')->first(); + } $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MINIO')->first(); - $fields->put('MinIO', [ - 'Console URL' => [ - 'key' => data_get($console_url, 'key'), - 'value' => data_get($console_url, 'value'), - 'rules' => 'required|url', - ], - 'S3 API URL' => [ - 'key' => data_get($s3_api_url, 'key'), - 'value' => data_get($s3_api_url, 'value'), - 'rules' => 'required|url', - ], - 'Admin User' => [ - 'key' => data_get($admin_user, 'key'), - 'value' => data_get($admin_user, 'value'), - 'rules' => 'required', - ], - 'Admin Password' => [ - 'key' => data_get($admin_password, 'key'), - 'value' => data_get($admin_password, 'value'), - 'rules' => 'required', - 'isPassword' => true, - ], - ]); + if (is_null($admin_password)) { + $admin_password = $this->environment_variables()->where('key', 'MINIO_ROOT_PASSWORD')->first(); + } + + if ($console_url) { + $data = $data->merge([ + 'Console URL' => [ + 'key' => data_get($console_url, 'key'), + 'value' => data_get($console_url, 'value'), + 'rules' => 'required|url', + ], + ]); + } + if ($s3_api_url) { + $data = $data->merge([ + 'S3 API URL' => [ + 'key' => data_get($s3_api_url, 'key'), + 'value' => data_get($s3_api_url, 'value'), + 'rules' => 'required|url', + ], + ]); + } + if ($admin_user) { + $data = $data->merge([ + 'Admin User' => [ + 'key' => data_get($admin_user, 'key'), + 'value' => data_get($admin_user, 'value'), + 'rules' => 'required', + ], + ]); + } + if ($admin_password) { + $data = $data->merge([ + 'Admin Password' => [ + 'key' => data_get($admin_password, 'key'), + 'value' => data_get($admin_password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + + $fields->put('MinIO', $data->toArray()); break; case str($image)->contains('weblate'): + $data = collect([]); $admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first(); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_WEBLATE')->first(); - $fields->put('Weblate', [ - 'Admin Email' => [ - 'key' => data_get($admin_email, 'key'), - 'value' => data_get($admin_email, 'value'), - 'rules' => 'required|email', - ], - 'Admin Password' => [ - 'key' => data_get($admin_password, 'key'), - 'value' => data_get($admin_password, 'value'), - 'rules' => 'required', - 'isPassword' => true, - ], - ]); + + if ($admin_email) { + $data = $data->merge([ + 'Admin Email' => [ + 'key' => data_get($admin_email, 'key'), + 'value' => data_get($admin_email, 'value'), + 'rules' => 'required|email', + ], + ]); + } + if ($admin_password) { + $data = $data->merge([ + 'Admin Password' => [ + 'key' => data_get($admin_password, 'key'), + 'value' => data_get($admin_password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + $fields->put('Weblate', $data); } } $databases = $this->databases()->get(); @@ -367,6 +400,19 @@ public function parse(bool $isNew = false): Collection $serviceNetworks = collect(data_get($service, 'networks', [])); $serviceVariables = collect(data_get($service, 'environment', [])); $serviceLabels = collect(data_get($service, 'labels', [])); + if ($serviceLabels->count() > 0) { + $removedLabels = collect([]); + $serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) { + if (!str($serviceLabel)->contains('=')) { + $removedLabels->put($serviceLabelName, $serviceLabel); + return false; + } + return $serviceLabel; + }); + foreach($removedLabels as $removedLabelName =>$removedLabel) { + $serviceLabels->push("$removedLabelName=$removedLabel"); + } + } $containerName = "$serviceName-{$this->uuid}"; diff --git a/resources/views/livewire/project/service/stack-form.blade.php b/resources/views/livewire/project/service/stack-form.blade.php index 1f6bd61ae..c8a853b37 100644 --- a/resources/views/livewire/project/service/stack-form.blade.php +++ b/resources/views/livewire/project/service/stack-form.blade.php @@ -17,10 +17,10 @@

Service Specific Configuration

- @foreach ($fields as $serviceName => $fields) - $field) + @endforeach
From 6cdba17aca07391dd4c555453ed58ac47c242c5b Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 20 Nov 2023 15:16:23 +0100 Subject: [PATCH 09/16] Update token retrieval in reset-password.blade.php --- resources/views/auth/reset-password.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/auth/reset-password.blade.php b/resources/views/auth/reset-password.blade.php index f605de14d..ae417a0d9 100644 --- a/resources/views/auth/reset-password.blade.php +++ b/resources/views/auth/reset-password.blade.php @@ -12,7 +12,7 @@
@csrf - +
From 4974ce6edafc36cf253aed8478bd21afb4ab701e Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 21 Nov 2023 08:41:43 +0100 Subject: [PATCH 10/16] Update release version to 4.0.0-beta.145 --- config/sentry.php | 2 +- config/version.php | 2 +- versions.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/sentry.php b/config/sentry.php index 38c7355e9..6460f9b65 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.146', + 'release' => '4.0.0-beta.145', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index 8c257643a..9bbb31a9f 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ Date: Tue, 21 Nov 2023 09:01:52 +0100 Subject: [PATCH 11/16] Add tracing option to Sentry configuration --- config/sentry.php | 1 + 1 file changed, 1 insertion(+) diff --git a/config/sentry.php b/config/sentry.php index 6460f9b65..a09025958 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -76,6 +76,7 @@ 'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', false), // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces-sample-rate + 'enable_tracing' => env('SENTRY_ENABLE_TRACING', false), 'traces_sample_rate' => 0.2, 'profiles_sample_rate' => env('SENTRY_PROFILES_SAMPLE_RATE') === null ? null : (float)env('SENTRY_PROFILES_SAMPLE_RATE'), From e78b6758d833e01431918df3b6c2315ab390498f Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 21 Nov 2023 11:39:19 +0100 Subject: [PATCH 12/16] feat: add docker engine support install script to rhel based systems --- app/Actions/Server/InstallDocker.php | 51 +++++++++++++++++----------- app/Http/Livewire/Server/Form.php | 21 +++++++----- app/Http/Livewire/Server/Show.php | 2 +- app/Models/Server.php | 47 +++++++++++++++++-------- bootstrap/helpers/constants.php | 5 +++ 5 files changed, 83 insertions(+), 43 deletions(-) diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index e99e8a11b..a39799d88 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -9,8 +9,9 @@ class InstallDocker { use AsAction; - public function handle(Server $server) + public function handle(Server $server, $supported_os_type) { + ray('Installing Docker on server: ' . $server->name . ' (' . $server->ip . ')' . ' with OS: ' . $supported_os_type); $dockerVersion = '24.0'; $config = base64_encode('{ "log-driver": "json-file", @@ -27,36 +28,48 @@ public function handle(Server $server) 'server_id' => $server->id, ]); } - + $command = collect([]); if (isDev() && $server->id === 0) { - $command = [ - "echo '####### Installing Prerequisites...'", + $command = $command->merge([ + "echo 'Installing Prerequisites...'", "sleep 1", - "echo '####### Installing/updating Docker Engine...'", - "echo '####### Configuring Docker Engine (merging existing configuration with the required)...'", + "echo 'Installing Docker Engine...'", + "echo 'Configuring Docker Engine (merging existing configuration with the required)...'", "sleep 4", - "echo '####### Restarting Docker Engine...'", + "echo 'Restarting Docker Engine...'", "ls -l /tmp" - ]; + ]); } else { - $command = [ - "echo '####### Installing Prerequisites...'", - "command -v jq >/dev/null || apt-get update", - "command -v jq >/dev/null || apt install -y jq", - "echo '####### Installing/updating Docker Engine...'", + if ($supported_os_type === 'debian') { + $command = $command->merge([ + "echo 'Installing Prerequisites...'", + "command -v jq >/dev/null || apt-get update", + "command -v jq >/dev/null || apt install -y jq", + + ]); + } else if ($supported_os_type === 'rhel') { + $command = $command->merge([ + "echo 'Installing Prerequisites...'", + "command -v jq >/dev/null || dnf install -y jq", + ]); + } else { + throw new \Exception('Unsupported OS'); + } + $command = $command->merge([ + "echo 'Installing Docker Engine...'", "curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh", - "echo '####### Configuring Docker Engine (merging existing configuration with the required)...'", + "echo 'Configuring Docker Engine (merging existing configuration with the required)...'", "test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json \"/etc/docker/daemon.json.original-`date +\"%Y%m%d-%H%M%S\"`\" || echo '{$config}' | base64 -d > /etc/docker/daemon.json", "echo '{$config}' | base64 -d > /etc/docker/daemon.json.coolify", "cat <<< $(jq . /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json.coolify", "cat <<< $(jq -s '.[0] * .[1]' /etc/docker/daemon.json /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json", - "echo '####### Restarting Docker Engine...'", + "echo 'Restarting Docker Engine...'", "systemctl restart docker", - "echo '####### Creating default Docker network (coolify)...'", + "echo 'Creating default Docker network (coolify)...'", "docker network create --attachable coolify >/dev/null 2>&1 || true", - "echo '####### Done!'" - ]; + "echo 'Done!'" + ]); + return remote_process($command, $server); } - return remote_process($command, $server); } } diff --git a/app/Http/Livewire/Server/Form.php b/app/Http/Livewire/Server/Form.php index 4caa15b3f..ecf7c80cf 100644 --- a/app/Http/Livewire/Server/Form.php +++ b/app/Http/Livewire/Server/Form.php @@ -43,9 +43,9 @@ public function mount() $this->wildcard_domain = $this->server->settings->wildcard_domain; $this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage; } - public function serverRefresh() + public function serverRefresh($install = true) { - $this->validateServer(); + $this->validateServer($install); } public function instantSave() { @@ -53,11 +53,11 @@ public function instantSave() $this->validateServer(); $this->server->settings->save(); } - public function installDocker() + public function installDocker($supported_os_type) { $this->emit('installDocker'); $this->dockerInstallationStarted = true; - $activity = InstallDocker::run($this->server); + $activity = InstallDocker::run($this->server, $supported_os_type); $this->emit('newMonitorActivity', $activity->id); } public function checkLocalhostConnection() @@ -77,24 +77,27 @@ public function validateServer($install = true) { try { $uptime = $this->server->validateConnection(); - if ($uptime) { - $install && $this->emit('success', 'Server is reachable.'); - } else { + if (!$uptime) { $install && $this->emit('error', 'Server is not reachable. Please check your connection and configuration.'); return; } + $supported_os_type = $this->server->validateOS(); + if (!$supported_os_type) { + $install && $this->emit('error', 'Server OS is not supported.
Please use a supported OS.'); + return; + } $dockerInstalled = $this->server->validateDockerEngine(); if ($dockerInstalled) { $install && $this->emit('success', 'Docker Engine is installed.
Checking version.'); } else { - $install && $this->installDocker(); + $install && $this->installDocker($supported_os_type); return; } $dockerVersion = $this->server->validateDockerEngineVersion(); if ($dockerVersion) { $install && $this->emit('success', 'Docker Engine version is 23+.'); } else { - $install && $this->installDocker(); + $install && $this->installDocker($supported_os_type); return; } } catch (\Throwable $e) { diff --git a/app/Http/Livewire/Server/Show.php b/app/Http/Livewire/Server/Show.php index 77ae447d7..3863381b2 100644 --- a/app/Http/Livewire/Server/Show.php +++ b/app/Http/Livewire/Server/Show.php @@ -25,7 +25,7 @@ public function mount() } public function submit() { - $this->emit('serverRefresh'); + $this->emit('serverRefresh',false); } public function render() { diff --git a/app/Models/Server.php b/app/Models/Server.php index 02c3186c6..7b34278e7 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -304,6 +304,27 @@ public function isLogDrainEnabled() { return $this->settings->is_logdrain_newrelic_enabled || $this->settings->is_logdrain_highlight_enabled || $this->settings->is_logdrain_axiom_enabled; } + public function validateOS() + { + $os_release = instant_remote_process(['cat /etc/os-release'], $this); + $datas = collect(explode("\n", $os_release)); + $collectedData = collect([]); + foreach ($datas as $data) { + $item = Str::of($data)->trim(); + $collectedData->put($item->before('=')->value(), $item->after('=')->lower()->replace('"', '')->value()); + } + $ID = data_get($collectedData, 'ID'); + $ID_LIKE = data_get($collectedData, 'ID_LIKE'); + $VERSION_ID = data_get($collectedData, 'VERSION_ID'); + // ray($ID, $ID_LIKE, $VERSION_ID); + if (collect(SUPPORTED_OS)->contains($ID_LIKE)) { + ray('supported'); + return str($ID_LIKE)->explode(' ')->first(); + } else { + ray('not supported'); + return false; + } + } public function validateConnection() { if ($this->skipServer()) { @@ -311,30 +332,27 @@ public function validateConnection() } $uptime = instant_remote_process(['uptime'], $this, false); + ray($uptime); if (!$uptime) { $this->settings()->update([ 'is_reachable' => false, - 'is_usable' => false ]); return false; + } else { + $this->settings()->update([ + 'is_reachable' => true, + ]); + $this->update([ + 'unreachable_count' => 0, + ]); + ray($this); } if (data_get($this, 'unreachable_notification_sent') === true) { $this->team->notify(new Revived($this)); $this->update(['unreachable_notification_sent' => false]); } - if ( - data_get($this, 'settings.is_reachable') === false || - data_get($this, 'settings.is_usable') === false - ) { - $this->settings()->update([ - 'is_reachable' => true, - 'is_usable' => true - ]); - } - $this->update([ - 'unreachable_count' => 0, - ]); + return true; } public function validateDockerEngine($throwError = false) @@ -344,7 +362,7 @@ public function validateDockerEngine($throwError = false) $this->settings->is_usable = false; $this->settings->save(); if ($throwError) { - throw new \Exception('Server is not usable.'); + throw new \Exception('Server is not usable. Docker Engine is not installed.'); } return false; } @@ -362,6 +380,7 @@ public function validateDockerEngineVersion() $this->settings->save(); return false; } + $this->settings->is_reachable = true; $this->settings->is_usable = true; $this->settings->save(); return true; diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index d26932165..299d3acb9 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -27,3 +27,8 @@ const SPECIFIC_SERVICES = [ 'quay.io/minio/minio', ]; + +const SUPPORTED_OS = [ + 'debian', + 'rhel centos fedora' +]; From 4a211029836bb2717d3876b17ed2633049270fa8 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 21 Nov 2023 12:07:06 +0100 Subject: [PATCH 13/16] fix: server adding process --- app/Actions/Database/StartMariadb.php | 4 ++-- app/Actions/Database/StartMongodb.php | 4 ++-- app/Actions/Database/StartMysql.php | 4 ++-- app/Actions/Database/StartPostgresql.php | 4 ++-- app/Actions/Database/StartRedis.php | 4 ++-- app/Actions/Server/InstallDocker.php | 6 +++++- app/Actions/Service/StartService.php | 10 +++++----- app/Http/Livewire/Boarding/Index.php | 19 ++++++++++++------- app/Http/Livewire/Server/Form.php | 10 +++++----- bootstrap/helpers/shared.php | 3 +-- resources/views/layouts/base.blade.php | 5 +++++ .../views/livewire/boarding/index.blade.php | 6 +++--- .../views/livewire/server/form.blade.php | 5 ----- 13 files changed, 46 insertions(+), 38 deletions(-) diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php index c6b243381..21fcdb8a5 100644 --- a/app/Actions/Database/StartMariadb.php +++ b/app/Actions/Database/StartMariadb.php @@ -23,7 +23,7 @@ public function handle(StandaloneMariadb $database) $this->configuration_dir = database_configuration_dir() . '/' . $container_name; $this->commands = [ - "echo '####### Starting {$database->name}.'", + "echo 'Starting {$database->name}.'", "mkdir -p $this->configuration_dir", ]; @@ -104,7 +104,7 @@ public function handle(StandaloneMariadb $database) $this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; - $this->commands[] = "echo '####### {$database->name} started.'"; + $this->commands[] = "echo '{$database->name} started.'"; return remote_process($this->commands, $database->destination->server); } diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index 9eb884dbe..e0197a7bc 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -25,7 +25,7 @@ public function handle(StandaloneMongodb $database) $this->configuration_dir = database_configuration_dir() . '/' . $container_name; $this->commands = [ - "echo '####### Starting {$database->name}.'", + "echo 'Starting {$database->name}.'", "mkdir -p $this->configuration_dir", ]; @@ -120,7 +120,7 @@ public function handle(StandaloneMongodb $database) $this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; - $this->commands[] = "echo '####### {$database->name} started.'"; + $this->commands[] = "echo '{$database->name} started.'"; return remote_process($this->commands, $database->destination->server); } diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php index 761832525..76e8af619 100644 --- a/app/Actions/Database/StartMysql.php +++ b/app/Actions/Database/StartMysql.php @@ -23,7 +23,7 @@ public function handle(StandaloneMysql $database) $this->configuration_dir = database_configuration_dir() . '/' . $container_name; $this->commands = [ - "echo '####### Starting {$database->name}.'", + "echo 'Starting {$database->name}.'", "mkdir -p $this->configuration_dir", ]; @@ -104,7 +104,7 @@ public function handle(StandaloneMysql $database) $this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; - $this->commands[] = "echo '####### {$database->name} started.'"; + $this->commands[] = "echo '{$database->name} started.'"; return remote_process($this->commands, $database->destination->server); } diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index b88da5e4f..97ae9da0e 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -23,7 +23,7 @@ public function handle(StandalonePostgresql $database) $this->configuration_dir = database_configuration_dir() . '/' . $container_name; $this->commands = [ - "echo '####### Starting {$database->name}.'", + "echo 'Starting {$database->name}.'", "mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/" ]; @@ -130,7 +130,7 @@ public function handle(StandalonePostgresql $database) $this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; - $this->commands[] = "echo '####### {$database->name} started.'"; + $this->commands[] = "echo '{$database->name} started.'"; return remote_process($this->commands, $database->destination->server); } diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php index fab055f20..fcb87b891 100644 --- a/app/Actions/Database/StartRedis.php +++ b/app/Actions/Database/StartRedis.php @@ -26,7 +26,7 @@ public function handle(StandaloneRedis $database) $this->configuration_dir = database_configuration_dir() . '/' . $container_name; $this->commands = [ - "echo '####### Starting {$database->name}.'", + "echo 'Starting {$database->name}.'", "mkdir -p $this->configuration_dir", ]; @@ -114,7 +114,7 @@ public function handle(StandaloneRedis $database) $this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; - $this->commands[] = "echo '####### {$database->name} started.'"; + $this->commands[] = "echo '{$database->name} started.'"; return remote_process($this->commands, $database->destination->server); } diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index a39799d88..01a2f8014 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -9,8 +9,12 @@ class InstallDocker { use AsAction; - public function handle(Server $server, $supported_os_type) + public function handle(Server $server) { + $supported_os_type = $server->validateOS(); + if (!$supported_os_type) { + throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation.'); + } ray('Installing Docker on server: ' . $server->name . ' (' . $server->ip . ')' . ' with OS: ' . $supported_os_type); $dockerVersion = '24.0'; $config = base64_encode('{ diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php index 74bdd81cb..ef473e578 100644 --- a/app/Actions/Service/StartService.php +++ b/app/Actions/Service/StartService.php @@ -14,13 +14,13 @@ public function handle(Service $service) $network = $service->destination->network; $service->saveComposeConfigs(); $commands[] = "cd " . $service->workdir(); - $commands[] = "echo '####### Saved configuration files to {$service->workdir()}.'"; - $commands[] = "echo '####### Creating Docker network.'"; + $commands[] = "echo 'Saved configuration files to {$service->workdir()}.'"; + $commands[] = "echo 'Creating Docker network.'"; $commands[] = "docker network create --attachable '{$service->uuid}' >/dev/null || true"; - $commands[] = "echo '####### Starting service {$service->name} on {$service->server->name}.'"; - $commands[] = "echo '####### Pulling images.'"; + $commands[] = "echo 'Starting service {$service->name} on {$service->server->name}.'"; + $commands[] = "echo 'Pulling images.'"; $commands[] = "docker compose pull"; - $commands[] = "echo '####### Starting containers.'"; + $commands[] = "echo 'Starting containers.'"; $commands[] = "docker compose up -d --remove-orphans --force-recreate"; $commands[] = "docker network connect $service->uuid coolify-proxy || true"; $compose = data_get($service,'docker_compose',[]); diff --git a/app/Http/Livewire/Boarding/Index.php b/app/Http/Livewire/Boarding/Index.php index d710b9e6f..7f53708ad 100644 --- a/app/Http/Livewire/Boarding/Index.php +++ b/app/Http/Livewire/Boarding/Index.php @@ -188,7 +188,6 @@ public function saveServer() public function validateServer() { try { - $customErrorMessage = "Server is not reachable:"; config()->set('coolify.mux_enabled', false); instant_remote_process(['uptime'], $this->createdServer, true); @@ -198,7 +197,7 @@ public function validateServer() ]); } catch (\Throwable $e) { $this->serverReachable = false; - return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this); + return handleError(error: $e, livewire: $this); } try { @@ -206,7 +205,7 @@ public function validateServer() $dockerVersion = checkMinimumDockerEngineVersion($dockerVersion); if (is_null($dockerVersion)) { $this->currentState = 'install-docker'; - throw new \Exception('Docker version is not supported or not installed.'); + throw new \Exception('Docker not found or old version is installed.'); } $this->createdServer->settings()->update([ 'is_usable' => true, @@ -214,14 +213,20 @@ public function validateServer() $this->getProxyType(); } catch (\Throwable $e) { // $this->dockerInstallationStarted = false; - return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this); + return handleError(error: $e, livewire: $this); } } public function installDocker() { - $this->dockerInstallationStarted = true; - $activity = InstallDocker::run($this->createdServer); - $this->emit('newMonitorActivity', $activity->id); + try { + $this->dockerInstallationStarted = true; + $activity = InstallDocker::run($this->createdServer); + $this->emit('installDocker'); + $this->emit('newMonitorActivity', $activity->id); + } catch (\Throwable $e) { + $this->dockerInstallationStarted = false; + return handleError(error: $e, livewire: $this); + } } public function dockerInstalledOrSkipped() { diff --git a/app/Http/Livewire/Server/Form.php b/app/Http/Livewire/Server/Form.php index ecf7c80cf..9d39ae4e4 100644 --- a/app/Http/Livewire/Server/Form.php +++ b/app/Http/Livewire/Server/Form.php @@ -53,11 +53,11 @@ public function instantSave() $this->validateServer(); $this->server->settings->save(); } - public function installDocker($supported_os_type) + public function installDocker() { $this->emit('installDocker'); $this->dockerInstallationStarted = true; - $activity = InstallDocker::run($this->server, $supported_os_type); + $activity = InstallDocker::run($this->server); $this->emit('newMonitorActivity', $activity->id); } public function checkLocalhostConnection() @@ -83,21 +83,21 @@ public function validateServer($install = true) } $supported_os_type = $this->server->validateOS(); if (!$supported_os_type) { - $install && $this->emit('error', 'Server OS is not supported.
Please use a supported OS.'); + $install && $this->emit('error', 'Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation.'); return; } $dockerInstalled = $this->server->validateDockerEngine(); if ($dockerInstalled) { $install && $this->emit('success', 'Docker Engine is installed.
Checking version.'); } else { - $install && $this->installDocker($supported_os_type); + $install && $this->installDocker(); return; } $dockerVersion = $this->server->validateDockerEngineVersion(); if ($dockerVersion) { $install && $this->emit('success', 'Docker Engine version is 23+.'); } else { - $install && $this->installDocker($supported_os_type); + $install && $this->installDocker(); return; } } catch (\Throwable $e) { diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 1953f554d..071c252ff 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -106,14 +106,13 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n $message = null; } if ($customErrorMessage) { - $error->message = $customErrorMessage . ' ' . $message; $message = $customErrorMessage . ' ' . $message; } if (isset($livewire)) { return $livewire->emit('error', $message); } - throw $error; + throw new Exception($message); } function get_route_parameters(): array { diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index 5cd5ce1c5..6f63290ca 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -1,5 +1,6 @@ + @@ -25,6 +26,7 @@ @endif @section('body') + @livewireScripts @@ -120,6 +122,9 @@ function copyToClipboard(text) { Livewire.on('success', (message) => { if (message) Toaster.success(message) }) + Livewire.on('installDocker', () => { + installDocker.showModal(); + }) @show diff --git a/resources/views/livewire/boarding/index.blade.php b/resources/views/livewire/boarding/index.blade.php index 4bac29022..75f2441f0 100644 --- a/resources/views/livewire/boarding/index.blade.php +++ b/resources/views/livewire/boarding/index.blade.php @@ -225,8 +225,7 @@ Could not find Docker Engine on your server. Do you want me to install it for you? - + Let's do it! @if ($dockerInstallationStarted) @@ -235,9 +234,10 @@

This will install the latest Docker Engine on your server, configure a few things to be able - to run optimal.

+ to run optimal.

Minimum Docker Engine version is: 22

To manually install Docker Engine, check this documentation.

+ @endif
diff --git a/resources/views/livewire/server/form.blade.php b/resources/views/livewire/server/form.blade.php index e872751f9..4004dd096 100644 --- a/resources/views/livewire/server/form.blade.php +++ b/resources/views/livewire/server/form.blade.php @@ -64,9 +64,4 @@ helper="Disk cleanup job will be executed if disk usage is more than this number." /> @endif -
From f58e6766e1b711425515300d0893e80513090c04 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 21 Nov 2023 13:06:05 +0100 Subject: [PATCH 14/16] Update Docker Engine version check --- app/Actions/Server/InstallDocker.php | 1 + app/Http/Livewire/Server/Form.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index 01a2f8014..0713ed086 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -68,6 +68,7 @@ public function handle(Server $server) "cat <<< $(jq . /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json.coolify", "cat <<< $(jq -s '.[0] * .[1]' /etc/docker/daemon.json /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json", "echo 'Restarting Docker Engine...'", + "systemctl enable docker >/dev/null 2>&1 || true", "systemctl restart docker", "echo 'Creating default Docker network (coolify)...'", "docker network create --attachable coolify >/dev/null 2>&1 || true", diff --git a/app/Http/Livewire/Server/Form.php b/app/Http/Livewire/Server/Form.php index 9d39ae4e4..dacb8faad 100644 --- a/app/Http/Livewire/Server/Form.php +++ b/app/Http/Livewire/Server/Form.php @@ -95,7 +95,7 @@ public function validateServer($install = true) } $dockerVersion = $this->server->validateDockerEngineVersion(); if ($dockerVersion) { - $install && $this->emit('success', 'Docker Engine version is 23+.'); + $install && $this->emit('success', 'Docker Engine version is 22+.'); } else { $install && $this->installDocker(); return; From ef7fc1b260ab3ca25c265bf03853b9142e2a9a77 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 21 Nov 2023 15:31:46 +0100 Subject: [PATCH 15/16] Refactor code and update destination component --- .../Controllers/ApplicationController.php | 17 ----- .../Project/Application/Configuration.php | 39 ++++++++++ .../Livewire/Project/Shared/Destination.php | 4 +- app/Jobs/ApplicationDeploymentJob.php | 71 +++++++++++++------ app/Jobs/ContainerStatusJob.php | 2 +- app/Models/Application.php | 8 ++- app/Models/Server.php | 2 - ...20_add_additional_destinations_to_apps.php | 28 ++++++++ .../application/configuration.blade.php | 6 +- .../project/shared/destination.blade.php | 31 +++++++- .../project/database/configuration.blade.php | 2 +- routes/web.php | 5 +- 12 files changed, 162 insertions(+), 53 deletions(-) create mode 100644 app/Http/Livewire/Project/Application/Configuration.php create mode 100644 database/migrations/2023_11_21_121920_add_additional_destinations_to_apps.php rename resources/views/{ => livewire}/project/application/configuration.blade.php (97%) diff --git a/app/Http/Controllers/ApplicationController.php b/app/Http/Controllers/ApplicationController.php index 6f58c71e6..12411d3fd 100644 --- a/app/Http/Controllers/ApplicationController.php +++ b/app/Http/Controllers/ApplicationController.php @@ -10,23 +10,6 @@ class ApplicationController extends Controller { use AuthorizesRequests, ValidatesRequests; - public function configuration() - { - $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); - if (!$project) { - return redirect()->route('dashboard'); - } - $environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']); - if (!$environment) { - return redirect()->route('dashboard'); - } - $application = $environment->applications->where('uuid', request()->route('application_uuid'))->first(); - if (!$application) { - return redirect()->route('dashboard'); - } - return view('project.application.configuration', ['application' => $application]); - } - public function deployments() { $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); diff --git a/app/Http/Livewire/Project/Application/Configuration.php b/app/Http/Livewire/Project/Application/Configuration.php new file mode 100644 index 000000000..7cb2939f2 --- /dev/null +++ b/app/Http/Livewire/Project/Application/Configuration.php @@ -0,0 +1,39 @@ +load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); + if (!$project) { + return redirect()->route('dashboard'); + } + $environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']); + if (!$environment) { + return redirect()->route('dashboard'); + } + $application = $environment->applications->where('uuid', request()->route('application_uuid'))->first(); + if (!$application) { + return redirect()->route('dashboard'); + } + $this->application = $application; + $mainServer = $application->destination->server; + $servers = Server::ownedByCurrentTeam()->get(); + $this->servers = $servers->filter(function ($server) use ($mainServer) { + return $server->id != $mainServer->id; + }); + } + public function render() + { + return view('livewire.project.application.configuration'); + } +} diff --git a/app/Http/Livewire/Project/Shared/Destination.php b/app/Http/Livewire/Project/Shared/Destination.php index 3bdb48af6..a946c013f 100644 --- a/app/Http/Livewire/Project/Shared/Destination.php +++ b/app/Http/Livewire/Project/Shared/Destination.php @@ -6,5 +6,7 @@ class Destination extends Component { - public $destination; + public $resource; + public $servers = []; + public $additionalServers = []; } diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 99aecba86..77c8bd13f 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -55,6 +55,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted private GithubApp|GitlabApp|string $source = 'other'; private StandaloneDocker|SwarmDocker $destination; private Server $server; + private Server $mainServer; private ?ApplicationPreview $preview = null; private ?string $git_type = null; @@ -111,7 +112,7 @@ public function __construct(int $application_deployment_queue_id) $this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first(); } $this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first(); - $this->server = $this->destination->server; + $this->server = $this->mainServer = $this->destination->server; $this->serverUser = $this->server->user; $this->basedir = "/artifacts/{$this->deployment_uuid}"; $this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/'); @@ -181,10 +182,6 @@ public function handle(): void $this->buildTarget = " --target {$this->application->dockerfile_target_build} "; } - // Get user home directory - $this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server); - $this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server); - // Check custom port preg_match('/(?<=:)\d+(?=\/)/', $this->application->git_repository, $matches); if (count($matches) === 1) { @@ -400,19 +397,19 @@ private function check_image_locally_or_remotely() ]); } } - private function save_environment_variables() - { - $envs = collect([]); - foreach ($this->application->environment_variables as $env) { - $envs->push($env->key . '=' . $env->value); - } - $envs_base64 = base64_encode($envs->implode("\n")); - $this->execute_remote_command( - [ - executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env") - ], - ); - } + // private function save_environment_variables() + // { + // $envs = collect([]); + // foreach ($this->application->environment_variables as $env) { + // $envs->push($env->key . '=' . $env->value); + // } + // $envs_base64 = base64_encode($envs->implode("\n")); + // $this->execute_remote_command( + // [ + // executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env") + // ], + // ); + // } private function deploy_simple_dockerfile() { $dockerfile_base64 = base64_encode($this->application->dockerfile); @@ -471,7 +468,12 @@ private function deploy_dockerfile_buildpack() $this->generate_build_env_variables(); $this->add_build_env_variables_to_dockerfile(); $this->build_image(); - $this->rolling_update(); + // if ($this->application->additional_destinations) { + // $this->push_to_docker_registry(); + // $this->deploy_to_additional_destinations(); + // } else { + $this->rolling_update(); + // } } private function deploy_nixpacks_buildpack() { @@ -629,12 +631,15 @@ private function deploy_pull_request() private function prepare_builder_image() { $helperImage = config('coolify.helper_image'); + // Get user home directory + $this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server); + $this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server); + if ($this->dockerConfigFileExists === 'OK') { $runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; } else { $runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; } - $this->execute_remote_command( [ "echo -n 'Preparing container with helper image: $helperImage.'", @@ -648,7 +653,31 @@ private function prepare_builder_image() ], ); } - + private function deploy_to_additional_destinations() + { + $destination_ids = collect(str($this->application->additional_destinations)->explode(',')); + foreach ($destination_ids as $destination_id) { + $destination = StandaloneDocker::find($destination_id); + $server = $destination->server; + if ($server->team_id !== $this->mainServer->team_id) { + $this->execute_remote_command( + [ + "echo -n 'Skipping deployment to {$server->name}. Not in the same team?!'", + ], + ); + continue; + } + $this->server = $server; + $this->execute_remote_command( + [ + "echo -n 'Deploying to {$this->server->name}.'", + ], + ); + $this->prepare_builder_image(); + $this->generate_image_names(); + $this->rolling_update(); + } + } private function set_base_dir() { $this->execute_remote_command( diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index 34678b18a..a45bebf8e 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -37,7 +37,7 @@ public function uniqueId(): int public function handle(): void { - ray("checking container statuses for {$this->server->id}"); + // ray("checking container statuses for {$this->server->id}"); try { if (!$this->server->isServerReady()) { return; diff --git a/app/Models/Application.php b/app/Models/Application.php index cb3d85f94..f049351e2 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -225,7 +225,8 @@ public function source() return $this->morphTo(); } - public function isDeploymentInprogress() { + public function isDeploymentInprogress() + { $deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'in_progress')->count(); if ($deployments > 0) { return true; @@ -300,8 +301,9 @@ public function isHealthcheckDisabled(): bool } return false; } - public function isLogDrainEnabled() { - return data_get($this, 'settings.is_log_drain_enabled', false); + public function isLogDrainEnabled() + { + return data_get($this, 'settings.is_log_drain_enabled', false); } public function isConfigurationChanged($save = false) { diff --git a/app/Models/Server.php b/app/Models/Server.php index 7b34278e7..0d57cea55 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -332,7 +332,6 @@ public function validateConnection() } $uptime = instant_remote_process(['uptime'], $this, false); - ray($uptime); if (!$uptime) { $this->settings()->update([ 'is_reachable' => false, @@ -345,7 +344,6 @@ public function validateConnection() $this->update([ 'unreachable_count' => 0, ]); - ray($this); } if (data_get($this, 'unreachable_notification_sent') === true) { diff --git a/database/migrations/2023_11_21_121920_add_additional_destinations_to_apps.php b/database/migrations/2023_11_21_121920_add_additional_destinations_to_apps.php new file mode 100644 index 000000000..4852007b4 --- /dev/null +++ b/database/migrations/2023_11_21_121920_add_additional_destinations_to_apps.php @@ -0,0 +1,28 @@ +string('additional_destinations')->nullable()->after('destination'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('additional_destinations'); + }); + } +}; diff --git a/resources/views/project/application/configuration.blade.php b/resources/views/livewire/project/application/configuration.blade.php similarity index 97% rename from resources/views/project/application/configuration.blade.php rename to resources/views/livewire/project/application/configuration.blade.php index d6eb25c10..7f2efbc5a 100644 --- a/resources/views/project/application/configuration.blade.php +++ b/resources/views/livewire/project/application/configuration.blade.php @@ -1,4 +1,4 @@ - +

Configuration

@@ -66,7 +66,7 @@
@endif
- +
@@ -91,4 +91,4 @@
- + diff --git a/resources/views/livewire/project/shared/destination.blade.php b/resources/views/livewire/project/shared/destination.blade.php index 71a667b7f..d1c8472cd 100644 --- a/resources/views/livewire/project/shared/destination.blade.php +++ b/resources/views/livewire/project/shared/destination.blade.php @@ -3,7 +3,34 @@
The destination server where your application will be deployed to.
On server {{ data_get($destination, 'server.name') }} - in {{ data_get($destination, 'network') }} network. + href="{{ route('server.show', ['server_uuid' => data_get($resource, 'destination.server.uuid')]) }}">On + server {{ data_get($resource, 'destination.server.name') }} + in {{ data_get($resource, 'destination.network') }} network.
+ {{-- {{$resource->additional_destinations}} --}} + {{-- @if (count($servers) > 0) +
+

Additional Servers

+ @foreach ($servers as $server) +
+

{{ $server->name }}

+
{{ $server->description }}
+ + + + @foreach ($server->destinations() as $destination) + @if ($loop->first) + + + @else + + + @endif + @endforeach + + Save +
+ @endforeach +
+ @endif --}} diff --git a/resources/views/project/database/configuration.blade.php b/resources/views/project/database/configuration.blade.php index c65ef4301..49fcf5d1c 100644 --- a/resources/views/project/database/configuration.blade.php +++ b/resources/views/project/database/configuration.blade.php @@ -63,7 +63,7 @@
- +
diff --git a/routes/web.php b/routes/web.php index 6ac0d578c..18bad3bf5 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,6 +5,7 @@ use App\Http\Controllers\DatabaseController; use App\Http\Controllers\MagicController; use App\Http\Controllers\ProjectController; +use App\Http\Livewire\Project\Application\Configuration as ApplicationConfiguration; use App\Http\Livewire\Boarding\Index as BoardingIndex; use App\Http\Livewire\Project\Service\Index as ServiceIndex; use App\Http\Livewire\Project\Service\Show as ServiceShow; @@ -101,7 +102,8 @@ Route::get('/project/{project_uuid}/{environment_name}', [ProjectController::class, 'resources'])->name('project.resources'); // Applications - Route::get('/project/{project_uuid}/{environment_name}/application/{application_uuid}', [ApplicationController::class, 'configuration'])->name('project.application.configuration'); + Route::get('/project/{project_uuid}/{environment_name}/application/{application_uuid}', ApplicationConfiguration::class)->name('project.application.configuration'); + Route::get('/project/{project_uuid}/{environment_name}/application/{application_uuid}/deployment', [ApplicationController::class, 'deployments'])->name('project.application.deployments'); Route::get( '/project/{project_uuid}/{environment_name}/application/{application_uuid}/deployment/{deployment_uuid}', @@ -167,7 +169,6 @@ 'private_key' => PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail() ]))->name('security.private-key.show'); Route::get('/security/api-tokens', ApiTokens::class)->name('security.api-tokens'); - }); From ce26127705806bad8e653348dd8b43a791cfcdea Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 21 Nov 2023 22:17:35 +0100 Subject: [PATCH 16/16] wip: new deployment jobs --- app/Jobs/ApplicationDeployDockerImageJob.php | 111 ++ .../ApplicationDeploySimpleDockerfileJob.php | 29 + app/Jobs/ApplicationRestartJob.php | 28 + app/Jobs/MultipleApplicationDeploymentJob.php | 1165 +++++++++++++++++ app/Models/Application.php | 10 + app/Models/Server.php | 2 +- app/Traits/ExecuteRemoteCommand.php | 1 - app/Traits/ExecuteRemoteCommandNew.php | 77 ++ bootstrap/helpers/applications.php | 295 +++++ 9 files changed, 1716 insertions(+), 2 deletions(-) create mode 100644 app/Jobs/ApplicationDeployDockerImageJob.php create mode 100644 app/Jobs/ApplicationDeploySimpleDockerfileJob.php create mode 100644 app/Jobs/ApplicationRestartJob.php create mode 100644 app/Jobs/MultipleApplicationDeploymentJob.php create mode 100644 app/Traits/ExecuteRemoteCommandNew.php diff --git a/app/Jobs/ApplicationDeployDockerImageJob.php b/app/Jobs/ApplicationDeployDockerImageJob.php new file mode 100644 index 000000000..dda5629c8 --- /dev/null +++ b/app/Jobs/ApplicationDeployDockerImageJob.php @@ -0,0 +1,111 @@ +applicationDeploymentQueueId = $applicationDeploymentQueueId; + } + public function handle() + { + ray()->clearAll(); + ray('Deploying Docker Image'); + try { + $applicationDeploymentQueue = ApplicationDeploymentQueue::find($this->applicationDeploymentQueueId); + $application = Application::find($applicationDeploymentQueue->application_id); + + $deploymentUuid = data_get($applicationDeploymentQueue, 'deployment_uuid'); + $dockerImage = data_get($application, 'docker_registry_image_name'); + $dockerImageTag = data_get($application, 'docker_registry_image_tag'); + $productionImageName = str("{$dockerImage}:{$dockerImageTag}"); + $destination = $application->destination->getMorphClass()::where('id', $application->destination->id)->first(); + $pullRequestId = data_get($applicationDeploymentQueue, 'pull_request_id'); + + $server = data_get($destination, 'server'); + $network = data_get($destination, 'network'); + + $containerName = generateApplicationContainerName($application, $pullRequestId); + savePrivateKeyToFs($server); + + ray("echo 'Starting deployment of {$productionImageName}.'"); + + $applicationDeploymentQueue->update([ + 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, + ]); + $this->executeRemoteCommand( + server: $server, + logModel: $applicationDeploymentQueue, + commands: prepareHelperContainer($server, $network, $deploymentUuid) + ); + + $this->executeRemoteCommand( + server: $server, + logModel: $applicationDeploymentQueue, + commands: generateComposeFile( + deploymentUuid: $deploymentUuid, + server: $server, + network: $network, + application: $application, + containerName: $containerName, + imageName: $productionImageName, + pullRequestId: $pullRequestId + ) + ); + $this->executeRemoteCommand( + server: $server, + logModel: $applicationDeploymentQueue, + commands: rollingUpdate(application: $application, deploymentUuid: $deploymentUuid) + ); + } catch (Throwable $e) { + $this->executeRemoteCommand( + server: $server, + logModel: $applicationDeploymentQueue, + commands: [ + "echo 'Oops something is not okay, are you okay? 😢'", + "echo '{$e->getMessage()}'", + "echo -n 'Deployment failed. Removing the new version of your application.'", + executeInDocker($deploymentUuid, "docker rm -f $containerName >/dev/null 2>&1"), + ] + ); + // $this->next(ApplicationDeploymentStatus::FAILED->value); + throw $e; + } + } + // private function next(string $status) + // { + // // If the deployment is cancelled by the user, don't update the status + // if ($this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value) { + // $this->application_deployment_queue->update([ + // 'status' => $status, + // ]); + // } + // queue_next_deployment($this->application); + // if ($status === ApplicationDeploymentStatus::FINISHED->value) { + // $this->application->environment->project->team->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview)); + // } + // if ($status === ApplicationDeploymentStatus::FAILED->value) { + // $this->application->environment->project->team->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview)); + // } + // } +} diff --git a/app/Jobs/ApplicationDeploySimpleDockerfileJob.php b/app/Jobs/ApplicationDeploySimpleDockerfileJob.php new file mode 100644 index 000000000..a9e17bc80 --- /dev/null +++ b/app/Jobs/ApplicationDeploySimpleDockerfileJob.php @@ -0,0 +1,29 @@ +applicationDeploymentQueueId = $applicationDeploymentQueueId; + } + public function handle() { + ray('Deploying Simple Dockerfile'); + } +} diff --git a/app/Jobs/ApplicationRestartJob.php b/app/Jobs/ApplicationRestartJob.php new file mode 100644 index 000000000..3216baa5a --- /dev/null +++ b/app/Jobs/ApplicationRestartJob.php @@ -0,0 +1,28 @@ +applicationDeploymentQueueId = $applicationDeploymentQueueId; + } + public function handle() { + ray('Restarting application'); + } +} diff --git a/app/Jobs/MultipleApplicationDeploymentJob.php b/app/Jobs/MultipleApplicationDeploymentJob.php new file mode 100644 index 000000000..32c98d3b0 --- /dev/null +++ b/app/Jobs/MultipleApplicationDeploymentJob.php @@ -0,0 +1,1165 @@ +clearScreen(); + $this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id); + $this->log_model = $this->application_deployment_queue; + $this->application = Application::find($this->application_deployment_queue->application_id); + $this->build_pack = data_get($this->application, 'build_pack'); + + $this->application_deployment_queue_id = $application_deployment_queue_id; + $this->deployment_uuid = $this->application_deployment_queue->deployment_uuid; + $this->commit = $this->application_deployment_queue->commit; + $this->force_rebuild = $this->application_deployment_queue->force_rebuild; + $this->restart_only = $this->application_deployment_queue->restart_only; + + $this->git_type = data_get($this->application_deployment_queue, 'git_type'); + + $source = data_get($this->application, 'source'); + if ($source) { + $this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first(); + } + $this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first(); + $this->server = $this->mainServer = $this->destination->server; + $this->serverUser = $this->server->user; + $this->basedir = generateBaseDir($this->deployment_uuid); + $this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/'); + $this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}"; + $this->is_debug_enabled = $this->application->settings->is_debug_enabled; + $this->saved_outputs = collect(); + $this->container_name = generateApplicationContainerName($this->application, 0); + } + + public function handle(): void + { + savePrivateKeyToFs($this->server); + $this->application_deployment_queue->update([ + 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, + ]); + + $this->addHosts = generateHostIpMapping($this->server, $this->destination->network); + + if ($this->application->dockerfile_target_build) { + $this->buildTarget = " --target {$this->application->dockerfile_target_build} "; + } + + // Check custom port + preg_match('/(?<=:)\d+(?=\/)/', $this->application->git_repository, $matches); + if (count($matches) === 1) { + $this->customPort = $matches[0]; + $gitHost = str($this->application->git_repository)->before(':'); + $gitRepo = str($this->application->git_repository)->after('/'); + $this->customRepository = "$gitHost:$gitRepo"; + } else { + $this->customRepository = $this->application->git_repository; + } + try { + if ($this->application->isMultipleServerDeployment()) { + if ($this->application->build_pack === 'dockerimage') { + $this->dockerImage = $this->application->docker_registry_image_name; + $this->dockerImageTag = $this->application->docker_registry_image_tag; + $this->execute_remote_command( + [ + "echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'" + ], + ); + $this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}"); + ray(prepareHelperContainer($this->server, $this->deployment_uuid)); + $this->execute_remote_command( + [prepareHelperContainer($this->server, $this->deployment_uuid)] + ); + } + } else { + throw new RuntimeException('Missing configuration for multiple server deployment.'); + } + // if ($this->restart_only && $this->application->build_pack !== 'dockerimage') { + // $this->just_restart(); + // if ($this->server->isProxyShouldRun()) { + // dispatch(new ContainerStatusJob($this->server)); + // } + // $this->next(ApplicationDeploymentStatus::FINISHED->value); + // $this->application->isConfigurationChanged(true); + // return; + // } else if ($this->application->dockerfile) { + // $this->deploy_simple_dockerfile(); + // } else if ($this->application->build_pack === 'dockerimage') { + // $this->deploy_dockerimage_buildpack(); + // } else if ($this->application->build_pack === 'dockerfile') { + // $this->deploy_dockerfile_buildpack(); + // } else if ($this->application->build_pack === 'static') { + // $this->deploy_static_buildpack(); + // } else { + // $this->deploy_nixpacks_buildpack(); + // } + // if ($this->server->isProxyShouldRun()) { + // dispatch(new ContainerStatusJob($this->server)); + // } + // if ($this->application->docker_registry_image_name) { + // $this->push_to_docker_registry(); + // } + // $this->next(ApplicationDeploymentStatus::FINISHED->value); + // $this->application->isConfigurationChanged(true); + } catch (Exception $e) { + $this->fail($e); + throw $e; + } finally { + // if (isset($this->docker_compose_base64)) { + // $readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at); + // $composeFileName = "$this->configuration_dir/docker-compose.yml"; + // $this->execute_remote_command( + // [ + // "mkdir -p $this->configuration_dir" + // ], + // [ + // "echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName", + // ], + // [ + // "echo '{$readme}' > $this->configuration_dir/README.md", + // ] + // ); + // } + // $this->execute_remote_command( + // [ + // "docker rm -f {$this->deployment_uuid} >/dev/null 2>&1", + // "hidden" => true, + // "ignore_errors" => true, + // ] + // ); + // $this->execute_remote_command( + // [ + // "docker image prune -f >/dev/null 2>&1", + // "hidden" => true, + // "ignore_errors" => true, + // ] + // ); + } + } + private function push_to_docker_registry() + { + try { + instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server); + $this->execute_remote_command( + [ + "echo '\n----------------------------------------'", + ], + ["echo -n 'Pushing image to docker registry ({$this->production_image_name}).'"], + [ + executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true + ], + ); + if ($this->application->docker_registry_image_tag) { + // Tag image with latest + $this->execute_remote_command( + ['echo -n "Tagging and pushing image with latest tag."'], + [ + executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true + ], + [ + executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true + ], + ); + } + $this->execute_remote_command([ + "echo -n 'Image pushed to docker registry.'" + ]); + } catch (Exception $e) { + $this->execute_remote_command( + ["echo -n 'Failed to push image to docker registry. Please check debug logs for more information.'"], + ); + ray($e); + } + } + // private function deploy_docker_compose() + // { + // $dockercompose_base64 = base64_encode($this->application->dockercompose); + // $this->execute_remote_command( + // [ + // "echo 'Starting deployment of {$this->application->name}.'" + // ], + // ); + // $this->prepare_builder_image(); + // $this->execute_remote_command( + // [ + // executeInDocker($this->deployment_uuid, "echo '$dockercompose_base64' | base64 -d > $this->workdir/docker-compose.yaml") + // ], + // ); + // $this->build_image_name = Str::lower("{$this->customRepository}:build"); + // $this->production_image_name = Str::lower("{$this->application->uuid}:latest"); + // $this->save_environment_variables(); + // $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id); + // ray($containers); + // if ($containers->count() > 0) { + // foreach ($containers as $container) { + // $containerName = data_get($container, 'Names'); + // if ($containerName) { + // instant_remote_process( + // ["docker rm -f {$containerName}"], + // $this->application->destination->server + // ); + // } + // } + // } + + // $this->execute_remote_command( + // ["echo -n 'Starting services (could take a while)...'"], + // [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true], + // ); + // } + private function generate_image_names() + { + if ($this->application->dockerfile) { + if ($this->application->docker_registry_image_name) { + $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:build"); + $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:latest"); + } else { + $this->build_image_name = Str::lower("{$this->application->uuid}:build"); + $this->production_image_name = Str::lower("{$this->application->uuid}:latest"); + } + } else if ($this->application->build_pack === 'dockerimage') { + $this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}"); + } else { + $this->dockerImageTag = str($this->commit)->substr(0, 128); + if ($this->application->docker_registry_image_name) { + $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build"); + $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}"); + } else { + $this->build_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}-build"); + $this->production_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}"); + } + } + } + private function just_restart() + { + $this->execute_remote_command( + [ + "echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'" + ], + ); + $this->prepare_builder_image(); + $this->check_git_if_build_needed(); + $this->set_base_dir(); + $this->generate_image_names(); + $this->check_image_locally_or_remotely(); + if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) { + $this->generate_compose_file(); + $this->rolling_update(); + return; + } + throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.'); + } + private function check_image_locally_or_remotely() + { + $this->execute_remote_command([ + "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" + ]); + if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) { + $this->execute_remote_command([ + "docker pull {$this->production_image_name} 2>/dev/null", "ignore_errors" => true, "hidden" => true + ]); + $this->execute_remote_command([ + "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" + ]); + } + } + // private function save_environment_variables() + // { + // $envs = collect([]); + // foreach ($this->application->environment_variables as $env) { + // $envs->push($env->key . '=' . $env->value); + // } + // $envs_base64 = base64_encode($envs->implode("\n")); + // $this->execute_remote_command( + // [ + // executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env") + // ], + // ); + // } + private function deploy_simple_dockerfile() + { + $dockerfile_base64 = base64_encode($this->application->dockerfile); + $this->execute_remote_command( + [ + "echo 'Starting deployment of {$this->application->name}.'" + ], + ); + $this->prepare_builder_image(); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > $this->workdir$this->dockerfile_location") + ], + ); + $this->generate_image_names(); + $this->generate_compose_file(); + $this->generate_build_env_variables(); + $this->add_build_env_variables_to_dockerfile(); + $this->build_image(); + $this->rolling_update(); + } + + private function deploy_dockerimage_buildpack() + { + // $this->dockerImage = $this->application->docker_registry_image_name; + // $this->dockerImageTag = $this->application->docker_registry_image_tag; + // ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'"); + // $this->execute_remote_command( + // [ + // "echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'" + // ], + // ); + // $this->generate_image_names(); + // $this->prepare_builder_image(); + $this->generate_compose_file(); + $this->rolling_update(); + } + + private function deploy_dockerfile_buildpack() + { + if (data_get($this->application, 'dockerfile_location')) { + $this->dockerfile_location = $this->application->dockerfile_location; + } + $this->execute_remote_command( + [ + "echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'" + ], + ); + $this->prepare_builder_image(); + $this->check_git_if_build_needed(); + $this->clone_repository(); + $this->set_base_dir(); + $this->generate_image_names(); + $this->cleanup_git(); + $this->generate_compose_file(); + $this->generate_build_env_variables(); + $this->add_build_env_variables_to_dockerfile(); + $this->build_image(); + // if ($this->application->additional_destinations) { + // $this->push_to_docker_registry(); + // $this->deploy_to_additional_destinations(); + // } else { + $this->rolling_update(); + // } + } + private function deploy_nixpacks_buildpack() + { + $this->execute_remote_command( + [ + "echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'" + ], + ); + $this->prepare_builder_image(); + $this->check_git_if_build_needed(); + $this->set_base_dir(); + $this->generate_image_names(); + if (!$this->force_rebuild) { + $this->check_image_locally_or_remotely(); + if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) { + $this->execute_remote_command([ + "echo 'No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.'", + ]); + $this->generate_compose_file(); + $this->rolling_update(); + return; + } + if ($this->application->isConfigurationChanged()) { + $this->execute_remote_command([ + "echo 'Configuration changed. Rebuilding image.'", + ]); + } + } + $this->clone_repository(); + $this->cleanup_git(); + $this->generate_nixpacks_confs(); + $this->generate_compose_file(); + $this->generate_build_env_variables(); + $this->add_build_env_variables_to_dockerfile(); + $this->build_image(); + $this->rolling_update(); + } + private function deploy_static_buildpack() + { + $this->execute_remote_command( + [ + "echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'" + ], + ); + $this->prepare_builder_image(); + $this->check_git_if_build_needed(); + $this->set_base_dir(); + $this->generate_image_names(); + $this->clone_repository(); + $this->cleanup_git(); + $this->build_image(); + $this->generate_compose_file(); + $this->rolling_update(); + } + + private function rolling_update() + { + if (count($this->application->ports_mappings_array) > 0) { + $this->execute_remote_command( + [ + "echo '\n----------------------------------------'", + ], + ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"], + ); + $this->stop_running_container(force: true); + $this->start_by_compose_file(); + } else { + $this->execute_remote_command( + [ + "echo '\n----------------------------------------'", + ], + ["echo -n 'Rolling update started.'"], + ); + $this->start_by_compose_file(); + $this->health_check(); + $this->stop_running_container(); + } + } + private function health_check() + { + if ($this->application->isHealthcheckDisabled()) { + $this->newVersionIsHealthy = true; + return; + } + // ray('New container name: ', $this->container_name); + if ($this->container_name) { + $counter = 1; + $this->execute_remote_command( + [ + "echo 'Waiting for healthcheck to pass on the new container.'" + ] + ); + if ($this->full_healthcheck_url) { + $this->execute_remote_command( + [ + "echo 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'" + ] + ); + } + while ($counter < $this->application->health_check_retries) { + $this->execute_remote_command( + [ + "docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}", + "hidden" => true, + "save" => "health_check" + ], + + ); + $this->execute_remote_command( + [ + "echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'" + ], + ); + if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) { + $this->newVersionIsHealthy = true; + $this->application->update(['status' => 'running']); + $this->execute_remote_command( + [ + "echo 'New container is healthy.'" + ], + ); + break; + } + $counter++; + sleep($this->application->health_check_interval); + } + } + } + + private function prepare_builder_image() + { + $helperImage = config('coolify.helper_image'); + // Get user home directory + $this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server); + $this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server); + + if ($this->dockerConfigFileExists === 'OK') { + $runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; + } else { + $runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; + } + $this->execute_remote_command( + [ + "echo -n 'Preparing container with helper image: $helperImage.'", + ], + [ + $runCommand, + "hidden" => true, + ], + [ + "command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->basedir}") + ], + ); + } + private function deploy_to_additional_destinations() + { + $destination_ids = collect(str($this->application->additional_destinations)->explode(',')); + foreach ($destination_ids as $destination_id) { + $destination = StandaloneDocker::find($destination_id); + $server = $destination->server; + if ($server->team_id !== $this->mainServer->team_id) { + $this->execute_remote_command( + [ + "echo -n 'Skipping deployment to {$server->name}. Not in the same team?!'", + ], + ); + continue; + } + $this->server = $server; + $this->execute_remote_command( + [ + "echo -n 'Deploying to {$this->server->name}.'", + ], + ); + $this->prepare_builder_image(); + $this->generate_image_names(); + $this->rolling_update(); + } + } + private function set_base_dir() + { + $this->execute_remote_command( + [ + "echo -n 'Setting base directory to {$this->workdir}.'" + ], + ); + } + private function check_git_if_build_needed() + { + $this->generate_git_import_commands(); + $private_key = data_get($this->application, 'private_key.private_key'); + if ($private_key) { + $private_key = base64_encode($private_key); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh") + ], + [ + executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa") + ], + [ + executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa") + ], + [ + executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$this->branch}"), + "hidden" => true, + "save" => "git_commit_sha" + ], + ); + } else { + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$this->branch}"), + "hidden" => true, + "save" => "git_commit_sha" + ], + ); + } + + if ($this->saved_outputs->get('git_commit_sha')) { + $this->commit = $this->saved_outputs->get('git_commit_sha')->before("\t"); + } + } + private function clone_repository() + { + $importCommands = $this->generate_git_import_commands(); + $this->execute_remote_command( + [ + "echo '\n----------------------------------------'", + ], + [ + "echo -n 'Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '" + ], + [ + $importCommands, "hidden" => true + ] + ); + } + + private function generate_git_import_commands() + { + $this->branch = $this->application->git_branch; + $commands = collect([]); + $git_clone_command = "git clone -q -b {$this->application->git_branch}"; + + if ($this->application->deploymentType() === 'source') { + $source_html_url = data_get($this->application, 'source.html_url'); + $url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL)); + $source_html_url_host = $url['host']; + $source_html_url_scheme = $url['scheme']; + + if ($this->source->getMorphClass() == 'App\Models\GithubApp') { + if ($this->source->is_public) { + $this->fullRepoUrl = "{$this->source->html_url}/{$this->customRepository}"; + $git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->customRepository} {$this->basedir}"; + $git_clone_command = $this->set_git_import_settings($git_clone_command); + + $commands->push(executeInDocker($this->deployment_uuid, $git_clone_command)); + } else { + $github_access_token = generate_github_installation_token($this->source); + $commands->push(executeInDocker($this->deployment_uuid, "git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->customRepository}.git {$this->basedir}")); + $this->fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->customRepository}.git"; + } + return $commands->implode(' && '); + } + } + if ($this->application->deploymentType() === 'deploy_key') { + $this->fullRepoUrl = $this->customRepository; + $private_key = data_get($this->application, 'private_key.private_key'); + if (is_null($private_key)) { + throw new RuntimeException('Private key not found. Please add a private key to the application and try again.'); + } + $private_key = base64_encode($private_key); + $git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}"; + $git_clone_command = $this->set_git_import_settings($git_clone_command_base); + $commands = collect([ + executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"), + executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"), + executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa"), + ]); + + $commands->push(executeInDocker($this->deployment_uuid, $git_clone_command)); + return $commands->implode(' && '); + } + if ($this->application->deploymentType() === 'other') { + $this->fullRepoUrl = $this->customRepository; + $git_clone_command = "{$git_clone_command} {$this->customRepository} {$this->basedir}"; + $git_clone_command = $this->set_git_import_settings($git_clone_command); + $commands->push(executeInDocker($this->deployment_uuid, $git_clone_command)); + return $commands->implode(' && '); + } + } + + private function set_git_import_settings($git_clone_command) + { + if ($this->application->git_commit_sha !== 'HEAD') { + $git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git -c advice.detachedHead=false checkout {$this->application->git_commit_sha} >/dev/null 2>&1"; + } + if ($this->application->settings->is_git_submodules_enabled) { + $git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git submodule update --init --recursive"; + } + if ($this->application->settings->is_git_lfs_enabled) { + $git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git lfs pull"; + } + return $git_clone_command; + } + + private function cleanup_git() + { + $this->execute_remote_command( + [executeInDocker($this->deployment_uuid, "rm -fr {$this->basedir}/.git")], + ); + } + + private function generate_nixpacks_confs() + { + $nixpacks_command = $this->nixpacks_build_cmd(); + $this->execute_remote_command( + [ + "echo -n 'Generating nixpacks configuration with: $nixpacks_command'", + ], + [executeInDocker($this->deployment_uuid, $nixpacks_command)], + [executeInDocker($this->deployment_uuid, "cp {$this->workdir}/.nixpacks/Dockerfile {$this->workdir}/Dockerfile")], + [executeInDocker($this->deployment_uuid, "rm -f {$this->workdir}/.nixpacks/Dockerfile")] + ); + } + + private function nixpacks_build_cmd() + { + $this->generate_env_variables(); + $nixpacks_command = "nixpacks build --cache-key '{$this->application->uuid}' -o {$this->workdir} {$this->env_args} --no-error-without-start"; + if ($this->application->build_command) { + $nixpacks_command .= " --build-cmd \"{$this->application->build_command}\""; + } + if ($this->application->start_command) { + $nixpacks_command .= " --start-cmd \"{$this->application->start_command}\""; + } + if ($this->application->install_command) { + $nixpacks_command .= " --install-cmd \"{$this->application->install_command}\""; + } + $nixpacks_command .= " {$this->workdir}"; + return $nixpacks_command; + } + + private function generate_env_variables() + { + $this->env_args = collect([]); + foreach ($this->application->nixpacks_environment_variables_preview as $env) { + $this->env_args->push("--env {$env->key}={$env->value}"); + } + $this->env_args = $this->env_args->implode(' '); + } + + private function generate_compose_file() + { + $ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array; + + $persistent_storages = $this->generate_local_persistent_volumes(); + $volume_names = $this->generate_local_persistent_volumes_only_volume_names(); + $environment_variables = $this->generate_environment_variables($ports); + + if (data_get($this->application, 'custom_labels')) { + $labels = collect(str($this->application->custom_labels)->explode(',')); + $labels = $labels->filter(function ($value, $key) { + return !Str::startsWith($value, 'coolify.'); + }); + $this->application->custom_labels = $labels->implode(','); + $this->application->save(); + } else { + $labels = collect(generateLabelsApplication($this->application, $this->preview)); + } + + $labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, 0))->toArray(); + $docker_compose = [ + 'version' => '3.8', + 'services' => [ + $this->container_name => [ + 'image' => $this->production_image_name, + 'container_name' => $this->container_name, + 'restart' => RESTART_MODE, + 'environment' => $environment_variables, + 'labels' => $labels, + 'expose' => $ports, + 'networks' => [ + $this->destination->network, + ], + 'healthcheck' => [ + 'test' => [ + 'CMD-SHELL', + $this->generate_healthcheck_commands() + ], + 'interval' => $this->application->health_check_interval . 's', + 'timeout' => $this->application->health_check_timeout . 's', + 'retries' => $this->application->health_check_retries, + 'start_period' => $this->application->health_check_start_period . 's' + ], + 'mem_limit' => $this->application->limits_memory, + 'memswap_limit' => $this->application->limits_memory_swap, + 'mem_swappiness' => $this->application->limits_memory_swappiness, + 'mem_reservation' => $this->application->limits_memory_reservation, + 'cpus' => (int) $this->application->limits_cpus, + 'cpuset' => $this->application->limits_cpuset, + 'cpu_shares' => $this->application->limits_cpu_shares, + ] + ], + 'networks' => [ + $this->destination->network => [ + 'external' => true, + 'name' => $this->destination->network, + 'attachable' => true + ] + ] + ]; + if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) { + $docker_compose['services'][$this->container_name]['logging'] = [ + 'driver' => 'fluentd', + 'options' => [ + 'fluentd-address' => "tcp://127.0.0.1:24224", + 'fluentd-async' => "true", + 'fluentd-sub-second-precision' => "true", + ] + ]; + } + if ($this->application->settings->is_gpu_enabled) { + ray('asd'); + $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [ + [ + 'driver' => data_get($this->application, 'settings.gpu_driver', 'nvidia'), + 'capabilities' => ['gpu'], + 'options' => data_get($this->application, 'settings.gpu_options', []) + ] + ]; + if (data_get($this->application, 'settings.gpu_count')) { + $count = data_get($this->application, 'settings.gpu_count'); + if ($count === 'all') { + $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = $count; + } else { + $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count; + } + } else if (data_get($this->application, 'settings.gpu_device_ids')) { + $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($this->application, 'settings.gpu_device_ids'); + } + } + if ($this->application->isHealthcheckDisabled()) { + data_forget($docker_compose, 'services.' . $this->container_name . '.healthcheck'); + } + if (count($persistent_storages) > 0) { + $docker_compose['services'][$this->container_name]['volumes'] = $persistent_storages; + } + if (count($volume_names) > 0) { + $docker_compose['volumes'] = $volume_names; + } + // if ($this->build_pack === 'dockerfile') { + // $docker_compose['services'][$this->container_name]['build'] = [ + // 'context' => $this->workdir, + // 'dockerfile' => $this->workdir . $this->dockerfile_location, + // ]; + // } + $this->docker_compose = Yaml::dump($docker_compose, 10); + $this->docker_compose_base64 = base64_encode($this->docker_compose); + $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]); + } + + private function generate_local_persistent_volumes() + { + $local_persistent_volumes = []; + foreach ($this->application->persistentStorages as $persistentStorage) { + $volume_name = $persistentStorage->host_path ?? $persistentStorage->name; + $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + } + return $local_persistent_volumes; + } + + private function generate_local_persistent_volumes_only_volume_names() + { + $local_persistent_volumes_names = []; + foreach ($this->application->persistentStorages as $persistentStorage) { + if ($persistentStorage->host_path) { + continue; + } + $name = $persistentStorage->name; + $local_persistent_volumes_names[$name] = [ + 'name' => $name, + 'external' => false, + ]; + } + return $local_persistent_volumes_names; + } + + private function generate_environment_variables($ports) + { + $environment_variables = collect(); + foreach ($this->application->runtime_environment_variables_preview as $env) { + $environment_variables->push("$env->key=$env->value"); + } + foreach ($this->application->nixpacks_environment_variables_preview as $env) { + $environment_variables->push("$env->key=$env->value"); + } + // Add PORT if not exists, use the first port as default + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) { + $environment_variables->push("PORT={$ports[0]}"); + } + return $environment_variables->all(); + } + + private function generate_healthcheck_commands() + { + if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile' || $this->application->build_pack === 'dockerimage') { + // TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl. + return 'exit 0'; + } + if (!$this->application->health_check_port) { + $health_check_port = $this->application->ports_exposes_array[0]; + } else { + $health_check_port = $this->application->health_check_port; + } + if ($this->application->health_check_path) { + $this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path}"; + $generated_healthchecks_commands = [ + "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null" + ]; + } else { + $this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/"; + $generated_healthchecks_commands = [ + "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/" + ]; + } + return implode(' ', $generated_healthchecks_commands); + } + private function pull_latest_image($image) + { + $this->execute_remote_command( + ["echo -n 'Pulling latest image ($image) from the registry.'"], + + [ + executeInDocker($this->deployment_uuid, "docker pull {$image}"), "hidden" => true + ] + ); + } + private function build_image() + { + if ($this->application->build_pack === 'static') { + $this->execute_remote_command([ + "echo -n 'Static deployment. Copying static assets to the image.'", + ]); + } else { + $this->execute_remote_command( + [ + "echo -n 'Building docker image started.'", + ], + ["echo -n 'To check the current progress, click on Show Debug Logs.'"] + ); + } + + if ($this->application->settings->is_static || $this->application->build_pack === 'static') { + if ($this->application->static_image) { + $this->pull_latest_image($this->application->static_image); + } + if ($this->application->build_pack === 'static') { + $dockerfile = base64_encode("FROM {$this->application->static_image} +WORKDIR /usr/share/nginx/html/ +LABEL coolify.deploymentId={$this->deployment_uuid} +COPY . . +RUN rm -f /usr/share/nginx/html/nginx.conf +RUN rm -f /usr/share/nginx/html/Dockerfile +COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); + $nginx_config = base64_encode("server { + listen 80; + listen [::]:80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html; + try_files \$uri \$uri.html \$uri/index.html \$uri/ /index.html =404; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + }"); + } else { + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true + ]); + + $dockerfile = base64_encode("FROM {$this->application->static_image} +WORKDIR /usr/share/nginx/html/ +LABEL coolify.deploymentId={$this->deployment_uuid} +COPY --from=$this->build_image_name /app/{$this->application->publish_directory} . +COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); + + $nginx_config = base64_encode("server { + listen 80; + listen [::]:80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html; + try_files \$uri \$uri.html \$uri/index.html \$uri/ /index.html =404; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + }"); + } + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d > {$this->workdir}/Dockerfile") + ], + [ + executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf") + ], + [ + executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true + ] + ); + } else { + // Pure Dockerfile based deployment + if ($this->application->dockerfile) { + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "docker build --pull $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true + ]); + } else { + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true + ]); + } + } + $this->execute_remote_command([ + "echo -n 'Building docker image completed.'", + ]); + } + + private function stop_running_container(bool $force = false) + { + $this->execute_remote_command(["echo -n 'Removing old container.'"]); + if ($this->newVersionIsHealthy || $force) { + $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, 0); + $containers = $containers->filter(function ($container) { + return data_get($container, 'Names') !== $this->container_name; + }); + $containers->each(function ($container) { + $containerName = data_get($container, 'Names'); + $this->execute_remote_command( + [executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], + ); + }); + $this->execute_remote_command( + [ + "echo 'Rolling update completed.'" + ], + ); + } else { + $this->execute_remote_command( + ["echo -n 'New container is not healthy, rolling back to the old container.'"], + [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], + ); + } + } + + private function start_by_compose_file() + { + if ($this->application->build_pack === 'dockerimage') { + $this->execute_remote_command( + ["echo -n 'Pulling latest images from the registry.'"], + [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true], + [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], + ); + } else { + $this->execute_remote_command( + [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], + ); + } + } + + private function generate_build_env_variables() + { + $this->build_args = collect(["--build-arg SOURCE_COMMIT=\"{$this->commit}\""]); + foreach ($this->application->build_environment_variables_preview as $env) { + $this->build_args->push("--build-arg {$env->key}=\"{$env->value}\""); + } + $this->build_args = $this->build_args->implode(' '); + } + + private function add_build_env_variables_to_dockerfile() + { + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile' + ]); + $dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); + + foreach ($this->application->build_environment_variables as $env) { + $dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}"); + } + $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}{$this->dockerfile_location}"), + "hidden" => true + ]); + } + + private function next(string $status) + { + // If the deployment is cancelled by the user, don't update the status + if ($this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value) { + $this->application_deployment_queue->update([ + 'status' => $status, + ]); + } + queue_next_deployment($this->application); + if ($status === ApplicationDeploymentStatus::FINISHED->value) { + $this->application->environment->project->team->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview)); + } + if ($status === ApplicationDeploymentStatus::FAILED->value) { + $this->application->environment->project->team->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview)); + } + } + + public function failed(Throwable $exception): void + { + $this->execute_remote_command( + ["echo 'Oops something is not okay, are you okay? 😢'", 'type' => 'err'], + ["echo '{$exception->getMessage()}'", 'type' => 'err'], + ["echo -n 'Deployment failed. Removing the new version of your application.'", 'type' => 'err'], + [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true] + ); + + $this->next(ApplicationDeploymentStatus::FAILED->value); + } +} diff --git a/app/Models/Application.php b/app/Models/Application.php index f049351e2..785ef3040 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -332,4 +332,14 @@ public function isConfigurationChanged($save = false) return true; } } + public function isMultipleServerDeployment() + { + if (isDev()) { + return true; + } + if (data_get($this, 'additional_destinations') && data_get($this, 'docker_registry_image_name')) { + return true; + } + return false; + } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 0d57cea55..30e07c1d8 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -171,7 +171,7 @@ public function isServerReady() break; } $result = $this->validateConnection(); - ray('validateConnection: ' . $result); + // ray('validateConnection: ' . $result); if (!$result) { $serverUptimeCheckNumber++; $this->update([ diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index 0a53c5bf6..1306f645c 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -12,7 +12,6 @@ trait ExecuteRemoteCommand { public ?string $save = null; - public function execute_remote_command(...$commands) { static::$batch_counter++; diff --git a/app/Traits/ExecuteRemoteCommandNew.php b/app/Traits/ExecuteRemoteCommandNew.php new file mode 100644 index 000000000..ca56d9e50 --- /dev/null +++ b/app/Traits/ExecuteRemoteCommandNew.php @@ -0,0 +1,77 @@ +each(function ($singleCommand) use ($server, $logModel) { + $command = data_get($singleCommand, 'command') ?? $singleCommand[0] ?? null; + if ($command === null) { + throw new \RuntimeException('Command is not set'); + } + $hidden = data_get($singleCommand, 'hidden', false); + $customType = data_get($singleCommand, 'type'); + $ignoreErrors = data_get($singleCommand, 'ignore_errors', false); + $save = data_get($singleCommand, 'save'); + + $remote_command = generateSshCommand($server, $command); + $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $logModel, $save) { + $output = str($output)->trim(); + if ($output->startsWith('╔')) { + $output = "\n" . $output; + } + $newLogEntry = [ + 'command' => remove_iip($command), + 'output' => remove_iip($output), + 'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout', + 'timestamp' => Carbon::now('UTC'), + 'hidden' => $hidden, + 'batch' => static::$batch_counter, + ]; + + if (!$logModel->logs) { + $newLogEntry['order'] = 1; + } else { + $previousLogs = json_decode($logModel->logs, associative: true, flags: JSON_THROW_ON_ERROR); + $newLogEntry['order'] = count($previousLogs) + 1; + } + + $previousLogs[] = $newLogEntry; + $logModel->logs = json_encode($previousLogs, flags: JSON_THROW_ON_ERROR); + $logModel->save(); + + if ($save) { + $this->remoteCommandOutputs[$save] = str($output)->trim(); + } + }); + $logModel->update([ + 'current_process_id' => $process->id(), + ]); + + $processResult = $process->wait(); + if ($processResult->exitCode() !== 0) { + if (!$ignoreErrors) { + $status = ApplicationDeploymentStatus::FAILED->value; + $logModel->status = $status; + $logModel->save(); + throw new \RuntimeException($processResult->errorOutput()); + } + } + }); + } +} diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index fce225dc5..1cead8a54 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -1,8 +1,15 @@ count() > 0) { return; } + // New deployment + // dispatchDeploymentJob($deployment->id); dispatch(new ApplicationDeploymentJob( application_deployment_queue_id: $deployment->id, ))->onConnection('long-running')->onQueue('long-running'); + } function queue_next_deployment(Application $application) { $next_found = ApplicationDeploymentQueue::where('application_id', $application->id)->where('status', 'queued')->first(); if ($next_found) { + // New deployment + // dispatchDeploymentJob($next_found->id); dispatch(new ApplicationDeploymentJob( application_deployment_queue_id: $next_found->id, ))->onConnection('long-running')->onQueue('long-running'); + + } +} +function dispatchDeploymentJob($id) +{ + $applicationQueue = ApplicationDeploymentQueue::find($id); + $application = Application::find($applicationQueue->application_id); + + $isRestartOnly = data_get($applicationQueue, 'restart_only'); + $isSimpleDockerFile = data_get($application, 'dockerfile'); + $isDockerImage = data_get($application, 'build_pack') === 'dockerimage'; + + if ($isRestartOnly) { + ApplicationRestartJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running'); + } else if ($isSimpleDockerFile) { + ApplicationDeploySimpleDockerfileJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running'); + } else if ($isDockerImage) { + ApplicationDeployDockerImageJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running'); + } else { + throw new Exception('Unknown build pack'); + } +} + +// Deployment things +function generateHostIpMapping(Server $server, string $network) +{ + // Generate custom host<->ip hostnames + $allContainers = instant_remote_process(["docker network inspect {$network} -f '{{json .Containers}}' "], $server); + $allContainers = format_docker_command_output_to_json($allContainers); + $ips = collect([]); + if (count($allContainers) > 0) { + $allContainers = $allContainers[0]; + foreach ($allContainers as $container) { + $containerName = data_get($container, 'Name'); + if ($containerName === 'coolify-proxy') { + continue; + } + $containerIp = data_get($container, 'IPv4Address'); + if ($containerName && $containerIp) { + $containerIp = str($containerIp)->before('/'); + $ips->put($containerName, $containerIp->value()); + } + } + } + return $ips->map(function ($ip, $name) { + return "--add-host $name:$ip"; + })->implode(' '); +} + +function generateBaseDir(string $deplyomentUuid) +{ + return "/artifacts/$deplyomentUuid"; +} +function generateWorkdir(string $deplyomentUuid, Application $application) +{ + return generateBaseDir($deplyomentUuid) . rtrim($application->base_directory, '/'); +} + +function prepareHelperContainer(Server $server, string $network, string $deploymentUuid) +{ + $basedir = generateBaseDir($deploymentUuid); + $helperImage = config('coolify.helper_image'); + + $serverUserHomeDir = instant_remote_process(["echo \$HOME"], $server); + $dockerConfigFileExists = instant_remote_process(["test -f {$serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $server); + + $commands = collect([]); + if ($dockerConfigFileExists === 'OK') { + $commands->push([ + "command" => "docker run -d --network $network -v /:/host --name $deploymentUuid --rm -v {$serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock $helperImage", + "hidden" => true, + ]); + } else { + $commands->push([ + "command" => "docker run -d --network {$network} -v /:/host --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}", + "hidden" => true, + ]); + } + $commands->push([ + "command" => executeInDocker($deploymentUuid, "mkdir -p {$basedir}"), + "hidden" => true, + ]); + return $commands; +} + +function generateComposeFile(string $deploymentUuid, Server $server, string $network, Application $application, string $containerName, string $imageName, ?ApplicationPreview $preview = null, int $pullRequestId = 0) +{ + $ports = $application->settings->is_static ? [80] : $application->ports_exposes_array; + $workDir = generateWorkdir($deploymentUuid, $application); + $persistent_storages = generateLocalPersistentVolumes($application, $pullRequestId); + $volume_names = generateLocalPersistentVolumesOnlyVolumeNames($application, $pullRequestId); + $environment_variables = generateEnvironmentVariables($application, $ports, $pullRequestId); + + if (data_get($application, 'custom_labels')) { + $labels = collect(str($application->custom_labels)->explode(',')); + $labels = $labels->filter(function ($value, $key) { + return !str($value)->startsWith('coolify.'); + }); + $application->custom_labels = $labels->implode(','); + $application->save(); + } else { + $labels = collect(generateLabelsApplication($application, $preview)); + } + if ($pullRequestId !== 0) { + $labels = collect(generateLabelsApplication($application, $preview)); + } + $labels = $labels->merge(defaultLabels($application->id, $application->uuid, 0))->toArray(); + $docker_compose = [ + 'version' => '3.8', + 'services' => [ + $containerName => [ + 'image' => $imageName, + 'container_name' => $containerName, + 'restart' => RESTART_MODE, + 'environment' => $environment_variables, + 'labels' => $labels, + 'expose' => $ports, + 'networks' => [ + $network, + ], + 'mem_limit' => $application->limits_memory, + 'memswap_limit' => $application->limits_memory_swap, + 'mem_swappiness' => $application->limits_memory_swappiness, + 'mem_reservation' => $application->limits_memory_reservation, + 'cpus' => (int) $application->limits_cpus, + 'cpuset' => $application->limits_cpuset, + 'cpu_shares' => $application->limits_cpu_shares, + ] + ], + 'networks' => [ + $network => [ + 'external' => true, + 'name' => $network, + 'attachable' => true + ] + ] + ]; + if ($server->isLogDrainEnabled() && $application->isLogDrainEnabled()) { + $docker_compose['services'][$containerName]['logging'] = [ + 'driver' => 'fluentd', + 'options' => [ + 'fluentd-address' => "tcp://127.0.0.1:24224", + 'fluentd-async' => "true", + 'fluentd-sub-second-precision' => "true", + ] + ]; + } + if ($application->settings->is_gpu_enabled) { + ray('asd'); + $docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'] = [ + [ + 'driver' => data_get($application, 'settings.gpu_driver', 'nvidia'), + 'capabilities' => ['gpu'], + 'options' => data_get($application, 'settings.gpu_options', []) + ] + ]; + if (data_get($application, 'settings.gpu_count')) { + $count = data_get($application, 'settings.gpu_count'); + if ($count === 'all') { + $docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['count'] = $count; + } else { + $docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count; + } + } else if (data_get($application, 'settings.gpu_device_ids')) { + $docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($application, 'settings.gpu_device_ids'); + } + } + if ($application->isHealthcheckDisabled()) { + data_forget($docker_compose, 'services.' . $containerName . '.healthcheck'); + } + if (count($application->ports_mappings_array) > 0 && $pullRequestId === 0) { + $docker_compose['services'][$containerName]['ports'] = $application->ports_mappings_array; + } + if (count($persistent_storages) > 0) { + $docker_compose['services'][$containerName]['volumes'] = $persistent_storages; + } + if (count($volume_names) > 0) { + $docker_compose['volumes'] = $volume_names; + } + $docker_compose = Yaml::dump($docker_compose, 10); + $docker_compose_base64 = base64_encode($docker_compose); + $commands = collect([]); + $commands->push([ + "command" => executeInDocker($deploymentUuid, "echo '{$docker_compose_base64}' | base64 -d > {$workDir}/docker-compose.yml"), + "hidden" => true, + ]); + return $commands; +} +function generateLocalPersistentVolumes(Application $application, int $pullRequestId = 0) +{ + $local_persistent_volumes = []; + foreach ($application->persistentStorages as $persistentStorage) { + $volume_name = $persistentStorage->host_path ?? $persistentStorage->name; + if ($pullRequestId !== 0) { + $volume_name = $volume_name . '-pr-' . $pullRequestId; + } + $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + } + return $local_persistent_volumes; +} + +function generateLocalPersistentVolumesOnlyVolumeNames(Application $application, int $pullRequestId = 0) +{ + $local_persistent_volumes_names = []; + foreach ($application->persistentStorages as $persistentStorage) { + if ($persistentStorage->host_path) { + continue; + } + $name = $persistentStorage->name; + + if ($pullRequestId !== 0) { + $name = $name . '-pr-' . $pullRequestId; + } + + $local_persistent_volumes_names[$name] = [ + 'name' => $name, + 'external' => false, + ]; + } + return $local_persistent_volumes_names; +} +function generateEnvironmentVariables(Application $application, $ports, int $pullRequestId = 0) +{ + $environment_variables = collect(); + // ray('Generate Environment Variables')->green(); + if ($pullRequestId === 0) { + // ray($this->application->runtime_environment_variables)->green(); + foreach ($application->runtime_environment_variables as $env) { + $environment_variables->push("$env->key=$env->value"); + } + foreach ($application->nixpacks_environment_variables as $env) { + $environment_variables->push("$env->key=$env->value"); + } + } else { + // ray($this->application->runtime_environment_variables_preview)->green(); + foreach ($application->runtime_environment_variables_preview as $env) { + $environment_variables->push("$env->key=$env->value"); + } + foreach ($application->nixpacks_environment_variables_preview as $env) { + $environment_variables->push("$env->key=$env->value"); + } + } + // Add PORT if not exists, use the first port as default + if ($environment_variables->filter(fn ($env) => str($env)->contains('PORT'))->isEmpty()) { + $environment_variables->push("PORT={$ports[0]}"); + } + return $environment_variables->all(); +} + +function rollingUpdate(Application $application, string $deploymentUuid) +{ + $commands = collect([]); + $workDir = generateWorkdir($deploymentUuid, $application); + if (count($application->ports_mappings_array) > 0) { + // $this->execute_remote_command( + // [ + // "echo '\n----------------------------------------'", + // ], + // ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"], + // ); + // $this->stop_running_container(force: true); + // $this->start_by_compose_file(); + } else { + $commands->push( + [ + "command" => "echo '\n----------------------------------------'" + ], + [ + "command" => "echo -n 'Rolling update started.'" + ] + ); + if ($application->build_pack === 'dockerimage') { + $commands->push( + ["echo -n 'Pulling latest images from the registry.'"], + [executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} pull"), "hidden" => true], + [executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"), "hidden" => true], + ); + } else { + $commands->push( + [executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"), "hidden" => true], + ); + } + return $commands; } }