From 99efa857f416d5f08978e286eb8f7dddd777f91b Mon Sep 17 00:00:00 2001
From: Andras Bacsai <andras.bacsai@gmail.com>
Date: Thu, 15 Feb 2024 11:55:43 +0100
Subject: [PATCH] feat: add metabase feat: consistent container names fix: for
 services, you only need to add basicauth label, others are added by coolify
 fix: label uuids are not randomly generated all the time fix: changing force
 https will change the labels

---
 app/Jobs/ApplicationDeploymentJob.php         | 35 ++++++++----
 app/Livewire/Project/Application/Advanced.php | 10 +++-
 app/Livewire/Project/Application/General.php  | 13 +----
 app/Models/Application.php                    |  2 +-
 bootstrap/helpers/docker.php                  | 53 ++++++++++++++++---
 bootstrap/helpers/shared.php                  |  4 +-
 ..._consistent_application_container_name.php | 28 ++++++++++
 .../project/application/advanced.blade.php    |  6 ++-
 templates/compose/metabase.yaml               | 35 ++++++++++++
 templates/service-templates.json              | 11 ++++
 10 files changed, 162 insertions(+), 35 deletions(-)
 create mode 100644 database/migrations/2024_02_15_101921_add_consistent_application_container_name.php
 create mode 100644 templates/compose/metabase.yaml

diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index ccf1bba3d..aa68c20ad 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -133,6 +133,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
         $this->is_debug_enabled = $this->application->settings->is_debug_enabled;
 
         $this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id);
+        ray('New container name: ', $this->container_name);
+
         savePrivateKeyToFs($this->server);
         $this->saved_outputs = collect();
 
@@ -711,9 +713,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
                 $this->write_deployment_configurations();
                 $this->server = $this->original_server;
             }
-            if (count($this->application->ports_mappings_array) > 0) {
+            if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled) {
                 $this->application_deployment_queue->addLogEntry("----------------------------------------");
-                $this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported.");
+                if (count($this->application->ports_mappings_array) > 0) {
+                    $this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported.");
+                }
+                if ((bool) $this->application->settings->is_consistent_container_name_enabled) {
+                    $this->application_deployment_queue->addLogEntry("Consistent container name feature enabled, rolling update is not supported.");
+                }
                 $this->stop_running_container(force: true);
                 $this->start_by_compose_file();
             } else {
@@ -1199,13 +1206,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
         //     ];
         // }
 
-        $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
-
-        data_forget($docker_compose, 'services.' . $this->container_name);
-
-        $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
-        if (count($custom_compose) > 0) {
-            $docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose);
+        if ((bool)$this->application->settings->is_consistent_container_name_enabled) {
+            $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
+            if (count($custom_compose) > 0) {
+                $docker_compose['services'][$this->container_name] = array_merge_recursive($docker_compose['services'][$this->container_name], $custom_compose);
+            }
+        } else {
+            $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
+            data_forget($docker_compose, 'services.' . $this->container_name);
+            $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
+            if (count($custom_compose) > 0) {
+                $docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose);
+            }
         }
 
         $this->docker_compose = Yaml::dump($docker_compose, 10);
@@ -1490,6 +1502,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
                     [executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
                 );
             });
+            if ($this->application->settings->is_consistent_container_name_enabled) {
+                $this->execute_remote_command(
+                    [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
+                );
+            }
         } else {
             $this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container.");
             $this->application_deployment_queue->update([
diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php
index a2b7afa6a..08b4f9523 100644
--- a/app/Livewire/Project/Application/Advanced.php
+++ b/app/Livewire/Project/Application/Advanced.php
@@ -8,20 +8,25 @@ use Livewire\Component;
 class Advanced extends Component
 {
     public Application $application;
+    public bool $is_force_https_enabled;
     protected $rules = [
         'application.settings.is_git_submodules_enabled' => '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',
+        'is_force_https_enabled' => 'boolean|required',
         'application.settings.is_log_drain_enabled' => 'boolean|required',
         'application.settings.is_gpu_enabled' => 'boolean|required',
         'application.settings.is_build_server_enabled' => 'boolean|required',
+        'application.settings.is_consistent_container_name_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 mount() {
+        $this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
+    }
     public function instantSave()
     {
         if ($this->application->isLogDrainEnabled()) {
@@ -31,7 +36,8 @@ class Advanced extends Component
                 return;
             }
         }
-        if ($this->application->settings->is_force_https_enabled) {
+        if ($this->application->settings->is_force_https_enabled !== $this->is_force_https_enabled) {
+            $this->application->settings->is_force_https_enabled = $this->is_force_https_enabled;
             $this->dispatch('resetDefaultLabels', false);
         }
         $this->application->settings->save();
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index c11dbfe4b..673d2473f 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -126,7 +126,6 @@ class General extends Component
             $this->application->save();
         }
         $this->initialDockerComposeLocation = $this->application->docker_compose_location;
-        $this->checkLabelUpdates();
     }
     public function instantSave()
     {
@@ -184,15 +183,6 @@ class General extends Component
         $this->submit();
         $this->dispatch('build_pack_updated');
     }
-    public function checkLabelUpdates()
-    {
-        if (md5($this->application->custom_labels) !== md5(implode("|", generateLabelsApplication($this->application)))) {
-            $this->labelsChanged = true;
-        } else {
-            $this->labelsChanged = false;
-        }
-    }
-
     public function getWildcardDomain()
     {
         $server = data_get($this->application, 'destination.server');
@@ -246,7 +236,7 @@ class General extends Component
                 if ($this->application->additional_servers->count() === 0) {
                     foreach ($domains as $domain) {
                         if (!validate_dns_entry($domain, $this->application->destination->server)) {
-                            $showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.","Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='text-white underline' href='https://coolify.io/docs/dns-settings'>documentation</a> for further help.");
+                            $showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.", "Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='text-white underline' href='https://coolify.io/docs/dns-settings'>documentation</a> for further help.");
                         }
                     }
                 }
@@ -279,7 +269,6 @@ class General extends Component
         } catch (\Throwable $e) {
             return handleError($e, $this);
         } finally {
-            $this->checkLabelUpdates();
             $this->isConfigurationChanged = $this->application->isConfigurationChanged();
         }
     }
diff --git a/app/Models/Application.php b/app/Models/Application.php
index b37476565..df1fb8038 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -470,7 +470,7 @@ class Application extends BaseModel
     {
         return data_get($this, 'settings.is_log_drain_enabled', false);
     }
-    public function isConfigurationChanged($save = false)
+    public function isConfigurationChanged(bool $save = false)
     {
         $newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command  . $this->build_command . $this->start_command . $this->port_exposes . $this->port_mappings . $this->base_directory . $this->publish_directory . $this->dockerfile . $this->dockerfile_location . $this->custom_labels;
         if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 5308b3fa8..ae8f259fc 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -123,10 +123,14 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
 
 function generateApplicationContainerName(Application $application, $pull_request_id = 0)
 {
+    $consistent_container_name = $application->settings->is_consistent_container_name_enabled;
     $now = now()->format('Hisu');
     if ($pull_request_id !== 0 && $pull_request_id !== null) {
         return $application->uuid . '-pr-' . $pull_request_id;
     } else {
+        if ($consistent_container_name) {
+            return $application->uuid;
+        }
         return $application->uuid . '-' . $now;
     }
 }
@@ -209,15 +213,34 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource,
     }
     return $payload;
 }
-function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null)
+function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null)
 {
     $labels = collect([]);
     $labels->push('traefik.enable=true');
     $labels->push("traefik.http.middlewares.gzip.compress=true");
     $labels->push("traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https");
+
+    $basic_auth = false;
+    $basic_auth_middleware = null;
+
+    if ($serviceLabels) {
+        $basic_auth = $serviceLabels->contains(function ($value) {
+            return str_contains($value, 'basicauth');
+        });
+        if ($basic_auth) {
+            $basic_auth_middleware = $serviceLabels
+                ->map(function ($item) {
+                    if (preg_match('/traefik\.http\.middlewares\.(.*?)\.basicauth\.users/', $item, $matches)) {
+                        return $matches[1];
+                    }
+                })
+                ->filter()
+                ->first();
+        }
+    }
     foreach ($domains as $loop => $domain) {
         try {
-            $uuid = new Cuid2(7);
+            // $uuid = new Cuid2(7);
             $url = Url::fromString($domain);
             $host = $url->getHost();
             $path = $url->getPath();
@@ -239,11 +262,18 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
                 }
                 if ($path !== '/') {
                     $labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}");
-                    $labels->push("traefik.http.routers.{$https_label}.middlewares={$https_label}-stripprefix,gzip");
+                    $middlewares = "gzip,{$https_label}-stripprefix";
+                    if ($basic_auth  && $basic_auth_middleware) {
+                        $middlewares = $middlewares . ',' . $basic_auth_middleware;
+                    }
+                    $labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}");
                 } else {
-                    $labels->push("traefik.http.routers.{$https_label}.middlewares=gzip");
+                    $middlewares = "gzip";
+                    if ($basic_auth && $basic_auth_middleware) {
+                        $middlewares = $middlewares . ',' . $basic_auth_middleware;
+                    }
+                    $labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}");
                 }
-
                 $labels->push("traefik.http.routers.{$https_label}.tls=true");
                 $labels->push("traefik.http.routers.{$https_label}.tls.certresolver=letsencrypt");
 
@@ -267,16 +297,23 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
                 }
                 if ($path !== '/') {
                     $labels->push("traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}");
-                    $labels->push("traefik.http.routers.{$http_label}.middlewares={$http_label}-stripprefix,gzip");
+                    $middlewares = "gzip,{$http_label}-stripprefix";
+                    if ($basic_auth  && $basic_auth_middleware) {
+                        $middlewares = $middlewares . ',' . $basic_auth_middleware;
+                    }
+                    $labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}");
                 } else {
-                    $labels->push("traefik.http.routers.{$http_label}.middlewares=gzip");
+                    $middlewares = "gzip";
+                    if ($basic_auth && $basic_auth_middleware) {
+                        $middlewares = $middlewares . ',' . $basic_auth_middleware;
+                    }
+                    $labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}");
                 }
             }
         } catch (\Throwable $e) {
             continue;
         }
     }
-
     return $labels->sort();
 }
 function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null): array
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 9ab26e59a..bc33388eb 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -1039,7 +1039,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
                 $serviceLabels = $serviceLabels->merge($defaultLabels);
                 if (!$isDatabase && $fqdns->count() > 0) {
                     if ($fqdns) {
-                        $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($resource->uuid, $fqdns, true));
+                        $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($resource->uuid, $fqdns, true, serviceLabels: $serviceLabels));
                     }
                 }
                 if ($resource->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) {
@@ -1480,7 +1480,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
                                 return $preview_fqdn;
                             });
                         }
-                        $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($uuid, $fqdns));
+                        $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($uuid, $fqdns,serviceLabels: $serviceLabels));
                     }
                 }
             }
diff --git a/database/migrations/2024_02_15_101921_add_consistent_application_container_name.php b/database/migrations/2024_02_15_101921_add_consistent_application_container_name.php
new file mode 100644
index 000000000..827b4c426
--- /dev/null
+++ b/database/migrations/2024_02_15_101921_add_consistent_application_container_name.php
@@ -0,0 +1,28 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('application_settings', function (Blueprint $table) {
+            $table->boolean('is_consistent_container_name_enabled')->default(false);
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('application_settings', function (Blueprint $table) {
+            $table->dropColumn('is_consistent_container_name_enabled');
+        });
+    }
+};
diff --git a/resources/views/livewire/project/application/advanced.blade.php b/resources/views/livewire/project/application/advanced.blade.php
index 49196b208..53d984698 100644
--- a/resources/views/livewire/project/application/advanced.blade.php
+++ b/resources/views/livewire/project/application/advanced.blade.php
@@ -15,7 +15,11 @@
             @endif
             <x-forms.checkbox
                 helper="Your application will be available only on https if your domain starts with https://..."
-                instantSave id="application.settings.is_force_https_enabled" label="Force Https" />
+                instantSave id="is_force_https_enabled" label="Force Https" />
+            <x-forms.checkbox
+                helper="The deployed container will have the same name ({{ $application->uuid }}). <span class='font-bold text-warning'>You will lose the rolling update feature!</span>"
+                instantSave id="application.settings.is_consistent_container_name_enabled"
+                label="Consistent Container Names" />
             <h4>Logs</h4>
             @if (!$application->settings->is_raw_compose_deployment_enabled)
                 <x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
diff --git a/templates/compose/metabase.yaml b/templates/compose/metabase.yaml
new file mode 100644
index 000000000..b038269e5
--- /dev/null
+++ b/templates/compose/metabase.yaml
@@ -0,0 +1,35 @@
+# documentation: https://www.metabase.com/docs/latest/installation-and-operation/running-metabase-on-docker
+# slogan: Fast analytics with the friendly UX and integrated tooling to let your company explore data on their own.
+# tags: analytics,bi,business,intelligence
+
+services:
+  metabase:
+    image: metabase/metabase:latest
+    volumes:
+      - /dev/urandom:/dev/random:ro
+    environment:
+      - SERVICE_FQDN_METABASE
+      - MB_DB_TYPE=postgres
+      - MB_DB_HOST=postgresql
+      - MB_DB_PORT=5432
+      - MB_DB_DBNAME=${POSTGRESQL_DATABASE:-metabase}
+      - MB_DB_USER=$SERVICE_USER_POSTGRESQL
+      - MB_DB_PASS=$SERVICE_PASSWORD_POSTGRESQL
+    healthcheck:
+      test: curl --fail -I http://localhost:3000/api/health || exit 1
+      interval: 5s
+      timeout: 20s
+      retries: 10
+  postgresql:
+    image: postgres:16-alpine
+    volumes:
+      - metabase-postgresql-data:/var/lib/postgresql/data
+    environment:
+      - POSTGRES_USER=${SERVICE_USER_POSTGRESQL}
+      - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRESQL}
+      - POSTGRES_DB=${POSTGRESQL_DATABASE:-metabase}
+    healthcheck:
+      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
+      interval: 5s
+      timeout: 20s
+      retries: 10
diff --git a/templates/service-templates.json b/templates/service-templates.json
index 8d13fe743..3aae101b5 100644
--- a/templates/service-templates.json
+++ b/templates/service-templates.json
@@ -329,6 +329,17 @@
             "meilisearch"
         ]
     },
+    "metabase": {
+        "documentation": "https:\/\/www.metabase.com\/docs\/latest\/installation-and-operation\/running-metabase-on-docker",
+        "slogan": "Fast analytics with the friendly UX and integrated tooling to let your company explore data on their own.",
+        "compose": "c2VydmljZXM6CiAgbWV0YWJhc2U6CiAgICBpbWFnZTogJ21ldGFiYXNlL21ldGFiYXNlOmxhdGVzdCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJy9kZXYvdXJhbmRvbTovZGV2L3JhbmRvbTpybycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NRVRBQkFTRQogICAgICAtIE1CX0RCX1RZUEU9cG9zdGdyZXMKICAgICAgLSBNQl9EQl9IT1NUPXBvc3RncmVzcWwKICAgICAgLSBNQl9EQl9QT1JUPTU0MzIKICAgICAgLSAnTUJfREJfREJOQU1FPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotbWV0YWJhc2V9JwogICAgICAtIE1CX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMCiAgICAgIC0gTUJfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ2N1cmwgLS1mYWlsIC1JIGh0dHA6Ly9sb2NhbGhvc3Q6MzAwMC9hcGkvaGVhbHRoIHx8IGV4aXQgMScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ21ldGFiYXNlLXBvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTUUxfREFUQUJBU0U6LW1ldGFiYXNlfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK",
+        "tags": [
+            "analytics",
+            "bi",
+            "business",
+            "intelligence"
+        ]
+    },
     "metube": {
         "documentation": "https:\/\/github.com\/alexta69\/metube",
         "slogan": "A web GUI for youtube-dl with playlist support. It enables you to effortlessly download videos from YouTube and dozens of other sites.",