mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 12:16:45 +00:00
fix(compose): normalize service-name keys for domains and env vars (#11040)
This commit is contained in:
1 parent
7b18777f06
commit
0b843bb07c
18 files changed
+1443
-220
No files matched your search
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
@@ -310,3 +311,281 @@ function checkIfDomainIsAlreadyUsedViaAPI(Collection|array $domains, ?string $te
|
||||
'hasConflicts' => count($conflicts) > 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a compose service name the way Coolify historically did for env var keys
|
||||
* (hyphens and dots → underscores). Used for comparison and SERVICE_* env names only —
|
||||
* docker_compose_domains keys should use the original compose service name.
|
||||
*/
|
||||
function normalizeComposeServiceName(string $serviceName): string
|
||||
{
|
||||
return str($serviceName)->replace('-', '_')->replace('.', '_')->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a candidate key (original or legacy-normalized) to the original compose service name.
|
||||
*
|
||||
* @param iterable<int|string, mixed> $serviceNames
|
||||
*/
|
||||
function findComposeServiceName(string $candidate, iterable $serviceNames): ?string
|
||||
{
|
||||
$names = collect($serviceNames)->map(fn ($name) => (string) $name)->values();
|
||||
|
||||
if ($names->containsStrict($candidate)) {
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
$normalized = normalizeComposeServiceName($candidate);
|
||||
$matches = $names->filter(
|
||||
fn ($name) => normalizeComposeServiceName($name) === $normalized
|
||||
)->values();
|
||||
|
||||
return $matches->count() === 1 ? $matches->first() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse domain-map keys that only differ by hyphen/dot/underscore into one preferred name.
|
||||
* Prefers a key that still has `-` or `.` over a fully underscore-normalized twin.
|
||||
*
|
||||
* @param iterable<int|string, mixed> $domainKeys
|
||||
* @return list<string>
|
||||
*/
|
||||
function preferredComposeServiceNamesFromDomainKeys(iterable $domainKeys): array
|
||||
{
|
||||
$groups = [];
|
||||
foreach ($domainKeys as $key) {
|
||||
$key = (string) $key;
|
||||
$groups[normalizeComposeServiceName($key)][] = $key;
|
||||
}
|
||||
|
||||
$preferred = [];
|
||||
foreach ($groups as $normalized => $keys) {
|
||||
$keys = array_values(array_unique($keys));
|
||||
$withSeparators = array_values(array_filter(
|
||||
$keys,
|
||||
fn (string $key) => $key !== $normalized
|
||||
));
|
||||
$preferred[] = $withSeparators[0] ?? $keys[0];
|
||||
}
|
||||
|
||||
return $preferred;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read domain string for a compose service from docker_compose_domains.
|
||||
* Prefers a filled domain on the requested key; falls back to any filled twin
|
||||
* (legacy underscore keys). Blank entries do not shadow filled twins.
|
||||
* Uses collection key access (not dotted data_get) so names like "api.test" work.
|
||||
*
|
||||
* @param array<string, mixed>|Collection<string, mixed> $domains
|
||||
*/
|
||||
function getComposeServiceDomainString(array|Collection $domains, string $serviceName): ?string
|
||||
{
|
||||
$domains = collect($domains);
|
||||
$normalized = normalizeComposeServiceName($serviceName);
|
||||
$matches = [];
|
||||
|
||||
foreach ($domains as $key => $entry) {
|
||||
$key = (string) $key;
|
||||
if ($key !== $serviceName
|
||||
&& $key !== $normalized
|
||||
&& normalizeComposeServiceName($key) !== $normalized) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$matches[] = [
|
||||
'key' => $key,
|
||||
'domain' => composeDomainEntryString($entry),
|
||||
'is_requested' => $key === $serviceName,
|
||||
];
|
||||
}
|
||||
|
||||
if ($matches === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$filled = array_values(array_filter(
|
||||
$matches,
|
||||
fn (array $match) => filled($match['domain'])
|
||||
));
|
||||
|
||||
if ($filled !== []) {
|
||||
foreach ($filled as $match) {
|
||||
if ($match['is_requested']) {
|
||||
return $match['domain'];
|
||||
}
|
||||
}
|
||||
|
||||
return $filled[0]['domain'];
|
||||
}
|
||||
|
||||
foreach ($matches as $match) {
|
||||
if ($match['is_requested']) {
|
||||
return $match['domain'];
|
||||
}
|
||||
}
|
||||
|
||||
return $matches[0]['domain'];
|
||||
}
|
||||
|
||||
function composeDomainEntryString(mixed $entry): ?string
|
||||
{
|
||||
if (is_object($entry)) {
|
||||
$entry = (array) $entry;
|
||||
}
|
||||
|
||||
if (! is_array($entry)) {
|
||||
return is_string($entry) ? $entry : null;
|
||||
}
|
||||
|
||||
$domain = $entry['domain'] ?? null;
|
||||
|
||||
return is_string($domain) ? $domain : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose which domain string wins when merging twin compose domain keys.
|
||||
* Filled values always beat blanks; when both are filled, prefer the canonical key.
|
||||
*/
|
||||
function preferComposeDomainValue(
|
||||
mixed $existingDomain,
|
||||
bool $existingIsCanonical,
|
||||
mixed $incomingDomain,
|
||||
bool $incomingIsCanonical,
|
||||
): mixed {
|
||||
$existingFilled = filled($existingDomain);
|
||||
$incomingFilled = filled($incomingDomain);
|
||||
|
||||
if ($existingFilled && ! $incomingFilled) {
|
||||
return $existingDomain;
|
||||
}
|
||||
|
||||
if ($incomingFilled && ! $existingFilled) {
|
||||
return $incomingDomain;
|
||||
}
|
||||
|
||||
if ($existingFilled && $incomingFilled) {
|
||||
if ($incomingIsCanonical && ! $existingIsCanonical) {
|
||||
return $incomingDomain;
|
||||
}
|
||||
|
||||
return $existingDomain;
|
||||
}
|
||||
|
||||
// Both blank: keep canonical slot when possible.
|
||||
if ($incomingIsCanonical) {
|
||||
return $incomingDomain;
|
||||
}
|
||||
|
||||
return $existingDomain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rekey docker_compose_domains to original compose service names.
|
||||
* Merges legacy underscore/dot twin keys onto the canonical compose name.
|
||||
* Never lets an empty canonical key wipe a filled legacy twin.
|
||||
*
|
||||
* @param array<string, mixed>|Collection<string, mixed> $domains
|
||||
* @param iterable<int|string, mixed> $serviceNames
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function rekeyComposeDomainsToServiceNames(array|Collection $domains, iterable $serviceNames): array
|
||||
{
|
||||
$domains = collect($domains);
|
||||
$serviceNames = collect($serviceNames)->map(fn ($name) => (string) $name)->values();
|
||||
$rekeyed = [];
|
||||
$canonicalDomainSources = [];
|
||||
|
||||
foreach ($domains as $key => $value) {
|
||||
$key = (string) $key;
|
||||
$original = findComposeServiceName($key, $serviceNames) ?? $key;
|
||||
$isCanonical = $key === $original;
|
||||
|
||||
if (is_object($value)) {
|
||||
$value = (array) $value;
|
||||
}
|
||||
if (! is_array($value)) {
|
||||
$value = ['domain' => $value];
|
||||
}
|
||||
|
||||
if (! isset($rekeyed[$original])) {
|
||||
$rekeyed[$original] = $value;
|
||||
$canonicalDomainSources[$original] = $isCanonical;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$existingDomain = $rekeyed[$original]['domain'] ?? null;
|
||||
$incomingDomain = $value['domain'] ?? null;
|
||||
$rekeyed[$original] = array_merge($rekeyed[$original], $value);
|
||||
$rekeyed[$original]['domain'] = preferComposeDomainValue(
|
||||
$existingDomain,
|
||||
$canonicalDomainSources[$original],
|
||||
$incomingDomain,
|
||||
$isCanonical,
|
||||
);
|
||||
$canonicalDomainSources[$original] = $canonicalDomainSources[$original] || $isCanonical;
|
||||
}
|
||||
|
||||
return $rekeyed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a SERVICE_FQDN_ / SERVICE_URL_ env fragment to a ServiceApplication by compose name.
|
||||
* Handles legacy underscore fragments and dotted compose names (api.test vs API_TEST).
|
||||
*/
|
||||
function findServiceApplicationForEnvName(Service $resource, string $envServiceName): ?ServiceApplication
|
||||
{
|
||||
$names = $resource->applications()->pluck('name');
|
||||
$resolved = findComposeServiceName($envServiceName, $names)
|
||||
?? findComposeServiceName(str($envServiceName)->replace('_', '-')->toString(), $names);
|
||||
|
||||
if ($resolved === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $resource->applications()->where('name', $resolved)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Put/update a service domain entry under the original compose service name,
|
||||
* removing legacy twin keys that resolve unambiguously to the same name.
|
||||
*
|
||||
* @param array<string, mixed>|Collection<string, mixed> $domains
|
||||
* @param iterable<int|string, mixed> $serviceNames
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function putComposeServiceDomain(
|
||||
array|Collection $domains,
|
||||
string $serviceName,
|
||||
?string $domainString,
|
||||
iterable $serviceNames = [],
|
||||
): array {
|
||||
$domains = collect($domains)->all();
|
||||
$names = collect($serviceNames)->map(fn ($name) => (string) $name);
|
||||
$storageKey = findComposeServiceName($serviceName, $names) ?? $serviceName;
|
||||
|
||||
$merged = ['domain' => $domainString];
|
||||
foreach (array_keys($domains) as $key) {
|
||||
$key = (string) $key;
|
||||
if ($key !== $storageKey && findComposeServiceName($key, $names) !== $storageKey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing = $domains[$key];
|
||||
if (is_object($existing)) {
|
||||
$existing = (array) $existing;
|
||||
}
|
||||
if (is_array($existing)) {
|
||||
$merged = array_merge($existing, $merged);
|
||||
}
|
||||
|
||||
if ($key !== $storageKey) {
|
||||
unset($domains[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$domains[$storageKey] = $merged;
|
||||
|
||||
return $domains;
|
||||
}
|
||||
Reference in new issue
Block a user