coolify/app/Models/Service.php

580 lines
27 KiB
PHP
Raw Normal View History

2023-09-20 15:42:41 +02:00
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
2023-09-26 15:07:33 +02:00
use Illuminate\Support\Facades\Cache;
2023-09-20 15:42:41 +02:00
use Symfony\Component\Yaml\Yaml;
use Illuminate\Support\Str;
class Service extends BaseModel
{
use HasFactory;
protected $guarded = [];
2023-09-22 11:23:49 +02:00
protected static function booted()
{
2023-10-05 08:46:26 +02:00
static::deleting(function ($service) {
$storagesToDelete = collect([]);
foreach ($service->applications()->get() as $application) {
$storages = $application->persistentStorages()->get();
foreach ($storages as $storage) {
$storagesToDelete->push($storage);
}
2023-09-22 11:23:49 +02:00
$application->persistentStorages()->delete();
}
foreach ($service->databases()->get() as $database) {
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
$storagesToDelete->push($storage);
}
2023-09-22 11:23:49 +02:00
$database->persistentStorages()->delete();
}
$service->environment_variables()->delete();
$service->applications()->delete();
$service->databases()->delete();
if ($storagesToDelete->count() > 0) {
$storagesToDelete->each(function ($storage) use ($service) {
instant_remote_process(["docker volume rm -f $storage->name"], $service->server, false);
});
}
instant_remote_process(["docker network rm {$service->uuid}"], $service->server, false);
2023-09-22 11:23:49 +02:00
});
2023-09-20 15:42:41 +02:00
}
2023-09-22 11:23:49 +02:00
public function type()
2023-09-20 15:42:41 +02:00
{
2023-09-22 11:23:49 +02:00
return 'service';
2023-09-20 15:42:41 +02:00
}
2023-09-22 11:23:49 +02:00
2023-09-26 15:07:33 +02:00
public function documentation()
{
$services = Cache::get('services', []);
$service = data_get($services, Str::of($this->name)->beforeLast('-')->value, []);
2023-09-28 11:01:00 +02:00
return data_get($service, 'documentation', config('constants.docs.base_url'));
2023-09-26 15:07:33 +02:00
}
2023-09-20 15:42:41 +02:00
public function applications()
{
return $this->hasMany(ServiceApplication::class);
}
public function databases()
{
return $this->hasMany(ServiceDatabase::class);
}
2023-10-02 15:51:06 +02:00
public function destination()
{
return $this->morphTo();
}
2023-09-21 17:48:31 +02:00
public function environment()
{
return $this->belongsTo(Environment::class);
}
2023-09-22 11:23:49 +02:00
public function server()
{
2023-09-21 17:48:31 +02:00
return $this->belongsTo(Server::class);
}
public function byName(string $name)
{
$app = $this->applications()->whereName($name)->first();
if ($app) {
return $app;
}
$db = $this->databases()->whereName($name)->first();
if ($db) {
return $db;
}
return null;
}
2023-09-20 15:42:41 +02:00
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->orderBy('key', 'asc');
}
2023-09-25 15:48:43 +02:00
public function workdir()
{
return service_configuration_dir() . "/{$this->uuid}";
}
public function saveComposeConfigs()
{
$workdir = $this->workdir();
$commands[] = "mkdir -p $workdir";
$commands[] = "cd $workdir";
$docker_compose_base64 = base64_encode($this->docker_compose);
$commands[] = "echo $docker_compose_base64 | base64 -d > docker-compose.yml";
$envs = $this->environment_variables()->get();
$commands[] = "rm -f .env || true";
foreach ($envs as $env) {
$commands[] = "echo '{$env->key}={$env->value}' >> .env";
}
2023-09-26 14:45:52 +02:00
if ($envs->count() === 0) {
$commands[] = "touch .env";
}
instant_remote_process($commands, $this->server);
}
2023-09-28 10:53:00 +02:00
2023-09-27 12:45:53 +02:00
public function parse(bool $isNew = false): Collection
2023-09-20 15:42:41 +02:00
{
ray()->clearAll();
2023-09-20 15:42:41 +02:00
if ($this->docker_compose_raw) {
2023-09-22 21:31:47 +02:00
try {
$yaml = Yaml::parse($this->docker_compose_raw);
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
2023-09-20 15:42:41 +02:00
2023-09-27 12:45:53 +02:00
$topLevelVolumes = collect(data_get($yaml, 'volumes', []));
$topLevelNetworks = collect(data_get($yaml, 'networks', []));
2023-09-21 17:48:31 +02:00
$dockerComposeVersion = data_get($yaml, 'version') ?? '3.8';
2023-09-20 15:42:41 +02:00
$services = data_get($yaml, 'services');
2023-09-27 15:48:19 +02:00
$generatedServiceFQDNS = collect([]);
2023-10-02 15:51:06 +02:00
if (is_null($this->destination)) {
$destination = $this->server->destinations()->first();
if ($destination) {
$this->destination()->associate($destination);
$this->save();
}
}
$definedNetwork = collect([$this->uuid]);
2023-09-27 15:48:19 +02:00
2023-10-04 18:06:25 +02:00
$services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS) {
2023-09-27 12:45:53 +02:00
$serviceVolumes = collect(data_get($service, 'volumes', []));
$servicePorts = collect(data_get($service, 'ports', []));
$serviceNetworks = collect(data_get($service, 'networks', []));
2023-09-22 12:08:51 +02:00
$serviceVariables = collect(data_get($service, 'environment', []));
2023-09-27 12:45:53 +02:00
$serviceLabels = collect(data_get($service, 'labels', []));
2023-09-22 12:08:51 +02:00
2023-09-27 12:45:53 +02:00
$containerName = "$serviceName-{$this->uuid}";
// Decide if the service is a database
$isDatabase = false;
$image = data_get_str($service, 'image');
if ($image->contains(':')) {
$image = Str::of($image);
2023-09-26 14:45:52 +02:00
} else {
2023-09-27 12:45:53 +02:00
$image = Str::of($image)->append(':latest');
2023-09-26 14:45:52 +02:00
}
2023-09-27 12:45:53 +02:00
$imageName = $image->before(':');
2023-09-26 14:45:52 +02:00
2023-09-27 12:45:53 +02:00
if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) {
$isDatabase = true;
2023-09-20 15:42:41 +02:00
}
2023-09-27 12:45:53 +02:00
data_set($service, 'is_database', $isDatabase);
// Create new serviceApplication or serviceDatabase
2023-09-25 15:48:43 +02:00
if ($isDatabase) {
2023-09-27 12:45:53 +02:00
if ($isNew) {
2023-09-20 15:42:41 +02:00
$savedService = ServiceDatabase::create([
'name' => $serviceName,
2023-09-26 14:45:52 +02:00
'image' => $image,
2023-09-20 15:42:41 +02:00
'service_id' => $this->id
]);
} else {
2023-09-27 12:45:53 +02:00
$savedService = ServiceDatabase::where([
'name' => $serviceName,
'service_id' => $this->id
])->first();
}
} else {
if ($isNew) {
2023-09-20 15:42:41 +02:00
$savedService = ServiceApplication::create([
'name' => $serviceName,
2023-09-26 14:45:52 +02:00
'image' => $image,
2023-09-20 15:42:41 +02:00
'service_id' => $this->id
]);
2023-09-21 17:48:31 +02:00
} else {
2023-09-27 12:45:53 +02:00
$savedService = ServiceApplication::where([
'name' => $serviceName,
'service_id' => $this->id
])->first();
2023-09-21 17:48:31 +02:00
}
2023-09-20 15:42:41 +02:00
}
2023-09-27 15:48:19 +02:00
if (is_null($savedService)) {
if ($isDatabase) {
$savedService = ServiceDatabase::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $this->id
]);
} else {
$savedService = ServiceApplication::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $this->id
]);
}
}
2023-09-25 17:51:04 +02:00
// Check if image changed
if ($savedService->image !== $image) {
$savedService->image = $image;
$savedService->save();
}
2023-09-27 12:45:53 +02:00
// Collect/create/update networks
if ($serviceNetworks->count() > 0) {
foreach ($serviceNetworks as $networkName => $networkDetails) {
$networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) {
return $value == $networkName || $key == $networkName;
});
if (!$networkExists) {
$topLevelNetworks->put($networkDetails, null);
}
}
}
2023-09-27 12:45:53 +02:00
// Collect/create/update ports
$collectedPorts = collect([]);
if ($servicePorts->count() > 0) {
foreach ($servicePorts as $sport) {
if (is_string($sport) || is_numeric($sport)) {
$collectedPorts->push($sport);
}
if (is_array($sport)) {
$target = data_get($sport, 'target');
$published = data_get($sport, 'published');
2023-09-27 12:45:53 +02:00
$protocol = data_get($sport, 'protocol');
$collectedPorts->push("$target:$published/$protocol");
2023-09-21 17:48:31 +02:00
}
}
2023-09-20 15:42:41 +02:00
}
$savedService->ports = $collectedPorts->implode(',');
$savedService->save();
2023-09-26 14:45:52 +02:00
2023-09-27 12:45:53 +02:00
// Add Coolify specific networks
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
return $value == $definedNetwork;
});
if (!$definedNetworkExists) {
2023-10-02 15:51:06 +02:00
foreach ($definedNetwork as $network) {
$topLevelNetworks->put($network, [
'name' => $network,
'external' => true
]);
}
2023-09-27 12:45:53 +02:00
}
$networks = $serviceNetworks->toArray();
2023-10-02 15:51:06 +02:00
foreach ($definedNetwork as $key => $network) {
$networks = array_merge($networks, [
2023-10-03 12:20:09 +02:00
$network
2023-10-02 15:51:06 +02:00
]);
}
2023-09-27 12:45:53 +02:00
data_set($service, 'networks', $networks);
// Collect/create/update volumes
2023-09-20 15:42:41 +02:00
if ($serviceVolumes->count() > 0) {
2023-10-04 18:06:25 +02:00
$serviceVolumes = $serviceVolumes->map(function ($volume) use ($savedService, $topLevelVolumes) {
2023-09-27 12:45:53 +02:00
$type = null;
$source = null;
$target = null;
2023-09-27 15:48:19 +02:00
$content = null;
2023-09-28 09:54:21 +02:00
$isDirectory = false;
2023-09-20 15:42:41 +02:00
if (is_string($volume)) {
2023-09-27 12:45:53 +02:00
$source = Str::of($volume)->before(':');
$target = Str::of($volume)->after(':')->beforeLast(':');
if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) {
$type = Str::of('bind');
} else {
$type = Str::of('volume');
}
2023-09-27 12:45:53 +02:00
} else if (is_array($volume)) {
$type = data_get_str($volume, 'type');
$source = data_get_str($volume, 'source');
$target = data_get_str($volume, 'target');
2023-09-27 15:48:19 +02:00
$content = data_get($volume, 'content');
2023-09-28 09:54:21 +02:00
$isDirectory = (bool) data_get($volume, 'isDirectory', false);
$foundConfig = $savedService->fileStorages()->whereMountPath($target)->first();
if ($foundConfig) {
2023-10-03 12:20:09 +02:00
$contentNotNull = data_get($foundConfig, 'content');
if ($contentNotNull) {
$content = $contentNotNull;
}
2023-09-28 09:54:21 +02:00
$isDirectory = (bool) data_get($foundConfig, 'is_directory');
}
2023-09-20 15:42:41 +02:00
}
2023-09-27 12:45:53 +02:00
if ($type->value() === 'bind') {
2023-09-27 15:48:19 +02:00
if ($source->value() === "/var/run/docker.sock") {
2023-10-01 20:46:49 +02:00
return $volume;
2023-09-27 15:48:19 +02:00
}
2023-09-28 09:54:21 +02:00
if ($source->value() === '/tmp' || $source->value() === '/tmp/') {
2023-10-01 20:46:49 +02:00
return $volume;
2023-09-27 15:48:19 +02:00
}
2023-09-27 12:45:53 +02:00
LocalFileVolume::updateOrCreate(
[
'mount_path' => $target,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
2023-09-27 12:45:53 +02:00
],
[
'fs_path' => $source,
'mount_path' => $target,
2023-09-27 15:48:19 +02:00
'content' => $content,
2023-09-28 09:54:21 +02:00
'is_directory' => $isDirectory,
2023-09-27 12:45:53 +02:00
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
]
);
} else if ($type->value() === 'volume') {
2023-10-03 13:14:11 +02:00
$slugWithoutUuid = Str::slug($source, '-');
$name = "{$savedService->service->uuid}_{$slugWithoutUuid}";
2023-10-01 20:46:49 +02:00
if (is_string($volume)) {
$source = Str::of($volume)->before(':');
$target = Str::of($volume)->after(':')->beforeLast(':');
$source = $name;
$volume = "$source:$target";
2023-10-02 15:51:06 +02:00
} else if (is_array($volume)) {
2023-10-01 20:46:49 +02:00
data_set($volume, 'source', $name);
}
2023-10-03 13:25:41 +02:00
$topLevelVolumes->put($name, [
'name' => $name,
]);
2023-09-27 12:45:53 +02:00
LocalPersistentVolume::updateOrCreate(
[
'mount_path' => $target,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
],
[
2023-10-03 13:25:41 +02:00
'name' => $name,
2023-09-27 12:45:53 +02:00
'mount_path' => $target,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
]
);
2023-09-20 15:42:41 +02:00
}
2023-10-03 12:20:09 +02:00
$savedService->getFilesFromServer(isInit: true);
2023-10-01 20:46:49 +02:00
return $volume;
});
data_set($service, 'volumes', $serviceVolumes->toArray());
2023-09-20 15:42:41 +02:00
}
2023-09-27 12:45:53 +02:00
// Add env_file with at least .env to the service
2023-09-27 15:48:19 +02:00
// $envFile = collect(data_get($service, 'env_file', []));
// if ($envFile->count() > 0) {
// if (!$envFile->contains('.env')) {
// $envFile->push('.env');
// }
// } else {
// $envFile = collect(['.env']);
// }
// data_set($service, 'env_file', $envFile->toArray());
2023-09-20 15:42:41 +02:00
2023-09-26 16:21:55 +02:00
2023-09-21 17:48:31 +02:00
// Get variables from the service
2023-09-27 12:45:53 +02:00
foreach ($serviceVariables as $variableName => $variable) {
if (is_numeric($variableName)) {
$variable = Str::of($variable);
if ($variable->contains('=')) {
// - SESSION_SECRET=123
// - SESSION_SECRET=
$key = $variable->before('=');
$value = $variable->after('=');
2023-09-20 15:42:41 +02:00
} else {
2023-09-27 12:45:53 +02:00
// - SESSION_SECRET
$key = $variable;
$value = null;
2023-09-20 15:42:41 +02:00
}
2023-09-27 12:45:53 +02:00
} else {
// SESSION_SECRET: 123
// SESSION_SECRET:
2023-09-27 15:48:19 +02:00
$key = Str::of($variableName);
2023-09-27 12:45:53 +02:00
$value = Str::of($variable);
}
2023-09-27 15:48:19 +02:00
if ($key->startsWith('SERVICE_FQDN')) {
if ($isNew || $savedService->fqdn === null) {
$name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower();
$fqdn = generateFqdn($this->server, "{$name->value()}-{$this->uuid}");
if (substr_count($key->value(), '_') === 3) {
// SERVICE_FQDN_UMAMI_1000
$port = $key->afterLast('_');
} else {
// SERVICE_FQDN_UMAMI
$port = null;
}
if ($port) {
$fqdn = "$fqdn:$port";
}
if (substr_count($key->value(), '_') >= 2) {
2023-10-04 11:32:27 +02:00
if (is_null($value)) {
$value = Str::of('/');
}
2023-09-27 15:48:19 +02:00
$path = $value->value();
if ($generatedServiceFQDNS->count() > 0) {
$alreadyGenerated = $generatedServiceFQDNS->has($key->value());
if ($alreadyGenerated) {
$fqdn = $generatedServiceFQDNS->get($key->value());
} else {
$generatedServiceFQDNS->put($key->value(), $fqdn);
}
} else {
$generatedServiceFQDNS->put($key->value(), $fqdn);
}
2023-09-30 15:39:40 +02:00
$fqdn = "$fqdn$path";
2023-09-27 15:48:19 +02:00
}
2023-09-27 15:48:19 +02:00
if (!$isDatabase) {
if ($savedService->fqdn) {
$fqdn = $savedService->fqdn . ',' . $fqdn;
} else {
$fqdn = $fqdn;
}
2023-09-27 15:48:19 +02:00
$savedService->fqdn = $fqdn;
$savedService->save();
}
}
2023-10-04 18:06:25 +02:00
// data_forget($service, "environment.$variableName");
// $yaml = data_forget($yaml, "services.$serviceName.environment.$variableName");
// if (count(data_get($yaml, 'services.' . $serviceName . '.environment')) === 0) {
// $yaml = data_forget($yaml, "services.$serviceName.environment");
// }
2023-09-27 15:48:19 +02:00
continue;
}
2023-09-27 12:45:53 +02:00
if ($value?->startsWith('$')) {
$value = Str::of(replaceVariables($value));
$key = $value;
$foundEnv = EnvironmentVariable::where([
'key' => $key,
'service_id' => $this->id,
])->first();
if ($value->startsWith('SERVICE_')) {
$command = $value->after('SERVICE_')->beforeLast('_');
$forService = $value->afterLast('_');
$generatedValue = null;
if ($command->value() === 'FQDN' || $command->value() === 'URL') {
2023-10-04 14:40:26 +02:00
if (Str::lower($forService) === $serviceName) {
$fqdn = generateFqdn($this->server, $containerName);
} else {
$fqdn = generateFqdn($this->server, Str::lower($forService) . '-' . $this->uuid);
}
2023-09-27 12:45:53 +02:00
if ($foundEnv) {
$fqdn = data_get($foundEnv, 'value');
} else {
2023-10-04 14:40:26 +02:00
if ($command->value() === 'URL') {
$fqdn = Str::of($fqdn)->after('://')->value();
}
2023-09-27 12:45:53 +02:00
EnvironmentVariable::create([
'key' => $key,
'value' => $fqdn,
2023-09-22 11:23:49 +02:00
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
2023-09-20 15:42:41 +02:00
}
2023-09-27 12:45:53 +02:00
if (!$isDatabase) {
2023-10-04 14:40:26 +02:00
if ($command->value() === 'FQDN') {
$savedService->fqdn = $fqdn;
$savedService->save();
}
2023-09-20 15:42:41 +02:00
}
} else {
2023-09-27 12:45:53 +02:00
switch ($command) {
case 'PASSWORD':
$generatedValue = Str::password(symbols: false);
break;
case 'PASSWORD_64':
$generatedValue = Str::password(length: 64, symbols: false);
break;
case 'BASE64_64':
$generatedValue = Str::random(64);
break;
case 'BASE64_128':
$generatedValue = Str::random(128);
break;
case 'BASE64':
$generatedValue = Str::random(32);
break;
case 'USER':
$generatedValue = Str::random(16);
break;
2023-09-26 16:21:55 +02:00
}
2023-09-27 12:45:53 +02:00
if (!$foundEnv) {
EnvironmentVariable::create([
'key' => $key,
'value' => $generatedValue,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
}
2023-09-21 17:48:31 +02:00
}
2023-09-27 12:45:53 +02:00
} else {
if ($value->contains(':-')) {
$key = $value->before(':');
$defaultValue = $value->after(':-');
} else if ($value->contains('-')) {
$key = $value->before('-');
$defaultValue = $value->after('-');
} else if ($value->contains(':?')) {
$key = $value->before(':');
$defaultValue = $value->after(':?');
} else if ($value->contains('?')) {
$key = $value->before('?');
$defaultValue = $value->after('?');
} else {
$key = $value;
$defaultValue = null;
2023-09-21 17:48:31 +02:00
}
2023-09-27 15:48:19 +02:00
if ($foundEnv) {
$defaultValue = data_get($foundEnv, 'value');
}
2023-09-27 12:45:53 +02:00
EnvironmentVariable::updateOrCreate([
'key' => $key,
'service_id' => $this->id,
], [
'value' => $defaultValue,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
2023-09-20 15:42:41 +02:00
}
}
}
2023-09-26 14:45:52 +02:00
2023-09-25 09:17:42 +02:00
// Add labels to the service
2023-09-27 15:48:19 +02:00
$fqdns = collect(data_get($savedService, 'fqdns'));
2023-09-27 12:45:53 +02:00
$defaultLabels = defaultLabels($this->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id);
$serviceLabels = $serviceLabels->merge($defaultLabels);
if (!$isDatabase && $fqdns->count() > 0) {
2023-09-25 09:17:42 +02:00
if ($fqdns) {
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true));
2023-09-21 17:48:31 +02:00
}
}
2023-09-27 12:45:53 +02:00
data_set($service, 'labels', $serviceLabels->toArray());
2023-09-20 15:42:41 +02:00
data_forget($service, 'is_database');
2023-09-21 17:48:31 +02:00
data_set($service, 'restart', RESTART_MODE);
2023-09-27 12:45:53 +02:00
data_set($service, 'container_name', $containerName);
data_forget($service, 'volumes.*.content');
2023-09-28 09:54:21 +02:00
data_forget($service, 'volumes.*.isDirectory');
2023-09-27 15:48:19 +02:00
// Remove unnecessary variables from service.environment
// $withoutServiceEnvs = collect([]);
// collect(data_get($service, 'environment'))->each(function ($value, $key) use ($withoutServiceEnvs) {
// ray($key, $value);
// if (!Str::of($key)->startsWith('$SERVICE_') && !Str::of($value)->startsWith('SERVICE_')) {
// $k = Str::of($value)->before("=");
// $v = Str::of($value)->after("=");
// $withoutServiceEnvs->put($k->value(), $v->value());
// }
// });
// ray($withoutServiceEnvs);
// data_set($service, 'environment', $withoutServiceEnvs->toArray());
2023-09-20 15:42:41 +02:00
return $service;
});
2023-09-21 17:48:31 +02:00
$finalServices = [
'version' => $dockerComposeVersion,
'services' => $services->toArray(),
2023-09-27 12:45:53 +02:00
'volumes' => $topLevelVolumes->toArray(),
'networks' => $topLevelNetworks->toArray(),
2023-09-21 17:48:31 +02:00
];
2023-10-04 18:06:25 +02:00
$this->docker_compose_raw = Yaml::dump($yaml, 10, 2);
$this->docker_compose = Yaml::dump($finalServices, 10, 2);
2023-09-21 17:48:31 +02:00
$this->save();
$this->saveComposeConfigs();
2023-09-27 12:45:53 +02:00
return collect([]);
2023-09-20 15:42:41 +02:00
} else {
return collect([]);
}
}
}