Add service-specific configuration fields and save

them to the database
This commit is contained in:
Andras Bacsai 2023-11-13 11:09:21 +01:00
parent 95baec99dd
commit ce0f560c44
12 changed files with 292 additions and 46 deletions

View File

@ -7,17 +7,34 @@
class StackForm extends Component class StackForm extends Component
{ {
public $service; public $service;
public $isConfigurationRequired = false; public $fields = [];
protected $listeners = ["saveCompose"]; protected $listeners = ["saveCompose"];
protected $rules = [ public $rules = [
'service.docker_compose_raw' => 'required', 'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required', 'service.docker_compose' => 'required',
'service.name' => 'required', 'service.name' => 'required',
'service.description' => 'nullable', 'service.description' => 'nullable',
]; ];
public function mount () { public $validationAttributes = [];
if ($this->service->applications->filter(fn($app) => str($app->image)->contains('minio/minio'))->count() > 0) { public function mount()
$this->isConfigurationRequired = true; {
$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) public function saveCompose($raw)
@ -32,6 +49,7 @@ public function submit()
try { try {
$this->validate(); $this->validate();
$this->service->save(); $this->service->save();
$this->service->saveExtraFields($this->fields);
$this->service->parse(); $this->service->parse();
$this->service->refresh(); $this->service->refresh();
$this->service->saveComposeConfigs(); $this->service->saveComposeConfigs();

View File

@ -45,7 +45,168 @@ public function type()
{ {
return 'service'; 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() public function documentation()
{ {
$services = getServiceTemplates(); $services = getServiceTemplates();
@ -257,7 +418,7 @@ public function parse(bool $isNew = false): Collection
} }
} }
$networks = collect(); $networks = collect();
foreach ($serviceNetworks as $key =>$serviceNetwork) { foreach ($serviceNetworks as $key => $serviceNetwork) {
if (gettype($serviceNetwork) === 'string') { if (gettype($serviceNetwork) === 'string') {
// networks: // networks:
// - appwrite // - appwrite
@ -268,7 +429,7 @@ public function parse(bool $isNew = false): Collection
// ipv4_address: 192.168.203.254 // ipv4_address: 192.168.203.254
// $networks->put($serviceNetwork, null); // $networks->put($serviceNetwork, null);
ray($key); ray($key);
$networks->put($key,$serviceNetwork); $networks->put($key, $serviceNetwork);
} }
} }
foreach ($definedNetwork as $key => $network) { foreach ($definedNetwork as $key => $network) {
@ -564,12 +725,18 @@ public function parse(bool $isNew = false): Collection
} }
// Add labels to the service // Add labels to the service
$fqdns = collect(data_get($savedService, 'fqdns')); if (!$isDatabase) {
$defaultLabels = defaultLabels($this->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id); if ($savedService->serviceType()) {
$serviceLabels = $serviceLabels->merge($defaultLabels); $fqdns = generateServiceSpecificFqdns($savedService, forTraefik: true);
if (!$isDatabase && $fqdns->count() > 0) { } else {
if ($fqdns) { $fqdns = collect(data_get($savedService, 'fqdns'));
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true)); }
$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()); data_set($service, 'labels', $serviceLabels->toArray());

View File

@ -22,6 +22,16 @@ public function type()
{ {
return 'service'; 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() public function service()
{ {
return $this->belongsTo(Service::class); return $this->belongsTo(Service::class);

View File

@ -29,7 +29,6 @@ public function databaseType()
return "standalone-$image"; return "standalone-$image";
} }
public function getServiceDatabaseUrl() { public function getServiceDatabaseUrl() {
// $type = $this->databaseType();
$port = $this->public_port; $port = $this->public_port;
$realIp = $this->service->server->ip; $realIp = $this->service->server->ip;
if ($realIp === 'host.docker.internal' || isDev()) { if ($realIp === 'host.docker.internal' || isDev()) {

View File

@ -16,22 +16,28 @@ public function __construct(public Service $service)
{ {
$this->links = collect([]); $this->links = collect([]);
$service->applications()->get()->map(function ($application) { $service->applications()->get()->map(function ($application) {
if ($application->fqdn) { $type = $application->serviceType();
$fqdns = collect(Str::of($application->fqdn)->explode(',')); if ($type) {
$fqdns->map(function ($fqdn) { $links = generateServiceSpecificFqdns($application, false);
$this->links->push(getFqdnWithoutPort($fqdn)); $this->links = $this->links->merge($links);
}); } else {
} if ($application->fqdn) {
if ($application->ports) { $fqdns = collect(Str::of($application->fqdn)->explode(','));
$portsCollection = collect(Str::of($application->ports)->explode(',')); $fqdns->map(function ($fqdn) {
$portsCollection->map(function ($port) { $this->links->push(getFqdnWithoutPort($fqdn));
if (Str::of($port)->contains(':')) { });
$hostPort = Str::of($port)->before(':'); }
} else { if ($application->ports) {
$hostPort = $port; $portsCollection = collect(Str::of($application->ports)->explode(','));
} $portsCollection->map(function ($port) {
$this->links->push(base_url(withPort:false) . ":{$hostPort}"); if (Str::of($port)->contains(':')) {
}); $hostPort = Str::of($port)->before(':');
} else {
$hostPort = $port;
}
$this->links->push(base_url(withPort: false) . ":{$hostPort}");
});
}
} }
}); });
} }

View File

@ -23,3 +23,6 @@
'influxdb', 'influxdb',
'clickhouse/clickhouse-server' 'clickhouse/clickhouse-server'
]; ];
const SPECIFIC_SERVICES = [
'quay.io/minio/minio',
];

View File

@ -144,6 +144,39 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
} }
return $labels; 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) function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled, $onlyPort = null)
{ {
$labels = collect([]); $labels = collect([]);

View File

@ -3,7 +3,7 @@
href="{{ route('project.service', $parameters) }}"> href="{{ route('project.service', $parameters) }}">
<button>Configuration</button> <button>Configuration</button>
</a> </a>
<x-services.links :service="$service" /> <x-services.links />
<div class="flex-1"></div> <div class="flex-1"></div>
@if (serviceStatus($service) === 'degraded') @if (serviceStatus($service) === 'degraded')
<button wire:click='deploy' onclick="startService.showModal()" <button wire:click='deploy' onclick="startService.showModal()"

View File

@ -5,7 +5,7 @@
<div class="fixed z-50 top-[4.5rem] left-4" id="vue"> <div class="fixed z-50 top-[4.5rem] left-4" id="vue">
<magic-bar></magic-bar> <magic-bar></magic-bar>
</div> </div>
<main class="main max-w-screen-2xl"> <main class="pb-10 main max-w-screen-2xl">
{{ $slot }} {{ $slot }}
</main> </main>
@endsection @endsection

View File

@ -16,19 +16,21 @@
<x-forms.input label="Description" id="application.description"></x-forms.input> <x-forms.input label="Description" id="application.description"></x-forms.input>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
@if ($application->required_fqdn) @if (!$application->serviceType()?->contains(str($application->image)->before(':')))
<x-forms.input required placeholder="https://app.coolify.io" label="Domains" @if ($application->required_fqdn)
id="application.fqdn" helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input> <x-forms.input required placeholder="https://app.coolify.io" label="Domains"
@else id="application.fqdn"
<x-forms.input placeholder="https://app.coolify.io" label="Domains" helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
id="application.fqdn" helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input> @else
<x-forms.input placeholder="https://app.coolify.io" label="Domains" id="application.fqdn"
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
@endif
@endif @endif
<x-forms.input required <x-forms.input required
helper="You can change the image you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>" helper="You can change the image you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
label="Image" id="application.image"></x-forms.input> label="Image" id="application.image"></x-forms.input>
</div> </div>
</div> </div>
<h3 class="pt-2">Advanced</h3> <h3 class="pt-2">Advanced</h3>
<div class="w-64"> <div class="w-64">
<x-forms.checkbox instantSave label="Exclude from service status" <x-forms.checkbox instantSave label="Exclude from service status"

View File

@ -28,7 +28,7 @@
<div class="w-full pl-8"> <div class="w-full pl-8">
<div x-cloak x-show="activeTab === 'service-stack'"> <div x-cloak x-show="activeTab === 'service-stack'">
<livewire:project.service.stack-form :service="$service" /> <livewire:project.service.stack-form :service="$service" />
<div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-3"> <div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-1">
@foreach ($applications as $application) @foreach ($applications as $application)
<div @class([ <div @class([
'border-l border-dashed border-red-500' => Str::of( 'border-l border-dashed border-red-500' => Str::of(
@ -58,7 +58,7 @@
@endif @endif
<div class="text-xs">{{ $application->status }}</div> <div class="text-xs">{{ $application->status }}</div>
</a> </a>
<a class="flex gap-2 p-1 mx-4 font-bold rounded group-hover:text-white hover:no-underline" <a class="flex items-center gap-2 p-1 mx-4 font-bold rounded group-hover:text-white hover:no-underline"
href="{{ route('project.service.logs', [...$parameters, 'service_name' => $application->name]) }}"><span href="{{ route('project.service.logs', [...$parameters, 'service_name' => $application->name]) }}"><span
class="hover:text-warning">Logs</span></a> class="hover:text-warning">Logs</span></a>
</div> </div>
@ -88,7 +88,7 @@ class="hover:text-warning">Logs</span></a>
@endif @endif
<div class="text-xs">{{ $database->status }}</div> <div class="text-xs">{{ $database->status }}</div>
</a> </a>
<a class="flex gap-2 p-1 mx-4 font-bold rounded hover:no-underline group-hover:text-white" <a class="flex items-center gap-2 p-1 mx-4 font-bold rounded hover:no-underline group-hover:text-white"
href="{{ route('project.service.logs', [...$parameters, 'service_name' => $database->name]) }}"><span href="{{ route('project.service.logs', [...$parameters, 'service_name' => $database->name]) }}"><span
class="hover:text-warning">Logs</span></a> class="hover:text-warning">Logs</span></a>
</div> </div>

View File

@ -12,9 +12,17 @@
<x-forms.input id="service.name" required label="Service Name" placeholder="My super wordpress site" /> <x-forms.input id="service.name" required label="Service Name" placeholder="My super wordpress site" />
<x-forms.input id="service.description" label="Description" /> <x-forms.input id="service.description" label="Description" />
</div> </div>
{{-- @if ($isConfigurationRequired) @if ($fields)
<div class="text-warning">This service requires additional confiugration. Please check our <a <div>
href="https://coolify.io/docs" class="text-white underline">documentation</a> for further information. <h3>Service Specific Configuration</h3>
</div> </div>
@endif --}} <div class="grid grid-cols-2 gap-2">
@foreach ($fields as $serviceName => $fields)
<x-forms.input type="{{ data_get($fields, 'isPassword') ? 'password' : 'text' }}" required
helper="Variable name: {{ $serviceName }}"
label="{{ data_get($fields, 'serviceName') }} {{ data_get($fields, 'name') }}"
id="fields.{{ $serviceName }}.value"></x-forms.input>
@endforeach
</div>
@endif
</form> </form>