From af1b479d73cf8d08049aa1cab1f990e13fc45a9e Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Fri, 23 Aug 2024 14:21:12 +0200 Subject: [PATCH] fix: parser ui: storage layout changed --- app/Jobs/ApplicationDeploymentJob.php | 26 +- app/Models/Application.php | 610 ++++++++- app/Models/Service.php | 595 ++++++++- bootstrap/helpers/docker.php | 81 +- bootstrap/helpers/proxy.php | 2 +- bootstrap/helpers/shared.php | 1134 +---------------- .../project/service/file-storage.blade.php | 18 +- .../project/service/storage.blade.php | 5 +- tests/Feature/DockerComposeParseTest.php | 30 +- 9 files changed, 1225 insertions(+), 1276 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 860a7d55c..f9fdeabe4 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -455,7 +455,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $yaml = $composeFile = $this->application->docker_compose_raw; $this->save_environment_variables(); } else { - $composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id, preview_id: data_get($this, 'preview.id')); + $composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id')); $this->save_environment_variables(); if (! is_null($this->env_filename)) { $services = collect($composeFile['services']); @@ -897,11 +897,13 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $url = str($this->preview->fqdn)->replace('http://', '')->replace('https://', ''); $envs->push("COOLIFY_URL={$url}"); } - if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) { - $envs->push("COOLIFY_BRANCH={$local_branch}"); - } - if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { - $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}"); + if ($this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') { + if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) { + $envs->push("COOLIFY_BRANCH={$local_branch}"); + } + if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { + $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}"); + } } foreach ($sorted_environment_variables_preview as $env) { $real_value = $env->real_value; @@ -943,11 +945,13 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $url = str($this->application->fqdn)->replace('http://', '')->replace('https://', ''); $envs->push("COOLIFY_URL={$url}"); } - if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { - $envs->push("COOLIFY_BRANCH={$local_branch}"); - } - if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { - $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}"); + if ($this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') { + if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { + $envs->push("COOLIFY_BRANCH={$local_branch}"); + } + if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { + $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}"); + } } foreach ($sorted_environment_variables as $env) { $real_value = $env->real_value; diff --git a/app/Models/Application.php b/app/Models/Application.php index f1621635b..60732a1ef 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -4,6 +4,7 @@ namespace App\Models; use App\Enums\ApplicationDeploymentStatus; use App\Enums\ProxyTypes; +use App\Jobs\ServerFilesFromServerJob; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; @@ -413,23 +414,6 @@ class Application extends BaseModel ); } - public function dockerComposePrLocation(): Attribute - { - return Attribute::make( - set: function ($value) { - if (is_null($value) || $value === '') { - return '/docker-compose.yaml'; - } else { - if ($value !== '/') { - return Str::start(Str::replaceEnd('/', '', $value), '/'); - } - - return Str::start($value, '/'); - } - } - ); - } - public function baseDirectory(): Attribute { return Attribute::make( @@ -1101,22 +1085,557 @@ class Application extends BaseModel instant_remote_process($commands, $this->destination->server, false); } + public function dockerComposeParser(int $pull_request_id = 0, ?int $preview_id = null) + { + $pullRequestId = $pull_request_id; + $isPullRequest = $pullRequestId == 0 ? false : true; + + $uuid = data_get($this, 'uuid'); + $server = data_get($this, 'destination.server'); + $compose = data_get($this, 'docker_compose_raw'); + try { + $yaml = Yaml::parse($compose); + } catch (\Exception $e) { + return; + } + $services = data_get($yaml, 'services', collect([])); + $topLevel = collect([ + 'volumes' => collect(data_get($compose, 'volumes', [])), + 'networks' => collect(data_get($compose, 'networks', [])), + 'configs' => collect(data_get($compose, 'configs', [])), + 'secrets' => collect(data_get($compose, 'secrets', [])), + ]); + + // If there are predefined volumes, make sure they are not null + if ($topLevel->get('volumes')->count() > 0) { + $temp = collect([]); + foreach ($topLevel['volumes'] as $volumeName => $volume) { + if (is_null($volume)) { + continue; + } + $temp->put($volumeName, $volume); + } + $topLevel['volumes'] = $temp; + } + // Get the base docker network + $baseNetwork = collect([$uuid]); + if ($isPullRequest) { + $baseNetwork = collect(["{$uuid}-{$pullRequestId}"]); + } + $parsedServices = collect([]); + $fileStorages = $this->fileStorages(); + // Let's loop through the services + foreach ($services as $serviceName => $service) { + $image = data_get_str($service, 'image'); + $restart = data_get_str($service, 'restart', RESTART_MODE); + $logging = data_get($service, 'logging'); + + if ($server->isLogDrainEnabled() && $this->isLogDrainEnabled()) { + $logging = [ + 'driver' => 'fluentd', + 'options' => [ + 'fluentd-address' => 'tcp://127.0.0.1:24224', + 'fluentd-async' => 'true', + 'fluentd-sub-second-precision' => 'true', + ], + ]; + } + + $volumes = collect(data_get($service, 'volumes', [])); + $networks = collect(data_get($service, 'networks', [])); + $depends_on = collect(data_get($service, 'depends_on', [])); + $labels = collect(data_get($service, 'labels', [])); + $environment = collect(data_get($service, 'environment', [])); + $buildArgs = collect(data_get($service, 'build.args', [])); + $environment = $environment->merge($buildArgs); + + $baseName = generateApplicationContainerName( + application: $this, + pull_request_id: $pullRequestId + ); + $containerName = "$serviceName-$baseName"; + $volumesParsed = collect([]); + + if ($volumes->count() > 0) { + foreach ($volumes as $index => $volume) { + $type = null; + $source = null; + $target = null; + $content = null; + $isDirectory = false; + if (is_string($volume)) { + $source = str($volume)->before(':'); + $target = str($volume)->after(':')->beforeLast(':'); + $foundConfig = $fileStorages->whereMountPath($target)->first(); + if (sourceIsLocal($source)) { + $type = str('bind'); + if ($foundConfig) { + $contentNotNull_temp = data_get($foundConfig, 'content'); + if ($contentNotNull_temp) { + $content = $contentNotNull_temp; + } + $isDirectory = data_get($foundConfig, 'is_directory'); + } else { + // By default, we cannot determine if the bind is a directory or not, so we set it to directory + $isDirectory = true; + } + } else { + $type = str('volume'); + } + } elseif (is_array($volume)) { + $type = data_get_str($volume, 'type'); + $source = data_get_str($volume, 'source'); + $target = data_get_str($volume, 'target'); + $content = data_get($volume, 'content'); + $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null); + + $foundConfig = $fileStorages->whereMountPath($target)->first(); + if ($foundConfig) { + $contentNotNull_temp = data_get($foundConfig, 'content'); + if ($contentNotNull_temp) { + $content = $contentNotNull_temp; + } + $isDirectory = data_get($foundConfig, 'is_directory'); + } else { + // if isDirectory is not set (or false) & content is also not set, we assume it is a directory + if ((is_null($isDirectory) || ! $isDirectory) && is_null($content)) { + $isDirectory = true; + } + } + } + if ($type->value() === 'bind') { + if ($source->value() === '/var/run/docker.sock') { + return $volume; + } + if ($source->value() === '/tmp' || $source->value() === '/tmp/') { + return $volume; + } + $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); + $source = replaceLocalSource($source, $mainDirectory); + if ($isPullRequest) { + $source = $source."-pr-$pullRequestId"; + } + if ( + ! $this?->settings?->is_preserve_repository_enabled || $foundConfig?->is_based_on_git + ) { + LocalFileVolume::updateOrCreate( + [ + 'mount_path' => $target, + 'resource_id' => $this->id, + 'resource_type' => get_class($this), + ], + [ + 'fs_path' => $source, + 'mount_path' => $target, + 'content' => $content, + 'is_directory' => $isDirectory, + 'resource_id' => $this->id, + 'resource_type' => get_class($this), + ] + ); + } + $volume = "$source:$target"; + } elseif ($type->value() === 'volume') { + if ($topLevel->get('volumes')->has($source->value())) { + $temp = $topLevel->get('volumes')->get($source->value()); + if (data_get($temp, 'driver_opts.type') === 'cifs') { + return $volume; + } + if (data_get($temp, 'driver_opts.type') === 'nfs') { + return $volume; + } + } + $slugWithoutUuid = Str::slug($source, '-'); + $name = "{$uuid}_{$slugWithoutUuid}"; + if ($isPullRequest) { + $name = "{$name}-pr-$pullRequestId"; + } + if (is_string($volume)) { + $source = str($volume)->before(':'); + $target = str($volume)->after(':')->beforeLast(':'); + $source = $name; + $volume = "$source:$target"; + } elseif (is_array($volume)) { + data_set($volume, 'source', $name); + } + $topLevel->get('volumes')->put($name, [ + 'name' => $name, + ]); + + LocalPersistentVolume::updateOrCreate( + [ + 'mount_path' => $target, + 'resource_id' => $this->id, + 'resource_type' => get_class($this), + ], + [ + 'name' => $name, + 'mount_path' => $target, + 'resource_id' => $this->id, + 'resource_type' => get_class($this), + ] + ); + } + dispatch(new ServerFilesFromServerJob($this)); + $volumesParsed->put($index, $volume); + } + } + + if ($depends_on?->count() > 0) { + if ($isPullRequest) { + $newDependsOn = collect([]); + $depends_on->each(function ($dependency, $condition) use ($pullRequestId, $newDependsOn) { + if (is_numeric($condition)) { + $dependency = "$dependency-pr-$pullRequestId"; + + $newDependsOn->put($condition, $dependency); + } else { + $condition = "$condition-pr-$pullRequestId"; + $newDependsOn->put($condition, $dependency); + } + }); + $depends_on = $newDependsOn; + } + + } + if ($topLevel->get('networks')?->count() > 0) { + foreach ($topLevel->get('networks') as $networkName => $network) { + if ($networkName === 'default') { + continue; + } + // ignore aliases + if ($network['aliases'] ?? false) { + continue; + } + $networkExists = $networks->contains(function ($value, $key) use ($networkName) { + return $value == $networkName || $key == $networkName; + }); + if (! $networkExists) { + $networks->put($networkName, null); + } + } + } + $baseNetworkExists = $networks->contains(function ($value, $_) use ($baseNetwork) { + return $value == $baseNetwork; + }); + if (! $baseNetworkExists) { + foreach ($baseNetwork as $network) { + $topLevel->get('networks')->put($network, [ + 'name' => $network, + 'external' => true, + ]); + } + } + $networks_temp = collect(); + + foreach ($networks as $key => $network) { + if (gettype($network) === 'string') { + // networks: + // - appwrite + $networks_temp->put($network, null); + } elseif (gettype($network) === 'array') { + // networks: + // default: + // ipv4_address: 192.168.203.254 + $networks_temp->put($key, $network); + } + } + foreach ($baseNetwork as $key => $network) { + $networks_temp->put($network, null); + } + + if (data_get($this, 'settings.connect_to_docker_network')) { + $network = $this->destination->network; + $networks_temp->put($network, null); + $topLevel->get('networks')->put($network, [ + 'name' => $network, + 'external' => true, + ]); + } + + foreach ($environment as $key => $value) { + if (is_numeric($key)) { + if (is_array($value)) { + // - SESSION_SECRET: 123 + // - SESSION_SECRET: + $key = str(collect($value)->keys()->first()); + $value = str(collect($value)->values()->first()); + } else { + $variable = str($value); + if ($variable->contains('=')) { + // - SESSION_SECRET=123 + // - SESSION_SECRET= + $key = $variable->before('='); + $value = $variable->after('='); + } else { + // - SESSION_SECRET + $key = $variable; + $value = null; + } + } + } else { + // SESSION_SECRET: 123 + // SESSION_SECRET: + $key = str($key); + $value = str($value); + } + + // Auto generate FQDN and URL + if ($key->startsWith('SERVICE_FQDN') || $value->startsWith('$SERVICE_FQDN') || $value->startsWith('${SERVICE_FQDN')) { + if ($value->contains('SERVICE_FQDN')) { + $key = str(replaceVariables($value)); + $value = null; + } + $name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower(); + $fqdn = generateFqdn($server, "{$name->value()}-{$uuid}"); + + $url = str($fqdn)->replace('http://', '')->replace('https://', '')->replace('www.', ''); + $keyUrl = $key->replace('SERVICE_FQDN', 'SERVICE_URL'); + // TODO: is this needed? + // 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 ($value && get_class($value) === 'Illuminate\Support\Stringable') { + $path = $value->value(); + $fqdn = "$fqdn$path"; + } + + // ray([ + // 'key' => $key, + // 'value' => $fqdn, + // ]); + // ray($this->environment_variables()->where('key', $key)->where('application_id', $this->id)->first()); + $this->environment_variables()->where('key', $key)->where('application_id', $this->id)->firstOrCreate([ + 'key' => $key, + 'application_id' => $this->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + $this->environment_variables()->where('key', $keyUrl)->where('application_id', $this->id)->firstOrCreate([ + 'key' => $keyUrl, + 'application_id' => $this->id, + ], [ + 'value' => $url, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } elseif ($value->startsWith('$')) { + // If the value is a variable then we will add it to Coolify's DB + + // ${VARIABLE} will be VARIABLE instead + $value = str(replaceVariables($value)); + if ($value->contains(':-')) { + $key = $value->before(':'); + $defaultValue = $value->after(':-'); + } elseif ($value->contains('-')) { + $key = $value->before('-'); + $defaultValue = $value->after('-'); + } elseif ($value->contains(':?')) { + $key = $value->before(':'); + $defaultValue = $value->after(':?'); + } elseif ($value->contains('?')) { + $key = $value->before('?'); + $defaultValue = $value->after('?'); + } else { + $key = $value; + $defaultValue = null; + } + $this->environment_variables()->where('key', $key)->where('application_id', $this->id)->firstOrCreate([ + 'key' => $key, + 'application_id' => $this->id, + 'is_preview' => false, + ], [ + 'value' => $defaultValue, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + } + $branch = $this->git_branch; + if ($pullRequestId !== 0) { + $branch = "pull/{$pullRequestId}/head"; + } + if ($this->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { + $environment->put('COOLIFY_BRANCH', $branch); + } + + if ($this->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { + $environment->put('COOLIFY_CONTAINER_NAME', $containerName); + } + // Remove SERVICE_FQDN and SERVICE_URL from environment + $environment = $environment->filter(function ($value, $key) { + return ! str($key)->startsWith('SERVICE_FQDN') && ! str($key)->startsWith('SERVICE_URL'); + }); + ray($environment); + + // Labels + $fqdns = collect([]); + + $domains = collect(json_decode($this->docker_compose_domains)) ?? collect([]); + if ($domains->count() !== 0) { + $fqdns = data_get($domains, "$serviceName.domain"); + if (! $fqdns) { + $fqdns = collect([]); + } else { + $fqdns = str($fqdns)->explode(','); + if ($isPullRequest) { + $preview = $this->previews()->find($preview_id); + $docker_compose_domains = collect(json_decode(data_get($preview, 'docker_compose_domains'))); + if ($docker_compose_domains->count() > 0) { + $found_fqdn = data_get($docker_compose_domains, "$serviceName.domain"); + if ($found_fqdn) { + $fqdns = collect($found_fqdn); + } else { + $fqdns = collect([]); + } + } else { + $fqdns = $fqdns->map(function ($fqdn) use ($pullRequestId) { + $preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->id, $pullRequestId); + $url = Url::fromString($fqdn); + $template = $this->preview_url_template; + $host = $url->getHost(); + $schema = $url->getScheme(); + $random = new Cuid2; + $preview_fqdn = str_replace('{{random}}', $random, $template); + $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); + $preview_fqdn = str_replace('{{pr_id}}', $pullRequestId, $preview_fqdn); + $preview_fqdn = "$schema://$preview_fqdn"; + $preview->fqdn = $preview_fqdn; + $preview->save(); + + return $preview_fqdn; + }); + } + } + $shouldGenerateLabelsExactly = $server->settings->generate_exact_labels; + if ($shouldGenerateLabelsExactly) { + switch ($server->proxyType()) { + case ProxyTypes::TRAEFIK->value: + $labels = $labels->merge( + fqdnLabelsForTraefik( + uuid: $this->uuid, + domains: $fqdns, + serviceLabels: $labels, + generate_unique_uuid: $this->build_pack === 'dockercompose', + image: $image, + is_force_https_enabled: $this->isForceHttpsEnabled(), + is_gzip_enabled: $this->isGzipEnabled(), + is_stripprefix_enabled: $this->isStripprefixEnabled(), + ) + ); + break; + case ProxyTypes::CADDY->value: + $labels = $labels->merge( + fqdnLabelsForCaddy( + network: $this->destination->network, + uuid: $this->uuid, + domains: $fqdns, + serviceLabels: $labels, + image: $image, + is_force_https_enabled: $this->isForceHttpsEnabled(), + is_gzip_enabled: $this->isGzipEnabled(), + is_stripprefix_enabled: $this->isStripprefixEnabled(), + ) + ); + break; + } + } else { + $labels = $labels->merge( + fqdnLabelsForTraefik( + uuid: $this->uuid, + domains: $fqdns, + serviceLabels: $labels, + generate_unique_uuid: $this->build_pack === 'dockercompose', + image: $image, + is_force_https_enabled: $this->isForceHttpsEnabled(), + is_gzip_enabled: $this->isGzipEnabled(), + is_stripprefix_enabled: $this->isStripprefixEnabled(), + ) + ); + $labels = $labels->merge( + fqdnLabelsForCaddy( + network: $this->destination->network, + uuid: $this->uuid, + domains: $fqdns, + serviceLabels: $labels, + image: $image, + is_force_https_enabled: $this->isForceHttpsEnabled(), + is_gzip_enabled: $this->isGzipEnabled(), + is_stripprefix_enabled: $this->isStripprefixEnabled(), + ) + ); + } + } + } + + $defaultLabels = defaultLabels( + id: $this->id, + name: $containerName, + pull_request_id: $pullRequestId, + type: 'application'); + $labels = $labels->merge($defaultLabels); + + if ($labels->count() > 0 && $this->settings->is_container_label_escape_enabled) { + $labels = $labels->map(function ($value, $key) { + return escapeDollarSign($value); + }); + } + $payload = collect($service)->merge([ + 'restart' => $restart->value(), + 'container_name' => $containerName, + 'volumes' => $volumesParsed, + 'networks' => $networks_temp, + 'labels' => $labels, + 'environment' => $environment, + ]); + + if ($logging) { + $payload['logging'] = $logging; + } + if ($depends_on->count() > 0) { + $payload['depends_on'] = $depends_on; + } + if ($isPullRequest) { + $serviceName = "{$serviceName}-pr-{$pullRequestId}"; + } + $parsedServices->put($serviceName, $payload); + + } + + $topLevel->put('services', $parsedServices); + $customOrder = ['services', 'volumes', 'networks', 'configs', 'secrets']; + + $topLevel = $topLevel->sortBy(function ($value, $key) use ($customOrder) { + return array_search($key, $customOrder); + }); + $this->docker_compose = Yaml::dump(convertToArray($topLevel), 10, 2); + data_forget($this, 'environment_variables'); + data_forget($this, 'environment_variables_preview'); + $this->save(); + + return $topLevel; + } + public function parseCompose(int $pull_request_id = 0, ?int $preview_id = null) { if (! $this->docker_compose_raw) { return collect([]); } if ($this->compose_parsing_version === '3') { - $compose = dockerComposeParserForApplications($this); + $compose = $this->dockerComposeParser($pull_request_id, $preview_id); return $compose; } - $isNew = false; - $isSameDockerComposeFile = false; - if ($this->dockerComposePrLocation() === $this->dockerComposeLocation()) { - $isSameDockerComposeFile = true; - } try { $yaml = Yaml::parse($this->docker_compose_raw); } catch (\Exception $e) { @@ -1157,7 +1676,7 @@ class Application extends BaseModel if ($pull_request_id !== 0) { $definedNetwork = collect(["{$this->uuid}-$pull_request_id"]); } - $services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $server, $pull_request_id, $preview_id) { + $services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $server, $pull_request_id, $preview_id) { $serviceVolumes = collect(data_get($service, 'volumes', [])); $servicePorts = collect(data_get($service, 'ports', [])); $serviceNetworks = collect(data_get($service, 'networks', [])); @@ -1434,39 +1953,6 @@ class Application extends BaseModel $value = str($variable); } if ($key->startsWith('SERVICE_FQDN')) { - if ($isNew) { - $name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower(); - $fqdn = generateFqdn($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) { - if ($value) { - $path = $value->value(); - } else { - $path = null; - } - 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); - } - $fqdn = "$fqdn$path"; - } - } - continue; } if ($value?->startsWith('$')) { @@ -1708,13 +2194,9 @@ class Application extends BaseModel 'configs' => $topLevelConfigs->toArray(), 'secrets' => $topLevelSecrets->toArray(), ]; - if ($isSameDockerComposeFile) { - $this->docker_compose_raw = Yaml::dump($yaml, 10, 2); - $this->docker_compose = Yaml::dump($finalServices, 10, 2); - } else { - $this->docker_compose_raw = Yaml::dump($yaml, 10, 2); - $this->docker_compose = Yaml::dump($finalServices, 10, 2); - } + + $this->docker_compose_raw = Yaml::dump($yaml, 10, 2); + $this->docker_compose = Yaml::dump($finalServices, 10, 2); data_forget($this, 'environment_variables'); data_forget($this, 'environment_variables_preview'); $this->save(); diff --git a/app/Models/Service.php b/app/Models/Service.php index 6c9068334..eeee2e971 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -2,11 +2,13 @@ namespace App\Models; +use App\Enums\ProxyTypes; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Collection; +use Illuminate\Support\Str; use OpenApi\Attributes as OA; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; @@ -983,7 +985,598 @@ class Service extends BaseModel public function parse(bool $isNew = false): Collection { - return parseDockerComposeFile($this, $isNew); + if (! $this->docker_compose_raw) { + return collect([]); + } + + try { + $yaml = Yaml::parse($this->docker_compose_raw); + } catch (\Exception $e) { + throw new \Exception($e->getMessage()); + } + $allServices = get_service_templates(); + $topLevelVolumes = collect(data_get($yaml, 'volumes', [])); + $topLevelNetworks = collect(data_get($yaml, 'networks', [])); + $topLevelConfigs = collect(data_get($yaml, 'configs', [])); + $topLevelSecrets = collect(data_get($yaml, 'secrets', [])); + $services = data_get($yaml, 'services'); + + $generatedServiceFQDNS = collect([]); + if (is_null($this->destination)) { + $destination = $this->server->destinations()->first(); + if ($destination) { + $this->destination()->associate($destination); + $this->save(); + } + } + $definedNetwork = collect([$this->uuid]); + if ($topLevelVolumes->count() > 0) { + $tempTopLevelVolumes = collect([]); + foreach ($topLevelVolumes as $volumeName => $volume) { + if (is_null($volume)) { + continue; + } + $tempTopLevelVolumes->put($volumeName, $volume); + } + $topLevelVolumes = collect($tempTopLevelVolumes); + } + $services = collect($services)->map(function ($service, $serviceName) use ($topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $allServices, $topLevelVolumes) { + // Workarounds for beta users. + if ($serviceName === 'registry') { + $tempServiceName = 'docker-registry'; + } else { + $tempServiceName = $serviceName; + } + if (str(data_get($service, 'image'))->contains('glitchtip')) { + $tempServiceName = 'glitchtip'; + } + if ($serviceName === 'supabase-kong') { + $tempServiceName = 'supabase'; + } + $serviceDefinition = data_get($allServices, $tempServiceName); + $predefinedPort = data_get($serviceDefinition, 'port'); + if ($serviceName === 'plausible') { + $predefinedPort = '8000'; + } + // End of workarounds for beta users. + $serviceVolumes = collect(data_get($service, 'volumes', [])); + $servicePorts = collect(data_get($service, 'ports', [])); + $serviceNetworks = collect(data_get($service, 'networks', [])); + $serviceVariables = collect(data_get($service, 'environment', [])); + $serviceLabels = collect(data_get($service, 'labels', [])); + $hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false; + if ($serviceLabels->count() > 0) { + $removedLabels = collect([]); + $serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) { + if (! str($serviceLabel)->contains('=')) { + $removedLabels->put($serviceLabelName, $serviceLabel); + + return false; + } + + return $serviceLabel; + }); + foreach ($removedLabels as $removedLabelName => $removedLabel) { + $serviceLabels->push("$removedLabelName=$removedLabel"); + } + } + + $containerName = "$serviceName-{$this->uuid}"; + + // Decide if the service is a database + $isDatabase = isDatabaseImage(data_get_str($service, 'image')); + $image = data_get_str($service, 'image'); + data_set($service, 'is_database', $isDatabase); + + // Create new serviceApplication or serviceDatabase + if ($isDatabase) { + if ($isNew) { + $savedService = ServiceDatabase::create([ + 'name' => $serviceName, + 'image' => $image, + 'service_id' => $this->id, + ]); + } else { + $savedService = ServiceDatabase::where([ + 'name' => $serviceName, + 'service_id' => $this->id, + ])->first(); + } + } else { + if ($isNew) { + $savedService = ServiceApplication::create([ + 'name' => $serviceName, + 'image' => $image, + 'service_id' => $this->id, + ]); + } else { + $savedService = ServiceApplication::where([ + 'name' => $serviceName, + 'service_id' => $this->id, + ])->first(); + } + } + 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, + ]); + } + } + + // Check if image changed + if ($savedService->image !== $image) { + $savedService->image = $image; + $savedService->save(); + } + // Collect/create/update networks + if ($serviceNetworks->count() > 0) { + foreach ($serviceNetworks as $networkName => $networkDetails) { + if ($networkName === 'default') { + continue; + } + // ignore alias + if ($networkDetails['aliases'] ?? false) { + continue; + } + $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { + return $value == $networkName || $key == $networkName; + }); + if (! $networkExists) { + $topLevelNetworks->put($networkDetails, null); + } + } + } + + // 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'); + $protocol = data_get($sport, 'protocol'); + $collectedPorts->push("$target:$published/$protocol"); + } + } + } + $savedService->ports = $collectedPorts->implode(','); + $savedService->save(); + + if (! $hasHostNetworkMode) { + // Add Coolify specific networks + $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { + return $value == $definedNetwork; + }); + if (! $definedNetworkExists) { + foreach ($definedNetwork as $network) { + $topLevelNetworks->put($network, [ + 'name' => $network, + 'external' => true, + ]); + } + } + $networks = collect(); + foreach ($serviceNetworks as $key => $serviceNetwork) { + if (gettype($serviceNetwork) === 'string') { + // networks: + // - appwrite + $networks->put($serviceNetwork, null); + } elseif (gettype($serviceNetwork) === 'array') { + // networks: + // default: + // ipv4_address: 192.168.203.254 + // $networks->put($serviceNetwork, null); + $networks->put($key, $serviceNetwork); + } + } + foreach ($definedNetwork as $key => $network) { + $networks->put($network, null); + } + data_set($service, 'networks', $networks->toArray()); + } + + // Collect/create/update volumes + if ($serviceVolumes->count() > 0) { + ['serviceVolumes' => $serviceVolumes, 'topLevelVolumes' => $topLevelVolumes] = parseServiceVolumes($serviceVolumes, $savedService, $topLevelVolumes); + data_set($service, 'volumes', $serviceVolumes->toArray()); + } + + // Get variables from the service + foreach ($serviceVariables as $variableName => $variable) { + if (is_numeric($variableName)) { + if (is_array($variable)) { + // - SESSION_SECRET: 123 + // - SESSION_SECRET: + $key = str(collect($variable)->keys()->first()); + $value = str(collect($variable)->values()->first()); + } else { + $variable = str($variable); + if ($variable->contains('=')) { + // - SESSION_SECRET=123 + // - SESSION_SECRET= + $key = $variable->before('='); + $value = $variable->after('='); + } else { + // - SESSION_SECRET + $key = $variable; + $value = null; + } + } + } else { + // SESSION_SECRET: 123 + // SESSION_SECRET: + $key = str($variableName); + $value = str($variable); + } + 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 { + $last = $key->afterLast('_'); + if (is_numeric($last->value())) { + // SERVICE_FQDN_3001 + $port = $last; + } else { + // SERVICE_FQDN_UMAMI + $port = null; + } + } + if ($port) { + $fqdn = "$fqdn:$port"; + } + if (substr_count($key->value(), '_') >= 2) { + if ($value) { + $path = $value->value(); + } else { + $path = null; + } + 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); + } + $fqdn = "$fqdn$path"; + } + + if (! $isDatabase) { + if ($savedService->fqdn) { + data_set($savedService, 'fqdn', $savedService->fqdn.','.$fqdn); + } else { + data_set($savedService, 'fqdn', $fqdn); + } + $savedService->save(); + } + EnvironmentVariable::create([ + 'key' => $key, + 'value' => $fqdn, + 'is_build_time' => false, + 'service_id' => $this->id, + 'is_preview' => false, + ]); + } + // Caddy needs exact port in some cases. + if ($predefinedPort && ! $key->endsWith("_{$predefinedPort}")) { + $fqdns_exploded = str($savedService->fqdn)->explode(','); + if ($fqdns_exploded->count() > 1) { + continue; + } + $env = EnvironmentVariable::where([ + 'key' => $key, + 'service_id' => $this->id, + ])->first(); + if ($env) { + $env_url = Url::fromString($savedService->fqdn); + $env_port = $env_url->getPort(); + if ($env_port !== $predefinedPort) { + $env_url = $env_url->withPort($predefinedPort); + $savedService->fqdn = $env_url->__toString(); + $savedService->save(); + } + } + } + + // 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"); + // } + continue; + } + if ($value?->startsWith('$')) { + $foundEnv = EnvironmentVariable::where([ + 'key' => $key, + 'service_id' => $this->id, + ])->first(); + $value = str(replaceVariables($value)); + $key = $value; + if ($value->startsWith('SERVICE_')) { + $foundEnv = EnvironmentVariable::where([ + 'key' => $key, + 'service_id' => $this->id, + ])->first(); + ['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value); + if (! is_null($command)) { + if ($command?->value() === 'FQDN' || $command?->value() === 'URL') { + if (Str::lower($forService) === $serviceName) { + $fqdn = generateFqdn($this->server, $containerName); + } else { + $fqdn = generateFqdn($this->server, Str::lower($forService).'-'.$this->uuid); + } + if ($port) { + $fqdn = "$fqdn:$port"; + } + if ($foundEnv) { + $fqdn = data_get($foundEnv, 'value'); + // if ($savedService->fqdn) { + // $savedServiceFqdn = Url::fromString($savedService->fqdn); + // $parsedFqdn = Url::fromString($fqdn); + // $savedServicePath = $savedServiceFqdn->getPath(); + // $parsedFqdnPath = $parsedFqdn->getPath(); + // if ($savedServicePath != $parsedFqdnPath) { + // $fqdn = $parsedFqdn->withPath($savedServicePath)->__toString(); + // $foundEnv->value = $fqdn; + // $foundEnv->save(); + // } + // } + } else { + if ($command->value() === 'URL') { + $fqdn = str($fqdn)->after('://')->value(); + } + EnvironmentVariable::create([ + 'key' => $key, + 'value' => $fqdn, + 'is_build_time' => false, + 'service_id' => $this->id, + 'is_preview' => false, + ]); + } + if (! $isDatabase) { + if ($command->value() === 'FQDN' && is_null($savedService->fqdn) && ! $foundEnv) { + $savedService->fqdn = $fqdn; + $savedService->save(); + } + // Caddy needs exact port in some cases. + if ($predefinedPort && ! $key->endsWith("_{$predefinedPort}") && $command?->value() === 'FQDN' && $this->server->proxyType() === 'CADDY') { + $fqdns_exploded = str($savedService->fqdn)->explode(','); + if ($fqdns_exploded->count() > 1) { + continue; + } + $env = EnvironmentVariable::where([ + 'key' => $key, + 'service_id' => $this->id, + ])->first(); + if ($env) { + $env_url = Url::fromString($env->value); + $env_port = $env_url->getPort(); + if ($env_port !== $predefinedPort) { + $env_url = $env_url->withPort($predefinedPort); + $savedService->fqdn = $env_url->__toString(); + $savedService->save(); + } + } + } + } + } else { + $generatedValue = generateEnvValue($command, $this); + if (! $foundEnv) { + EnvironmentVariable::create([ + 'key' => $key, + 'value' => $generatedValue, + 'is_build_time' => false, + 'service_id' => $this->id, + 'is_preview' => false, + ]); + } + } + } + } else { + if ($value->contains(':-')) { + $key = $value->before(':'); + $defaultValue = $value->after(':-'); + } elseif ($value->contains('-')) { + $key = $value->before('-'); + $defaultValue = $value->after('-'); + } elseif ($value->contains(':?')) { + $key = $value->before(':'); + $defaultValue = $value->after(':?'); + } elseif ($value->contains('?')) { + $key = $value->before('?'); + $defaultValue = $value->after('?'); + } else { + $key = $value; + $defaultValue = null; + } + $foundEnv = EnvironmentVariable::where([ + 'key' => $key, + 'service_id' => $this->id, + ])->first(); + if ($foundEnv) { + $defaultValue = data_get($foundEnv, 'value'); + } + EnvironmentVariable::updateOrCreate([ + 'key' => $key, + 'service_id' => $this->id, + ], [ + 'value' => $defaultValue, + 'is_build_time' => false, + 'service_id' => $this->id, + 'is_preview' => false, + ]); + } + } + } + // Add labels to the service + if ($savedService->serviceType()) { + $fqdns = generateServiceSpecificFqdns($savedService); + } else { + $fqdns = collect(data_get($savedService, 'fqdns'))->filter(); + } + $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) { + $shouldGenerateLabelsExactly = $this->server->settings->generate_exact_labels; + if ($shouldGenerateLabelsExactly) { + switch ($this->server->proxyType()) { + case ProxyTypes::TRAEFIK->value: + $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( + uuid: $this->uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $savedService->isGzipEnabled(), + is_stripprefix_enabled: $savedService->isStripprefixEnabled(), + service_name: $serviceName, + image: data_get($service, 'image') + )); + break; + case ProxyTypes::CADDY->value: + $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( + network: $this->destination->network, + uuid: $this->uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $savedService->isGzipEnabled(), + is_stripprefix_enabled: $savedService->isStripprefixEnabled(), + service_name: $serviceName, + image: data_get($service, 'image') + )); + break; + } + } else { + $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( + uuid: $this->uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $savedService->isGzipEnabled(), + is_stripprefix_enabled: $savedService->isStripprefixEnabled(), + service_name: $serviceName, + image: data_get($service, 'image') + )); + $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( + network: $this->destination->network, + uuid: $this->uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $savedService->isGzipEnabled(), + is_stripprefix_enabled: $savedService->isStripprefixEnabled(), + service_name: $serviceName, + image: data_get($service, 'image') + )); + } + } + } + if ($this->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) { + data_set($service, 'logging', [ + 'driver' => 'fluentd', + 'options' => [ + 'fluentd-address' => 'tcp://127.0.0.1:24224', + 'fluentd-async' => 'true', + 'fluentd-sub-second-precision' => 'true', + ], + ]); + } + if ($serviceLabels->count() > 0) { + if ($this->is_container_label_escape_enabled) { + $serviceLabels = $serviceLabels->map(function ($value, $key) { + return escapeDollarSign($value); + }); + } + } + data_set($service, 'labels', $serviceLabels->toArray()); + data_forget($service, 'is_database'); + if (! data_get($service, 'restart')) { + data_set($service, 'restart', RESTART_MODE); + } + if (data_get($service, 'restart') === 'no' || data_get($service, 'exclude_from_hc')) { + $savedService->update(['exclude_from_status' => true]); + } + data_set($service, 'container_name', $containerName); + data_forget($service, 'volumes.*.content'); + data_forget($service, 'volumes.*.isDirectory'); + data_forget($service, 'volumes.*.is_directory'); + data_forget($service, 'exclude_from_hc'); + data_set($service, 'environment', $serviceVariables->toArray()); + updateCompose($savedService); + + return $service; + + }); + + $envs_from_coolify = $this->environment_variables()->get(); + $services = collect($services)->map(function ($service, $serviceName) use ($envs_from_coolify) { + $serviceVariables = collect(data_get($service, 'environment', [])); + $parsedServiceVariables = collect([]); + foreach ($serviceVariables as $key => $value) { + if (is_numeric($key)) { + $value = str($value); + if ($value->contains('=')) { + $key = $value->before('=')->value(); + $value = $value->after('=')->value(); + } else { + $key = $value->value(); + $value = null; + } + $parsedServiceVariables->put($key, $value); + } else { + $parsedServiceVariables->put($key, $value); + } + } + $parsedServiceVariables->put('COOLIFY_CONTAINER_NAME', "$serviceName-{$this->uuid}"); + $parsedServiceVariables = $parsedServiceVariables->map(function ($value, $key) use ($envs_from_coolify) { + if (! str($value)->startsWith('$')) { + $found_env = $envs_from_coolify->where('key', $key)->first(); + if ($found_env) { + return $found_env->value; + } + } + + return $value; + }); + + data_set($service, 'environment', $parsedServiceVariables->toArray()); + + return $service; + }); + $finalServices = [ + 'services' => $services->toArray(), + 'volumes' => $topLevelVolumes->toArray(), + 'networks' => $topLevelNetworks->toArray(), + 'configs' => $topLevelConfigs->toArray(), + 'secrets' => $topLevelSecrets->toArray(), + ]; + $yaml = data_forget($yaml, 'services.*.volumes.*.content'); + $this->docker_compose_raw = Yaml::dump($yaml, 10, 2); + $this->docker_compose = Yaml::dump($finalServices, 10, 2); + $this->save(); + $this->saveComposeConfigs(); + + return collect($finalServices); + } public function networks() diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 0333e8660..d59d3da4d 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -13,13 +13,13 @@ use Visus\Cuid2\Cuid2; function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null, ?bool $includePullrequests = false): Collection { $containers = collect([]); - if (!$server->isSwarm()) { + if (! $server->isSwarm()) { $containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server); $containers = format_docker_command_output_to_json($containers); $containers = $containers->map(function ($container) use ($pullRequestId, $includePullrequests) { $labels = data_get($container, 'Labels'); - if (!str($labels)->contains('coolify.pullRequestId=')) { - data_set($container, 'Labels', $labels . ",coolify.pullRequestId={$pullRequestId}"); + if (! str($labels)->contains('coolify.pullRequestId=')) { + data_set($container, 'Labels', $labels.",coolify.pullRequestId={$pullRequestId}"); return $container; } @@ -51,14 +51,14 @@ function format_docker_command_output_to_json($rawOutput): Collection try { return $outputLines - ->reject(fn($line) => empty($line)) - ->map(fn($outputLine) => json_decode($outputLine, true, flags: JSON_THROW_ON_ERROR)); + ->reject(fn ($line) => empty($line)) + ->map(fn ($outputLine) => json_decode($outputLine, true, flags: JSON_THROW_ON_ERROR)); } catch (\Throwable $e) { return collect([]); } } -function format_docker_labels_to_json(string | array $rawOutput): Collection +function format_docker_labels_to_json(string|array $rawOutput): Collection { if (is_array($rawOutput)) { return collect($rawOutput); @@ -66,7 +66,7 @@ function format_docker_labels_to_json(string | array $rawOutput): Collection $outputLines = explode(PHP_EOL, $rawOutput); return collect($outputLines) - ->reject(fn($line) => empty($line)) + ->reject(fn ($line) => empty($line)) ->map(function ($outputLine) { $outputArray = explode(',', $outputLine); @@ -116,7 +116,7 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data } else { $container = instant_remote_process(["docker inspect --format '{{json .}}' {$container_id}"], $server, $throwError); } - if (!$container) { + if (! $container) { return 'exited'; } $container = format_docker_command_output_to_json($container); @@ -140,16 +140,18 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data function generateApplicationContainerName(Application $application, $pull_request_id = 0) { + // TODO: refactor generateApplicationContainerName, we do not need $application and $pull_request_id + $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; + return $application->uuid.'-pr-'.$pull_request_id; } else { if ($consistent_container_name) { return $application->uuid; } - return $application->uuid . '-' . $now; + return $application->uuid.'-'.$now; } } function get_port_from_dockerfile($dockerfile): ?int @@ -174,19 +176,19 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica { $labels = collect([]); $labels->push('coolify.managed=true'); - $labels->push('coolify.version=' . config('version')); - $labels->push('coolify.' . $type . 'Id=' . $id); + $labels->push('coolify.version='.config('version')); + $labels->push('coolify.'.$type.'Id='.$id); $labels->push("coolify.type=$type"); - $labels->push('coolify.name=' . $name); - $labels->push('coolify.pullRequestId=' . $pull_request_id); + $labels->push('coolify.name='.$name); + $labels->push('coolify.pullRequestId='.$pull_request_id); if ($type === 'service') { - $subId && $labels->push('coolify.service.subId=' . $subId); - $subType && $labels->push('coolify.service.subType=' . $subType); + $subId && $labels->push('coolify.service.subId='.$subId); + $subType && $labels->push('coolify.service.subType='.$subType); } return $labels; } -function generateServiceSpecificFqdns(ServiceApplication | Application $resource) +function generateServiceSpecificFqdns(ServiceApplication|Application $resource) { if ($resource->getMorphClass() === 'App\Models\ServiceApplication') { $uuid = data_get($resource, 'uuid'); @@ -213,17 +215,17 @@ function generateServiceSpecificFqdns(ServiceApplication | Application $resource } if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) { $MINIO_BROWSER_REDIRECT_URL?->update([ - 'value' => generateFqdn($server, 'console-' . $uuid), + 'value' => generateFqdn($server, 'console-'.$uuid), ]); } if (is_null($MINIO_SERVER_URL?->value)) { $MINIO_SERVER_URL?->update([ - 'value' => generateFqdn($server, 'minio-' . $uuid), + 'value' => generateFqdn($server, 'minio-'.$uuid), ]); } $payload = collect([ - $MINIO_BROWSER_REDIRECT_URL->value . ':9001', - $MINIO_SERVER_URL->value . ':9000', + $MINIO_BROWSER_REDIRECT_URL->value.':9001', + $MINIO_SERVER_URL->value.':9000', ]); break; case $type?->contains('logto'): @@ -234,17 +236,17 @@ function generateServiceSpecificFqdns(ServiceApplication | Application $resource } if (is_null($LOGTO_ENDPOINT?->value)) { $LOGTO_ENDPOINT?->update([ - 'value' => generateFqdn($server, 'logto-' . $uuid), + 'value' => generateFqdn($server, 'logto-'.$uuid), ]); } if (is_null($LOGTO_ADMIN_ENDPOINT?->value)) { $LOGTO_ADMIN_ENDPOINT?->update([ - 'value' => generateFqdn($server, 'logto-admin-' . $uuid), + 'value' => generateFqdn($server, 'logto-admin-'.$uuid), ]); } $payload = collect([ - $LOGTO_ENDPOINT->value . ':3001', - $LOGTO_ADMIN_ENDPOINT->value . ':3002', + $LOGTO_ENDPOINT->value.':3001', + $LOGTO_ADMIN_ENDPOINT->value.':3002', ]); break; } @@ -267,7 +269,7 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, $host_without_www = str($host)->replace('www.', ''); $schema = $url->getScheme(); $port = $url->getPort(); - if (is_null($port) && !is_null($onlyPort)) { + if (is_null($port) && ! is_null($onlyPort)) { $port = $onlyPort; } $labels->push("caddy_{$loop}={$schema}://{$host}"); @@ -283,7 +285,7 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, if ($is_gzip_enabled) { $labels->push("caddy_{$loop}.encode=zstd gzip"); } - if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) { + if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) { $labels->push("caddy_{$loop}.redir={$schema}://www.{$host}{uri}"); } if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { @@ -347,7 +349,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $path = $url->getPath(); $schema = $url->getScheme(); $port = $url->getPort(); - if (is_null($port) && !is_null($onlyPort)) { + if (is_null($port) && ! is_null($onlyPort)) { $port = $onlyPort; } $http_label = "http-{$loop}-{$uuid}"; @@ -383,7 +385,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ } if ($path !== '/') { $middlewares = collect([]); - if ($is_stripprefix_enabled && !str($image)->contains('ghost')) { + if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) { $labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}"); $middlewares->push("{$https_label}-stripprefix"); } @@ -403,7 +405,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels = $labels->merge($redirect_to_non_www); $middlewares->push($to_non_www_name); } - if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) { + if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) { $labels = $labels->merge($redirect_to_www); $middlewares->push($to_www_name); } @@ -429,7 +431,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels = $labels->merge($redirect_to_non_www); $middlewares->push($to_non_www_name); } - if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) { + if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) { $labels = $labels->merge($redirect_to_www); $middlewares->push($to_www_name); } @@ -461,7 +463,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ } if ($path !== '/') { $middlewares = collect([]); - if ($is_stripprefix_enabled && !str($image)->contains('ghost')) { + if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) { $labels->push("traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}"); $middlewares->push("{$http_label}-stripprefix"); } @@ -481,7 +483,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels = $labels->merge($redirect_to_non_www); $middlewares->push($to_non_www_name); } - if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) { + if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) { $labels = $labels->merge($redirect_to_www); $middlewares->push($to_www_name); } @@ -507,7 +509,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels = $labels->merge($redirect_to_non_www); $middlewares->push($to_non_www_name); } - if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) { + if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) { $labels = $labels->merge($redirect_to_www); $middlewares->push($to_www_name); } @@ -534,7 +536,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview $pull_request_id = data_get($preview, 'pull_request_id', 0); $appUuid = $application->uuid; if ($pull_request_id !== 0) { - $appUuid = $appUuid . '-pr-' . $pull_request_id; + $appUuid = $appUuid.'-pr-'.$pull_request_id; } $labels = collect([]); if ($pull_request_id === 0) { @@ -706,7 +708,7 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null // Easily get mappings from https://github.com/composerize/composerize/blob/master/packages/composerize/src/mappings.js foreach ($options as $option => $value) { // ray($option,$value); - if (!data_get($mapping, $option)) { + if (! data_get($mapping, $option)) { continue; } if ($option === '--ulimit') { @@ -731,13 +733,13 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null }); $compose_options->put($mapping[$option], $ulimits); } elseif ($option === '--shm-size') { - if (!is_null($value) && is_array($value) && count($value) > 0) { + if (! is_null($value) && is_array($value) && count($value) > 0) { $compose_options->put($mapping[$option], $value[0]); } } else { if ($list_options->contains($option)) { if ($compose_options->has($mapping[$option])) { - $compose_options->put($mapping[$option], $options->get($mapping[$option]) . ',' . $value); + $compose_options->put($mapping[$option], $options->get($mapping[$option]).','.$value); } else { $compose_options->put($mapping[$option], $value); } @@ -771,10 +773,11 @@ function generate_custom_docker_run_options_for_databases($docker_run_options, $ $docker_compose['services'][$container_name]['networks'][$network]['ipv6_address'] = $ipv6; } $docker_compose['services'][$container_name] = array_merge_recursive($docker_compose['services'][$container_name], $docker_run_options); + return $docker_compose; } -function validateComposeFile(string $compose, int $server_id): string | Throwable +function validateComposeFile(string $compose, int $server_id): string|Throwable { return 'OK'; try { diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index e50983535..4bdfb0cf1 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -151,7 +151,7 @@ function generate_default_proxy_configuration(Server $server) 'services' => [ 'traefik' => [ 'container_name' => 'coolify-proxy', - 'image' => 'traefik:v2.11', + 'image' => 'traefik:v3.1', 'restart' => RESTART_MODE, 'extra_hosts' => [ 'host.docker.internal:host-gateway', diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 32f63213e..d4df08b60 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1,19 +1,15 @@ startsWith('..')) { $source = $source->replaceFirst('..', $replacedWith->value()); } + if ($source->endsWith('/')) { + $source = $source->replaceLast('/', ''); + } return $source; } -function dockerComposeParserForApplications(Application $application): Collection -{ - $pullRequestId = data_get($application, 'pull_request_id', 0); - $isPullRequest = $pullRequestId === 0 ? false : true; - - $uuid = data_get($application, 'uuid'); - $server = data_get($application, 'destination.server'); - $compose = data_get($application, 'docker_compose_raw'); - $yaml = Yaml::parse($compose); - $services = data_get($yaml, 'services', collect([])); - $topLevel = collect([ - 'volumes' => collect(data_get($compose, 'volumes', [])), - 'networks' => collect(data_get($compose, 'networks', [])), - 'configs' => collect(data_get($compose, 'configs', [])), - 'secrets' => collect(data_get($compose, 'secrets', [])), - ]); - - // If there are predefined volumes, make sure they are not null - if ($topLevel->get('volumes')->count() > 0) { - $temp = collect([]); - foreach ($topLevel['volumes'] as $volumeName => $volume) { - if (is_null($volume)) { - continue; - } - $temp->put($volumeName, $volume); - } - $topLevel['volumes'] = $temp; - } - // Get the base docker network - $baseNetwork = collect([$uuid]); - if ($isPullRequest) { - $baseNetwork = collect(["{$uuid}-{$pullRequestId}"]); - } - $parsedServices = collect([]); - $fileStorages = $application->fileStorages(); - // Let's loop through the services - foreach ($services as $serviceName => $service) { - $isDatabase = isDatabaseImage(data_get_str($service, 'image')); - $image = data_get_str($service, 'image'); - $restart = data_get_str($service, 'restart', RESTART_MODE); - $logging = data_get($service, 'logging'); - $healthcheck = data_get($service, 'healthcheck'); - - if ($server->isLogDrainEnabled() && $application->isLogDrainEnabled()) { - $logging = [ - 'driver' => 'fluentd', - 'options' => [ - 'fluentd-address' => 'tcp://127.0.0.1:24224', - 'fluentd-async' => 'true', - 'fluentd-sub-second-precision' => 'true', - ], - ]; - } - - $volumes = collect(data_get($service, 'volumes', [])); - $ports = collect(data_get($service, 'ports', [])); - $networks = collect(data_get($service, 'networks', [])); - $depends_on = collect(data_get($service, 'depends_on', [])); - $labels = collect(data_get($service, 'labels', [])); - $environment = collect(data_get($service, 'environment', [])); - $buildArgs = collect(data_get($service, 'build.args', [])); - $environment = $environment->merge($buildArgs); - - // TODO: refactor generateApplicationContainerName - $baseName = generateApplicationContainerName( - application: $application, - pull_request_id: $pullRequestId - ); - $containerName = "$serviceName-$baseName"; - $volumesParsed = collect([]); - - if ($volumes->count() > 0) { - foreach ($volumes as $index => $volume) { - $type = null; - $source = null; - $target = null; - $content = null; - $isDirectory = false; - if (is_string($volume)) { - $source = str($volume)->before(':'); - $target = str($volume)->after(':')->beforeLast(':'); - $foundConfig = $fileStorages->whereMountPath($target)->first(); - if (sourceIsLocal($source)) { - $type = str('bind'); - if ($foundConfig) { - $contentNotNull_temp = data_get($foundConfig, 'content'); - if ($contentNotNull_temp) { - $content = $contentNotNull_temp; - } - $isDirectory = data_get($foundConfig, 'is_directory'); - } else { - // By default, we cannot determine if the bind is a directory or not, so we set it to directory - $isDirectory = true; - } - } else { - $type = str('volume'); - } - } elseif (is_array($volume)) { - $type = data_get_str($volume, 'type'); - $source = data_get_str($volume, 'source'); - $target = data_get_str($volume, 'target'); - $content = data_get($volume, 'content'); - $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null); - - $foundConfig = $fileStorages->whereMountPath($target)->first(); - if ($foundConfig) { - $contentNotNull_temp = data_get($foundConfig, 'content'); - if ($contentNotNull_temp) { - $content = $contentNotNull_temp; - } - $isDirectory = data_get($foundConfig, 'is_directory'); - } else { - // if isDirectory is not set (or false) & content is also not set, we assume it is a directory - if ((is_null($isDirectory) || ! $isDirectory) && is_null($content)) { - $isDirectory = true; - } - } - } - if ($type->value() === 'bind') { - if ($source->value() === '/var/run/docker.sock') { - return $volume; - } - if ($source->value() === '/tmp' || $source->value() === '/tmp/') { - return $volume; - } - $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); - $source = replaceLocalSource($source, $mainDirectory); - if ($isPullRequest) { - $source = $source."-pr-$pullRequestId"; - } - if ( - ! $application?->settings?->is_preserve_repository_enabled || $foundConfig?->is_based_on_git - ) { - // ray([ - // 'fs_path' => $source->value(), - // 'mount_path' => $target->value(), - // 'content' => $content, - // 'is_directory' => $isDirectory, - // 'resource_id' => $application->id, - // 'resource_type' => get_class($application), - // ]); - // LocalFileVolume::updateOrCreate( - // [ - // 'mount_path' => $target, - // 'resource_id' => $application->id, - // 'resource_type' => get_class($application), - // ], - // [ - // 'fs_path' => $source, - // 'mount_path' => $target, - // 'content' => $content, - // 'is_directory' => $isDirectory, - // 'resource_id' => $application->id, - // 'resource_type' => get_class($application), - // ] - // ); - } - $volume = "$source:$target"; - } elseif ($type->value() === 'volume') { - if ($topLevel->get('volumes')->has($source->value())) { - $temp = $topLevel->get('volumes')->get($source->value()); - if (data_get($temp, 'driver_opts.type') === 'cifs') { - return $volume; - } - if (data_get($temp, 'driver_opts.type') === 'nfs') { - return $volume; - } - } - $slugWithoutUuid = Str::slug($source, '-'); - $name = "{$uuid}_{$slugWithoutUuid}"; - if (is_string($volume)) { - $source = str($volume)->before(':'); - $target = str($volume)->after(':')->beforeLast(':'); - $source = $name; - $volume = "$source:$target"; - } elseif (is_array($volume)) { - data_set($volume, 'source', $name); - } - $topLevel->get('volumes')->put($name, [ - 'name' => $name, - ]); - // ray([ - // 'name' => $name, - // 'mount_path' => $target, - // 'resource_id' => $application->id, - // 'resource_type' => get_class($application), - // ]); - // LocalPersistentVolume::updateOrCreate( - // [ - // 'mount_path' => $target, - // 'resource_id' => $application->id, - // 'resource_type' => get_class($application), - // ], - // [ - // 'name' => $name, - // 'mount_path' => $target, - // 'resource_id' => $application->id, - // 'resource_type' => get_class($application), - // ] - // ); - } - // dispatch(new ServerFilesFromServerJob($application)); - $volumesParsed->put($index, $volume); - } - } - if ($depends_on?->count() > 0) { - if ($isPullRequest) { - $depends_on->transform(function ($dependency) use ($pullRequestId) { - return "$dependency-pr-$pullRequestId"; - }); - } - } - - if ($topLevel->get('networks')?->count() > 0) { - foreach ($topLevel->get('networks') as $networkName => $network) { - if ($networkName === 'default') { - continue; - } - // ignore aliases - if ($network['aliases'] ?? false) { - continue; - } - $networkExists = $networks->contains(function ($value, $key) use ($networkName) { - return $value == $networkName || $key == $networkName; - }); - if (! $networkExists) { - $networks->put($networkName, null); - } - } - } - $baseNetworkExists = $networks->contains(function ($value, $_) use ($baseNetwork) { - return $value == $baseNetwork; - }); - if (! $baseNetworkExists) { - foreach ($baseNetwork as $network) { - $topLevel->get('networks')->put($network, [ - 'name' => $network, - 'external' => true, - ]); - } - } - $networks_temp = collect(); - - foreach ($networks as $key => $network) { - if (gettype($network) === 'string') { - // networks: - // - appwrite - $networks_temp->put($network, null); - } elseif (gettype($network) === 'array') { - // networks: - // default: - // ipv4_address: 192.168.203.254 - $networks_temp->put($key, $network); - } - } - foreach ($baseNetwork as $key => $network) { - $networks_temp->put($network, null); - } - - if (data_get($application, 'settings.connect_to_docker_network')) { - $network = $application->destination->network; - $networks_temp->put($network, null); - $topLevel->get('networks')->put($network, [ - 'name' => $network, - 'external' => true, - ]); - } - - foreach ($environment as $key => $value) { - if (is_numeric($key)) { - if (is_array($value)) { - // - SESSION_SECRET: 123 - // - SESSION_SECRET: - $key = str(collect($value)->keys()->first()); - $value = str(collect($value)->values()->first()); - } else { - $variable = str($value); - if ($variable->contains('=')) { - // - SESSION_SECRET=123 - // - SESSION_SECRET= - $key = $variable->before('='); - $value = $variable->after('='); - } else { - // - SESSION_SECRET - $key = $variable; - $value = null; - } - } - } else { - // SESSION_SECRET: 123 - // SESSION_SECRET: - $key = str($key); - $value = str($value); - } - - // Auto generate FQDN and URL - if ($key->startsWith('SERVICE_FQDN') || $value->startsWith('$SERVICE_FQDN') || $value->startsWith('${SERVICE_FQDN')) { - if ($value->contains('SERVICE_FQDN')) { - $key = str(replaceVariables($value)); - $value = null; - } - $name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower(); - $fqdn = generateFqdn($server, "{$name->value()}-{$uuid}"); - - $url = str($fqdn)->replace('http://', '')->replace('https://', '')->replace('www.', ''); - $keyUrl = $key->replace('SERVICE_FQDN', 'SERVICE_URL'); - // TODO: is this needed? - // 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 ($value && get_class($value) === 'Illuminate\Support\Stringable') { - $path = $value->value(); - $fqdn = "$fqdn$path"; - } - - // ray([ - // 'key' => $key, - // 'value' => $fqdn, - // ]); - // ray($application->environment_variables()->where('key', $key)->where('application_id', $application->id)->first()); - $application->environment_variables()->where('key', $key)->where('application_id', $application->id)->firstOrCreate([ - 'key' => $key, - 'application_id' => $application->id, - ], [ - 'value' => $fqdn, - 'is_build_time' => false, - 'is_preview' => false, - ]); - $application->environment_variables()->where('key', $keyUrl)->where('application_id', $application->id)->firstOrCreate([ - 'key' => $keyUrl, - 'application_id' => $application->id, - ], [ - 'value' => $url, - 'is_build_time' => false, - 'is_preview' => false, - ]); - } elseif ($value->startsWith('$')) { - // If the value is a variable then we will add it to Coolify's DB - - // ${VARIABLE} will be VARIABLE instead - $value = str(replaceVariables($value)); - if ($value->contains(':-')) { - $key = $value->before(':'); - $defaultValue = $value->after(':-'); - } elseif ($value->contains('-')) { - $key = $value->before('-'); - $defaultValue = $value->after('-'); - } elseif ($value->contains(':?')) { - $key = $value->before(':'); - $defaultValue = $value->after(':?'); - } elseif ($value->contains('?')) { - $key = $value->before('?'); - $defaultValue = $value->after('?'); - } else { - $key = $value; - $defaultValue = null; - } - $application->environment_variables()->where('key', $key)->where('application_id', $application->id)->firstOrCreate([ - 'key' => $key, - 'application_id' => $application->id, - 'is_preview' => false, - ], [ - 'value' => $defaultValue, - 'is_build_time' => false, - 'is_preview' => false, - ]); - } - } - - // Labels - $fqdns = collect([]); - if ($application?->serviceType()) { - $fqdns = generateServiceSpecificFqdns($application); - } else { - $domains = collect(json_decode($application->docker_compose_domains)) ?? collect([]); - if ($domains->count() !== 0) { - $fqdns = data_get($domains, "$serviceName.domain"); - if (! $fqdns) { - $fqdns = collect([]); - } else { - $fqdns = str($fqdns)->explode(','); - if ($isPullRequest) { - $preview = $application->previews()->find($pullRequestId); - $docker_compose_domains = collect(json_decode(data_get($preview, 'docker_compose_domains'))); - if ($docker_compose_domains->count() > 0) { - $found_fqdn = data_get($docker_compose_domains, "$serviceName.domain"); - if ($found_fqdn) { - $fqdns = collect($found_fqdn); - } else { - $fqdns = collect([]); - } - } else { - $fqdns = $fqdns->map(function ($fqdn) use ($pullRequestId) { - $preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->id, $pullRequestId); - $url = Url::fromString($fqdn); - $template = $this->preview_url_template; - $host = $url->getHost(); - $schema = $url->getScheme(); - $random = new Cuid2; - $preview_fqdn = str_replace('{{random}}', $random, $template); - $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); - $preview_fqdn = str_replace('{{pr_id}}', $pullRequestId, $preview_fqdn); - $preview_fqdn = "$schema://$preview_fqdn"; - $preview->fqdn = $preview_fqdn; - $preview->save(); - - return $preview_fqdn; - }); - } - } - $shouldGenerateLabelsExactly = $server->settings->generate_exact_labels; - if ($shouldGenerateLabelsExactly) { - switch ($server->proxyType()) { - case ProxyTypes::TRAEFIK->value: - $labels = $labels->merge( - fqdnLabelsForTraefik( - uuid: $application->uuid, - domains: $fqdns, - serviceLabels: $labels, - generate_unique_uuid: $application->build_pack === 'dockercompose', - image: $image, - is_force_https_enabled: $application->isForceHttpsEnabled(), - is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled(), - ) - ); - break; - case ProxyTypes::CADDY->value: - $labels = $labels->merge( - fqdnLabelsForCaddy( - network: $application->destination->network, - uuid: $application->uuid, - domains: $fqdns, - serviceLabels: $labels, - image: $image, - is_force_https_enabled: $application->isForceHttpsEnabled(), - is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled(), - ) - ); - break; - } - } else { - $labels = $labels->merge( - fqdnLabelsForTraefik( - uuid: $application->uuid, - domains: $fqdns, - serviceLabels: $labels, - generate_unique_uuid: $application->build_pack === 'dockercompose', - image: $image, - is_force_https_enabled: $application->isForceHttpsEnabled(), - is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled(), - ) - ); - $labels = $labels->merge( - fqdnLabelsForCaddy( - network: $application->destination->network, - uuid: $application->uuid, - domains: $fqdns, - serviceLabels: $labels, - image: $image, - is_force_https_enabled: $application->isForceHttpsEnabled(), - is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled(), - ) - ); - } - } - } - } - - $defaultLabels = defaultLabels( - id: $application->id, - name: $containerName, - pull_request_id: $pullRequestId, - type: 'application'); - $labels = $labels->merge($defaultLabels); - - if ($labels->count() > 0 && $application->settings->is_container_label_escape_enabled) { - $labels = $labels->map(function ($value, $key) { - return escapeDollarSign($value); - }); - } - $payload = [ - 'image' => $image, - 'restart' => $restart, - 'container_name' => $containerName, - 'volumes' => $volumesParsed, - 'networks' => $networks_temp, - 'labels' => $labels, - 'environment' => $environment, - - ]; - if ($ports->count() > 0) { - $payload['ports'] = $ports; - } - if ($logging) { - $payload['logging'] = $logging; - } - if ($depends_on->count() > 0) { - $payload['depends_on'] = $depends_on; - } - if ($healthcheck) { - $payload['healthcheck'] = $healthcheck; - } - - $parsedServices->put($serviceName, $payload); - - } - - $topLevel->put('services', $parsedServices); - $customOrder = ['services', 'volumes', 'networks', 'configs', 'secrets']; - - $topLevel = $topLevel->sortBy(function ($value, $key) use ($customOrder) { - return array_search($key, $customOrder); - }); - $application->docker_compose = Yaml::dump(convertToArray($topLevel), 10, 2); - data_forget($application, 'environment_variables'); - data_forget($application, 'environment_variables_preview'); - $application->save(); - - return $topLevel; -} function convertToArray($collection) { @@ -1329,602 +797,6 @@ function convertToArray($collection) return $collection; } -function parseDockerComposeFile(Service|Application $resource, bool $isNew = false, int $pull_request_id = 0, ?int $preview_id = null) -{ - if ($resource->getMorphClass() === 'App\Models\Service') { - if ($resource->docker_compose_raw) { - try { - $yaml = Yaml::parse($resource->docker_compose_raw); - } catch (\Exception $e) { - throw new \Exception($e->getMessage()); - } - $allServices = get_service_templates(); - $topLevelVolumes = collect(data_get($yaml, 'volumes', [])); - $topLevelNetworks = collect(data_get($yaml, 'networks', [])); - $topLevelConfigs = collect(data_get($yaml, 'configs', [])); - $topLevelSecrets = collect(data_get($yaml, 'secrets', [])); - $services = data_get($yaml, 'services'); - - $generatedServiceFQDNS = collect([]); - if (is_null($resource->destination)) { - $destination = $resource->server->destinations()->first(); - if ($destination) { - $resource->destination()->associate($destination); - $resource->save(); - } - } - $definedNetwork = collect([$resource->uuid]); - if ($topLevelVolumes->count() > 0) { - $tempTopLevelVolumes = collect([]); - foreach ($topLevelVolumes as $volumeName => $volume) { - if (is_null($volume)) { - continue; - } - $tempTopLevelVolumes->put($volumeName, $volume); - } - $topLevelVolumes = collect($tempTopLevelVolumes); - } - $services = collect($services)->map(function ($service, $serviceName) use ($topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $resource, $allServices, $topLevelVolumes) { - // Workarounds for beta users. - if ($serviceName === 'registry') { - $tempServiceName = 'docker-registry'; - } else { - $tempServiceName = $serviceName; - } - if (str(data_get($service, 'image'))->contains('glitchtip')) { - $tempServiceName = 'glitchtip'; - } - if ($serviceName === 'supabase-kong') { - $tempServiceName = 'supabase'; - } - $serviceDefinition = data_get($allServices, $tempServiceName); - $predefinedPort = data_get($serviceDefinition, 'port'); - if ($serviceName === 'plausible') { - $predefinedPort = '8000'; - } - // End of workarounds for beta users. - $serviceVolumes = collect(data_get($service, 'volumes', [])); - $servicePorts = collect(data_get($service, 'ports', [])); - $serviceNetworks = collect(data_get($service, 'networks', [])); - $serviceVariables = collect(data_get($service, 'environment', [])); - $serviceLabels = collect(data_get($service, 'labels', [])); - $hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false; - if ($serviceLabels->count() > 0) { - $removedLabels = collect([]); - $serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) { - if (! str($serviceLabel)->contains('=')) { - $removedLabels->put($serviceLabelName, $serviceLabel); - - return false; - } - - return $serviceLabel; - }); - foreach ($removedLabels as $removedLabelName => $removedLabel) { - $serviceLabels->push("$removedLabelName=$removedLabel"); - } - } - - $containerName = "$serviceName-{$resource->uuid}"; - - // Decide if the service is a database - $isDatabase = isDatabaseImage(data_get_str($service, 'image')); - $image = data_get_str($service, 'image'); - data_set($service, 'is_database', $isDatabase); - - // Create new serviceApplication or serviceDatabase - if ($isDatabase) { - if ($isNew) { - $savedService = ServiceDatabase::create([ - 'name' => $serviceName, - 'image' => $image, - 'service_id' => $resource->id, - ]); - } else { - $savedService = ServiceDatabase::where([ - 'name' => $serviceName, - 'service_id' => $resource->id, - ])->first(); - } - } else { - if ($isNew) { - $savedService = ServiceApplication::create([ - 'name' => $serviceName, - 'image' => $image, - 'service_id' => $resource->id, - ]); - } else { - $savedService = ServiceApplication::where([ - 'name' => $serviceName, - 'service_id' => $resource->id, - ])->first(); - } - } - if (is_null($savedService)) { - if ($isDatabase) { - $savedService = ServiceDatabase::create([ - 'name' => $serviceName, - 'image' => $image, - 'service_id' => $resource->id, - ]); - } else { - $savedService = ServiceApplication::create([ - 'name' => $serviceName, - 'image' => $image, - 'service_id' => $resource->id, - ]); - } - } - - // Check if image changed - if ($savedService->image !== $image) { - $savedService->image = $image; - $savedService->save(); - } - // Collect/create/update networks - if ($serviceNetworks->count() > 0) { - foreach ($serviceNetworks as $networkName => $networkDetails) { - if ($networkName === 'default') { - continue; - } - // ignore alias - if ($networkDetails['aliases'] ?? false) { - continue; - } - $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { - return $value == $networkName || $key == $networkName; - }); - if (! $networkExists) { - $topLevelNetworks->put($networkDetails, null); - } - } - } - - // 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'); - $protocol = data_get($sport, 'protocol'); - $collectedPorts->push("$target:$published/$protocol"); - } - } - } - $savedService->ports = $collectedPorts->implode(','); - $savedService->save(); - - if (! $hasHostNetworkMode) { - // Add Coolify specific networks - $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { - return $value == $definedNetwork; - }); - if (! $definedNetworkExists) { - foreach ($definedNetwork as $network) { - $topLevelNetworks->put($network, [ - 'name' => $network, - 'external' => true, - ]); - } - } - $networks = collect(); - foreach ($serviceNetworks as $key => $serviceNetwork) { - if (gettype($serviceNetwork) === 'string') { - // networks: - // - appwrite - $networks->put($serviceNetwork, null); - } elseif (gettype($serviceNetwork) === 'array') { - // networks: - // default: - // ipv4_address: 192.168.203.254 - // $networks->put($serviceNetwork, null); - $networks->put($key, $serviceNetwork); - } - } - foreach ($definedNetwork as $key => $network) { - $networks->put($network, null); - } - data_set($service, 'networks', $networks->toArray()); - } - - // Collect/create/update volumes - if ($serviceVolumes->count() > 0) { - ['serviceVolumes' => $serviceVolumes, 'topLevelVolumes' => $topLevelVolumes] = parseServiceVolumes($serviceVolumes, $savedService, $topLevelVolumes); - data_set($service, 'volumes', $serviceVolumes->toArray()); - } - - // Get variables from the service - foreach ($serviceVariables as $variableName => $variable) { - if (is_numeric($variableName)) { - if (is_array($variable)) { - // - SESSION_SECRET: 123 - // - SESSION_SECRET: - $key = str(collect($variable)->keys()->first()); - $value = str(collect($variable)->values()->first()); - } else { - $variable = str($variable); - if ($variable->contains('=')) { - // - SESSION_SECRET=123 - // - SESSION_SECRET= - $key = $variable->before('='); - $value = $variable->after('='); - } else { - // - SESSION_SECRET - $key = $variable; - $value = null; - } - } - } else { - // SESSION_SECRET: 123 - // SESSION_SECRET: - $key = str($variableName); - $value = str($variable); - } - if ($key->startsWith('SERVICE_FQDN')) { - if ($isNew || $savedService->fqdn === null) { - $name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower(); - $fqdn = generateFqdn($resource->server, "{$name->value()}-{$resource->uuid}"); - if (substr_count($key->value(), '_') === 3) { - // SERVICE_FQDN_UMAMI_1000 - $port = $key->afterLast('_'); - } else { - $last = $key->afterLast('_'); - if (is_numeric($last->value())) { - // SERVICE_FQDN_3001 - $port = $last; - } else { - // SERVICE_FQDN_UMAMI - $port = null; - } - } - if ($port) { - $fqdn = "$fqdn:$port"; - } - if (substr_count($key->value(), '_') >= 2) { - if ($value) { - $path = $value->value(); - } else { - $path = null; - } - 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); - } - $fqdn = "$fqdn$path"; - } - - if (! $isDatabase) { - if ($savedService->fqdn) { - data_set($savedService, 'fqdn', $savedService->fqdn.','.$fqdn); - } else { - data_set($savedService, 'fqdn', $fqdn); - } - $savedService->save(); - } - EnvironmentVariable::create([ - 'key' => $key, - 'value' => $fqdn, - 'is_build_time' => false, - 'service_id' => $resource->id, - 'is_preview' => false, - ]); - } - // Caddy needs exact port in some cases. - if ($predefinedPort && ! $key->endsWith("_{$predefinedPort}")) { - $fqdns_exploded = str($savedService->fqdn)->explode(','); - if ($fqdns_exploded->count() > 1) { - continue; - } - $env = EnvironmentVariable::where([ - 'key' => $key, - 'service_id' => $resource->id, - ])->first(); - if ($env) { - $env_url = Url::fromString($savedService->fqdn); - $env_port = $env_url->getPort(); - if ($env_port !== $predefinedPort) { - $env_url = $env_url->withPort($predefinedPort); - $savedService->fqdn = $env_url->__toString(); - $savedService->save(); - } - } - } - - // 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"); - // } - continue; - } - if ($value?->startsWith('$')) { - $foundEnv = EnvironmentVariable::where([ - 'key' => $key, - 'service_id' => $resource->id, - ])->first(); - $value = str(replaceVariables($value)); - $key = $value; - if ($value->startsWith('SERVICE_')) { - $foundEnv = EnvironmentVariable::where([ - 'key' => $key, - 'service_id' => $resource->id, - ])->first(); - ['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value); - if (! is_null($command)) { - if ($command?->value() === 'FQDN' || $command?->value() === 'URL') { - if (Str::lower($forService) === $serviceName) { - $fqdn = generateFqdn($resource->server, $containerName); - } else { - $fqdn = generateFqdn($resource->server, Str::lower($forService).'-'.$resource->uuid); - } - if ($port) { - $fqdn = "$fqdn:$port"; - } - if ($foundEnv) { - $fqdn = data_get($foundEnv, 'value'); - // if ($savedService->fqdn) { - // $savedServiceFqdn = Url::fromString($savedService->fqdn); - // $parsedFqdn = Url::fromString($fqdn); - // $savedServicePath = $savedServiceFqdn->getPath(); - // $parsedFqdnPath = $parsedFqdn->getPath(); - // if ($savedServicePath != $parsedFqdnPath) { - // $fqdn = $parsedFqdn->withPath($savedServicePath)->__toString(); - // $foundEnv->value = $fqdn; - // $foundEnv->save(); - // } - // } - } else { - if ($command->value() === 'URL') { - $fqdn = str($fqdn)->after('://')->value(); - } - EnvironmentVariable::create([ - 'key' => $key, - 'value' => $fqdn, - 'is_build_time' => false, - 'service_id' => $resource->id, - 'is_preview' => false, - ]); - } - if (! $isDatabase) { - if ($command->value() === 'FQDN' && is_null($savedService->fqdn) && ! $foundEnv) { - $savedService->fqdn = $fqdn; - $savedService->save(); - } - // Caddy needs exact port in some cases. - if ($predefinedPort && ! $key->endsWith("_{$predefinedPort}") && $command?->value() === 'FQDN' && $resource->server->proxyType() === 'CADDY') { - $fqdns_exploded = str($savedService->fqdn)->explode(','); - if ($fqdns_exploded->count() > 1) { - continue; - } - $env = EnvironmentVariable::where([ - 'key' => $key, - 'service_id' => $resource->id, - ])->first(); - if ($env) { - $env_url = Url::fromString($env->value); - $env_port = $env_url->getPort(); - if ($env_port !== $predefinedPort) { - $env_url = $env_url->withPort($predefinedPort); - $savedService->fqdn = $env_url->__toString(); - $savedService->save(); - } - } - } - } - } else { - $generatedValue = generateEnvValue($command, $resource); - if (! $foundEnv) { - EnvironmentVariable::create([ - 'key' => $key, - 'value' => $generatedValue, - 'is_build_time' => false, - 'service_id' => $resource->id, - 'is_preview' => false, - ]); - } - } - } - } else { - if ($value->contains(':-')) { - $key = $value->before(':'); - $defaultValue = $value->after(':-'); - } elseif ($value->contains('-')) { - $key = $value->before('-'); - $defaultValue = $value->after('-'); - } elseif ($value->contains(':?')) { - $key = $value->before(':'); - $defaultValue = $value->after(':?'); - } elseif ($value->contains('?')) { - $key = $value->before('?'); - $defaultValue = $value->after('?'); - } else { - $key = $value; - $defaultValue = null; - } - $foundEnv = EnvironmentVariable::where([ - 'key' => $key, - 'service_id' => $resource->id, - ])->first(); - if ($foundEnv) { - $defaultValue = data_get($foundEnv, 'value'); - } - EnvironmentVariable::updateOrCreate([ - 'key' => $key, - 'service_id' => $resource->id, - ], [ - 'value' => $defaultValue, - 'is_build_time' => false, - 'service_id' => $resource->id, - 'is_preview' => false, - ]); - } - } - } - // Add labels to the service - if ($savedService->serviceType()) { - $fqdns = generateServiceSpecificFqdns($savedService); - } else { - $fqdns = collect(data_get($savedService, 'fqdns'))->filter(); - } - $defaultLabels = defaultLabels($resource->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id); - $serviceLabels = $serviceLabels->merge($defaultLabels); - if (! $isDatabase && $fqdns->count() > 0) { - if ($fqdns) { - $shouldGenerateLabelsExactly = $resource->server->settings->generate_exact_labels; - if ($shouldGenerateLabelsExactly) { - switch ($resource->server->proxyType()) { - case ProxyTypes::TRAEFIK->value: - $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( - uuid: $resource->uuid, - domains: $fqdns, - is_force_https_enabled: true, - serviceLabels: $serviceLabels, - is_gzip_enabled: $savedService->isGzipEnabled(), - is_stripprefix_enabled: $savedService->isStripprefixEnabled(), - service_name: $serviceName, - image: data_get($service, 'image') - )); - break; - case ProxyTypes::CADDY->value: - $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( - network: $resource->destination->network, - uuid: $resource->uuid, - domains: $fqdns, - is_force_https_enabled: true, - serviceLabels: $serviceLabels, - is_gzip_enabled: $savedService->isGzipEnabled(), - is_stripprefix_enabled: $savedService->isStripprefixEnabled(), - service_name: $serviceName, - image: data_get($service, 'image') - )); - break; - } - } else { - $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( - uuid: $resource->uuid, - domains: $fqdns, - is_force_https_enabled: true, - serviceLabels: $serviceLabels, - is_gzip_enabled: $savedService->isGzipEnabled(), - is_stripprefix_enabled: $savedService->isStripprefixEnabled(), - service_name: $serviceName, - image: data_get($service, 'image') - )); - $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( - network: $resource->destination->network, - uuid: $resource->uuid, - domains: $fqdns, - is_force_https_enabled: true, - serviceLabels: $serviceLabels, - is_gzip_enabled: $savedService->isGzipEnabled(), - is_stripprefix_enabled: $savedService->isStripprefixEnabled(), - service_name: $serviceName, - image: data_get($service, 'image') - )); - } - } - } - if ($resource->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) { - data_set($service, 'logging', [ - 'driver' => 'fluentd', - 'options' => [ - 'fluentd-address' => 'tcp://127.0.0.1:24224', - 'fluentd-async' => 'true', - 'fluentd-sub-second-precision' => 'true', - ], - ]); - } - if ($serviceLabels->count() > 0) { - if ($resource->is_container_label_escape_enabled) { - $serviceLabels = $serviceLabels->map(function ($value, $key) { - return escapeDollarSign($value); - }); - } - } - data_set($service, 'labels', $serviceLabels->toArray()); - data_forget($service, 'is_database'); - if (! data_get($service, 'restart')) { - data_set($service, 'restart', RESTART_MODE); - } - if (data_get($service, 'restart') === 'no' || data_get($service, 'exclude_from_hc')) { - $savedService->update(['exclude_from_status' => true]); - } - data_set($service, 'container_name', $containerName); - data_forget($service, 'volumes.*.content'); - data_forget($service, 'volumes.*.isDirectory'); - data_forget($service, 'volumes.*.is_directory'); - data_forget($service, 'exclude_from_hc'); - data_set($service, 'environment', $serviceVariables->toArray()); - updateCompose($savedService); - - return $service; - - }); - - $envs_from_coolify = $resource->environment_variables()->get(); - $services = collect($services)->map(function ($service, $serviceName) use ($resource, $envs_from_coolify) { - $serviceVariables = collect(data_get($service, 'environment', [])); - $parsedServiceVariables = collect([]); - foreach ($serviceVariables as $key => $value) { - if (is_numeric($key)) { - $value = str($value); - if ($value->contains('=')) { - $key = $value->before('=')->value(); - $value = $value->after('=')->value(); - } else { - $key = $value->value(); - $value = null; - } - $parsedServiceVariables->put($key, $value); - } else { - $parsedServiceVariables->put($key, $value); - } - } - $parsedServiceVariables->put('COOLIFY_CONTAINER_NAME', "$serviceName-{$resource->uuid}"); - $parsedServiceVariables = $parsedServiceVariables->map(function ($value, $key) use ($envs_from_coolify) { - if (! str($value)->startsWith('$')) { - $found_env = $envs_from_coolify->where('key', $key)->first(); - if ($found_env) { - return $found_env->value; - } - } - - return $value; - }); - - data_set($service, 'environment', $parsedServiceVariables->toArray()); - - return $service; - }); - $finalServices = [ - 'services' => $services->toArray(), - 'volumes' => $topLevelVolumes->toArray(), - 'networks' => $topLevelNetworks->toArray(), - 'configs' => $topLevelConfigs->toArray(), - 'secrets' => $topLevelSecrets->toArray(), - ]; - $yaml = data_forget($yaml, 'services.*.volumes.*.content'); - $resource->docker_compose_raw = Yaml::dump($yaml, 10, 2); - $resource->docker_compose = Yaml::dump($finalServices, 10, 2); - $resource->save(); - $resource->saveComposeConfigs(); - - return collect($finalServices); - } else { - return collect([]); - } - } -} function parseEnvVariable(Str|string $value) { diff --git a/resources/views/livewire/project/service/file-storage.blade.php b/resources/views/livewire/project/service/file-storage.blade.php index 78cf70429..3fa0574d6 100644 --- a/resources/views/livewire/project/service/file-storage.blade.php +++ b/resources/views/livewire/project/service/file-storage.blade.php @@ -1,16 +1,17 @@ -
+
- @if (data_get($resource, 'build_pack') === 'dockercompose') -

{{ data_get($resource, 'name', 'unknown') }}

- @endif + {{-- @if (data_get($resource, 'build_pack') === 'dockercompose') +

{{ data_get($resource, 'name', 'unknown') }}

+ @endif --}} @if ($fileStorage->is_directory) -
Directory Mount
+

Directory Mount

@else -
File Mount
+

File Mount

@endif -
{{ $workdir }}{{ $fs_path }} -> {{ $fileStorage->mount_path }}
-
+ + +
@if ($fileStorage->is_directory) @@ -62,5 +63,4 @@ @endif -
diff --git a/resources/views/livewire/project/service/storage.blade.php b/resources/views/livewire/project/service/storage.blade.php index 3a37f8091..df29362b7 100644 --- a/resources/views/livewire/project/service/storage.blade.php +++ b/resources/views/livewire/project/service/storage.blade.php @@ -31,10 +31,13 @@ @endif @if ($resource->persistentStorages()->get()->count() > 0) +

Volumes

@endif @if ($fileStorage->count() > 0) -
+ +

Mounts

+
@foreach ($fileStorage->sort() as $fileStorage) diff --git a/tests/Feature/DockerComposeParseTest.php b/tests/Feature/DockerComposeParseTest.php index c9bae520f..54fd8bb21 100644 --- a/tests/Feature/DockerComposeParseTest.php +++ b/tests/Feature/DockerComposeParseTest.php @@ -21,13 +21,11 @@ beforeEach(function () { 'APP_URL' => '$SERVICE_FQDN_APP', ], 'volumes' => [ - './:/var/www/html', './nginx:/etc/nginx', + 'data:/var/www/html', ], 'depends_on' => [ - 'db' => [ - 'condition' => 'service_healthy', - ], + 'db', ], ], 'db' => [ @@ -45,6 +43,11 @@ beforeEach(function () { 'timeout' => '10s', 'retries' => 10, ], + 'depends_on' => [ + 'app' => [ + 'condition' => 'service_healthy', + ], + ], ], @@ -83,23 +86,12 @@ afterEach(function () { test('ComposeParse', function () { // expect($this->jsonComposeFile)->toBeJson()->ray(); - $output = dockerComposeParserForApplications( - application: $this->application, - ); + $output = $this->application->dockerComposeParser(pull_request_id: 1, preview_id: 77); $outputOld = $this->application->parseCompose(); - expect($output)->toBeInstanceOf(Collection::class)->ray(); - expect($outputOld)->toBeInstanceOf(Collection::class)->ray(); + expect($output)->toBeInstanceOf(Collection::class); + expect($outputOld)->toBeInstanceOf(Collection::class); - // Test if image is parsed correctly - $image = data_get_str($output, 'services.app.image'); - expect($image->value())->toBe('nginx'); - - $imageOld = data_get_str($outputOld, 'services.app.image'); - expect($image->value())->toBe($imageOld->value()); - - // Test environment variables are parsed correctly - $environment = data_get_str($output, 'services.app.environment'); - $service_fqdn_app = data_get_str($environment, 'SERVICE_FQDN_APP'); + ray(Yaml::dump($output->toArray(), 10, 2)); });