From ce0f560c447892dd641bd0318448f6535fe180e4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 13 Nov 2023 11:09:21 +0100 Subject: [PATCH] Add service-specific configuration fields and save them to the database --- .../Livewire/Project/Service/StackForm.php | 28 ++- app/Models/Service.php | 183 +++++++++++++++++- app/Models/ServiceApplication.php | 10 + app/Models/ServiceDatabase.php | 1 - app/View/Components/Services/Links.php | 38 ++-- bootstrap/helpers/constants.php | 3 + bootstrap/helpers/docker.php | 33 ++++ .../components/services/navbar.blade.php | 2 +- resources/views/layouts/app.blade.php | 2 +- .../project/service/application.blade.php | 16 +- .../livewire/project/service/index.blade.php | 6 +- .../project/service/stack-form.blade.php | 16 +- 12 files changed, 292 insertions(+), 46 deletions(-) diff --git a/app/Http/Livewire/Project/Service/StackForm.php b/app/Http/Livewire/Project/Service/StackForm.php index 30a3e7380..ebdb2d481 100644 --- a/app/Http/Livewire/Project/Service/StackForm.php +++ b/app/Http/Livewire/Project/Service/StackForm.php @@ -7,17 +7,34 @@ use Livewire\Component; class StackForm extends Component { public $service; - public $isConfigurationRequired = false; + public $fields = []; protected $listeners = ["saveCompose"]; - protected $rules = [ + public $rules = [ 'service.docker_compose_raw' => 'required', 'service.docker_compose' => 'required', 'service.name' => 'required', 'service.description' => 'nullable', ]; - public function mount () { - if ($this->service->applications->filter(fn($app) => str($app->image)->contains('minio/minio'))->count() > 0) { - $this->isConfigurationRequired = true; + public $validationAttributes = []; + public function mount() + { + $extraFields = $this->service->extraFields(); + foreach ($extraFields as $serviceName => $fields) { + foreach ($fields as $fieldKey => $field) { + $key = data_get($field, 'key'); + $value = data_get($field, 'value'); + $rules = data_get($field, 'rules'); + $isPassword = data_get($field, 'isPassword'); + $this->fields[$key] = [ + "serviceName" => $serviceName, + "key" => $key, + "name" => $fieldKey, + "value" => $value, + "isPassword" => $isPassword, + ]; + $this->rules["fields.$key.value"] = $rules; + $this->validationAttributes["fields.$key.value"] = $fieldKey; + } } } public function saveCompose($raw) @@ -32,6 +49,7 @@ class StackForm extends Component try { $this->validate(); $this->service->save(); + $this->service->saveExtraFields($this->fields); $this->service->parse(); $this->service->refresh(); $this->service->saveComposeConfigs(); diff --git a/app/Models/Service.php b/app/Models/Service.php index 14084f282..eabdc1d88 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -45,7 +45,168 @@ class Service extends BaseModel { return 'service'; } + public function extraFields() + { + $fields = collect([]); + $applications = $this->applications()->get(); + foreach ($applications as $application) { + $image = str($application->image)->before(':')->value(); + switch ($image) { + case str($image)->contains('minio'): + $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(); + $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, + ], + ]); + break; + } + } + $databases = $this->databases()->get(); + foreach ($databases as $database) { + $image = str($database->image)->before(':')->value(); + switch ($image) { + case str($image)->contains('postgres'): + $userVariables = ['SERVICE_USER_POSTGRES', 'SERVICE_USER_POSTGRESQL']; + $passwordVariables = ['SERVICE_PASSWORD_POSTGRES', 'SERVICE_PASSWORD_POSTGRESQL']; + $dbNameVariables = ['POSTGRESQL_DATABASE', 'POSTGRES_DB']; + $postgres_user = $this->environment_variables()->whereIn('key', $userVariables)->first(); + $postgres_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first(); + $postgres_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first(); + $fields->put('PostgreSQL', [ + 'User' => [ + 'key' => data_get($postgres_user, 'key'), + 'value' => data_get($postgres_user, 'value'), + 'rules' => 'required', + ], + 'Password' => [ + 'key' => data_get($postgres_password, 'key'), + 'value' => data_get($postgres_password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + 'Database Name' => [ + 'key' => data_get($postgres_db_name, 'key'), + 'value' => data_get($postgres_db_name, 'value'), + 'rules' => 'required', + ], + ]); + break; + case str($image)->contains('mysql'): + $userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS']; + $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS']; + $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT']; + $dbNameVariables = ['MYSQL_DATABASE']; + $mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first(); + $mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first(); + $mysql_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first(); + $mysql_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first(); + $fields->put('MySQL', [ + 'User' => [ + 'key' => data_get($mysql_user, 'key'), + 'value' => data_get($mysql_user, 'value'), + 'rules' => 'required', + ], + 'Password' => [ + 'key' => data_get($mysql_password, 'key'), + 'value' => data_get($mysql_password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + 'Root Password' => [ + 'key' => data_get($mysql_root_password, 'key'), + 'value' => data_get($mysql_root_password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + 'Database Name' => [ + 'key' => data_get($mysql_db_name, 'key'), + 'value' => data_get($mysql_db_name, 'value'), + 'rules' => 'required', + ], + ]); + break; + case str($image)->contains('mariadb'): + $userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER']; + $passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS']; + $rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS']; + $dbNameVariables = ['SERVICE_DATABASE_MARIADB', 'SERVICE_DATABASE_WORDPRESS', '_APP_DB_SCHEMA']; + $mariadb_user = $this->environment_variables()->whereIn('key', $userVariables)->first(); + $mariadb_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first(); + $mariadb_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first(); + $mariadb_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first(); + $fields->put('MariaDB', [ + 'User' => [ + 'key' => data_get($mariadb_user, 'key'), + 'value' => data_get($mariadb_user, 'value'), + 'rules' => 'required', + ], + 'Password' => [ + 'key' => data_get($mariadb_password, 'key'), + 'value' => data_get($mariadb_password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + 'Root Password' => [ + 'key' => data_get($mariadb_root_password, 'key'), + 'value' => data_get($mariadb_root_password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + 'Database Name' => [ + 'key' => data_get($mariadb_db_name, 'key'), + 'value' => data_get($mariadb_db_name, 'value'), + 'rules' => data_get($mariadb_db_name, 'value') && 'required', + ], + ]); + + break; + } + } + return $fields; + } + public function saveExtraFields($fields) + { + foreach ($fields as $field) { + $key = data_get($field, 'key'); + $value = data_get($field, 'value'); + $found = $this->environment_variables()->where('key', $key)->first(); + if ($found) { + $found->value = $value; + $found->save(); + } else { + $this->environment_variables()->create([ + 'key' => $key, + 'value' => $value, + 'is_build_time' => false, + 'service_id' => $this->id, + 'is_preview' => false, + ]); + } + } + } public function documentation() { $services = getServiceTemplates(); @@ -257,7 +418,7 @@ class Service extends BaseModel } } $networks = collect(); - foreach ($serviceNetworks as $key =>$serviceNetwork) { + foreach ($serviceNetworks as $key => $serviceNetwork) { if (gettype($serviceNetwork) === 'string') { // networks: // - appwrite @@ -268,7 +429,7 @@ class Service extends BaseModel // ipv4_address: 192.168.203.254 // $networks->put($serviceNetwork, null); ray($key); - $networks->put($key,$serviceNetwork); + $networks->put($key, $serviceNetwork); } } foreach ($definedNetwork as $key => $network) { @@ -564,12 +725,18 @@ class Service extends BaseModel } // Add labels to the service - $fqdns = collect(data_get($savedService, 'fqdns')); - $defaultLabels = defaultLabels($this->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id); - $serviceLabels = $serviceLabels->merge($defaultLabels); - if (!$isDatabase && $fqdns->count() > 0) { - if ($fqdns) { - $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true)); + if (!$isDatabase) { + if ($savedService->serviceType()) { + $fqdns = generateServiceSpecificFqdns($savedService, forTraefik: true); + } else { + $fqdns = collect(data_get($savedService, 'fqdns')); + } + $defaultLabels = defaultLabels($this->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id); + $serviceLabels = $serviceLabels->merge($defaultLabels); + if ($fqdns->count() > 0) { + if ($fqdns) { + $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true)); + } } } data_set($service, 'labels', $serviceLabels->toArray()); diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index c70ceeccf..b1db1c581 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -22,6 +22,16 @@ class ServiceApplication extends BaseModel { return 'service'; } + public function serviceType() + { + $found = str(collect(SPECIFIC_SERVICES)->filter(function ($service) { + return str($this->image)->before(':')->value() === $service; + })->first()); + if ($found->isNotEmpty()) { + return $found; + } + return null; + } public function service() { return $this->belongsTo(Service::class); diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index 0dbbf6196..e37821af2 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -29,7 +29,6 @@ class ServiceDatabase extends BaseModel return "standalone-$image"; } public function getServiceDatabaseUrl() { - // $type = $this->databaseType(); $port = $this->public_port; $realIp = $this->service->server->ip; if ($realIp === 'host.docker.internal' || isDev()) { diff --git a/app/View/Components/Services/Links.php b/app/View/Components/Services/Links.php index b2cc8618d..b3953c174 100644 --- a/app/View/Components/Services/Links.php +++ b/app/View/Components/Services/Links.php @@ -16,22 +16,28 @@ class Links extends Component { $this->links = collect([]); $service->applications()->get()->map(function ($application) { - if ($application->fqdn) { - $fqdns = collect(Str::of($application->fqdn)->explode(',')); - $fqdns->map(function ($fqdn) { - $this->links->push(getFqdnWithoutPort($fqdn)); - }); - } - if ($application->ports) { - $portsCollection = collect(Str::of($application->ports)->explode(',')); - $portsCollection->map(function ($port) { - if (Str::of($port)->contains(':')) { - $hostPort = Str::of($port)->before(':'); - } else { - $hostPort = $port; - } - $this->links->push(base_url(withPort:false) . ":{$hostPort}"); - }); + $type = $application->serviceType(); + if ($type) { + $links = generateServiceSpecificFqdns($application, false); + $this->links = $this->links->merge($links); + } else { + if ($application->fqdn) { + $fqdns = collect(Str::of($application->fqdn)->explode(',')); + $fqdns->map(function ($fqdn) { + $this->links->push(getFqdnWithoutPort($fqdn)); + }); + } + if ($application->ports) { + $portsCollection = collect(Str::of($application->ports)->explode(',')); + $portsCollection->map(function ($port) { + if (Str::of($port)->contains(':')) { + $hostPort = Str::of($port)->before(':'); + } else { + $hostPort = $port; + } + $this->links->push(base_url(withPort: false) . ":{$hostPort}"); + }); + } } }); } diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index 10c1353d3..e844efea9 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -23,3 +23,6 @@ const DATABASE_DOCKER_IMAGES = [ 'influxdb', 'clickhouse/clickhouse-server' ]; +const SPECIFIC_SERVICES = [ + 'quay.io/minio/minio', +]; diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 90c4179a7..f932c95f3 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -144,6 +144,39 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica } return $labels; } +function generateServiceSpecificFqdns($service, $forTraefik = false) +{ + $variables = collect($service->service->environment_variables); + $type = $service->serviceType(); + $payload = collect([]); + switch ($type) { + case $type->contains('minio'): + $MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first(); + if (is_null($MINIO_BROWSER_REDIRECT_URL->value)) { + $MINIO_BROWSER_REDIRECT_URL->update([ + "value" => generateFqdn($service->service->server, 'console-' . $service->uuid) + ]); + } + $MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first(); + if (is_null($MINIO_SERVER_URL->value)) { + $MINIO_SERVER_URL->update([ + "value" => generateFqdn($service->service->server, 'minio-' . $service->uuid) + ]); + } + if ($forTraefik) { + $payload = collect([ + $MINIO_BROWSER_REDIRECT_URL->value . ':9001', + $MINIO_SERVER_URL->value . ':9000', + ]); + } else { + $payload = collect([ + $MINIO_BROWSER_REDIRECT_URL->value, + $MINIO_SERVER_URL->value, + ]); + } + } + return $payload; +} function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled, $onlyPort = null) { $labels = collect([]); diff --git a/resources/views/components/services/navbar.blade.php b/resources/views/components/services/navbar.blade.php index 1d0cf03a5..6c9af5f07 100644 --- a/resources/views/components/services/navbar.blade.php +++ b/resources/views/components/services/navbar.blade.php @@ -3,7 +3,7 @@ href="{{ route('project.service', $parameters) }}"> - +
@if (serviceStatus($service) === 'degraded')