From 35b1a81dfe18d9142eb1d38286f8dfa0a4b5c325 Mon Sep 17 00:00:00 2001 From: Alejandro Akbal <37181533+AlejandroAkbal@users.noreply.github.com> Date: Sun, 12 Nov 2023 12:10:53 +0000 Subject: [PATCH 01/12] fix(fider template): use the correct docs url --- templates/compose/fider.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/fider.yaml b/templates/compose/fider.yaml index e2bf00910..b75ba68cd 100644 --- a/templates/compose/fider.yaml +++ b/templates/compose/fider.yaml @@ -1,4 +1,4 @@ -# documentation: https://fider.io/doc +# documentation: https://fider.io/docs # slogan: Fider is an open-source feedback platform for collecting and managing user feedback, helping you prioritize improvements to your products and services. # tags: feedback, user-feedback From 30db2b2a090698e195cd5c76df89d30cc0ae80be Mon Sep 17 00:00:00 2001 From: Ashik Nesin Date: Sun, 12 Nov 2023 19:30:20 +0000 Subject: [PATCH 02/12] Update typo in onboarding screen --- resources/views/livewire/boarding/index.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/boarding/index.blade.php b/resources/views/livewire/boarding/index.blade.php index 03b554882..4bac29022 100644 --- a/resources/views/livewire/boarding/index.blade.php +++ b/resources/views/livewire/boarding/index.blade.php @@ -67,7 +67,7 @@ services, called resources. Any CPU intensive process will use the server's CPU where you are deploying your resources.

Localhost is the server where Coolify is running on. It is not recommended to use one server - for everyting.

+ for everything.

Remote Server is a server reachable through SSH. It can be hosted at home, or from any cloud provider.

From e49caba9206733d620fcf79909945857735f1393 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 13 Nov 2023 08:46:17 +0100 Subject: [PATCH 03/12] Add STRIPE_EXCLUDED_PLANS to services in docker-compose.prod.yml --- docker-compose.prod.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 0c427d233..be972721f 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -44,6 +44,7 @@ services: - STRIPE_PRICE_ID_PRO_YEARLY - STRIPE_PRICE_ID_ULTIMATE_MONTHLY - STRIPE_PRICE_ID_ULTIMATE_YEARLY + - STRIPE_EXCLUDED_PLANS - PADDLE_VENDOR_ID - PADDLE_WEBHOOK_SECRET - PADDLE_VENDOR_AUTH_CODE From 363e8fc0b50ed0260ec1610833d2ca57d53a4359 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 13 Nov 2023 08:46:43 +0100 Subject: [PATCH 04/12] Update code with bug fixes and improvements --- .tinkerwell/snippets/DeleteUser.php | 22 +++++++++++++++++++ .../Livewire/Project/Application/General.php | 2 +- config/sentry.php | 2 +- config/version.php | 2 +- versions.json | 2 +- 5 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 .tinkerwell/snippets/DeleteUser.php diff --git a/.tinkerwell/snippets/DeleteUser.php b/.tinkerwell/snippets/DeleteUser.php new file mode 100644 index 000000000..bd562557b --- /dev/null +++ b/.tinkerwell/snippets/DeleteUser.php @@ -0,0 +1,22 @@ +first(); +$teams = $user->teams; +foreach ($teams as $team) { + $servers = $team->servers; + if ($servers->count() > 0) { + foreach ($servers as $server) { + dump($server); + $server->delete(); + } + } + dump($team); + $team->delete(); +} +if ($user) { + dump($user); + $user->delete(); +} diff --git a/app/Http/Livewire/Project/Application/General.php b/app/Http/Livewire/Project/Application/General.php index bc853108f..3cb4e4142 100644 --- a/app/Http/Livewire/Project/Application/General.php +++ b/app/Http/Livewire/Project/Application/General.php @@ -5,7 +5,7 @@ namespace App\Http\Livewire\Project\Application; use App\Models\Application; use Illuminate\Support\Collection; use Illuminate\Support\Str; -use Livewire\Component; +use Livewire\Componwent; class General extends Component { diff --git a/config/sentry.php b/config/sentry.php index c889afb12..d52a8a9b4 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ return [ // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.123', + 'release' => '4.0.0-beta.124', // 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 f31b19ca0..352ef7c4b 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ Date: Mon, 13 Nov 2023 09:04:19 +0100 Subject: [PATCH 05/12] Fix typo in General.php component --- app/Http/Livewire/Project/Application/General.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Livewire/Project/Application/General.php b/app/Http/Livewire/Project/Application/General.php index 3cb4e4142..bc853108f 100644 --- a/app/Http/Livewire/Project/Application/General.php +++ b/app/Http/Livewire/Project/Application/General.php @@ -5,7 +5,7 @@ namespace App\Http\Livewire\Project\Application; use App\Models\Application; use Illuminate\Support\Collection; use Illuminate\Support\Str; -use Livewire\Componwent; +use Livewire\Component; class General extends Component { From ce0f560c447892dd641bd0318448f6535fe180e4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 13 Nov 2023 11:09:21 +0100 Subject: [PATCH 06/12] 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') @endif @empty -
No service found.
+
No service found. Please try to reload the list!
@endforelse @endif