fix: resolve Docker Compose service name collisions with dots

Services like "api.test" and "api-test" now create unique Traefik labels
by normalizing dots to hyphens and adding a 4-char hash suffix. Update
docker_compose_domains storage to use original service names instead of
transformed names to prevent key collisions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai 2025-12-01 13:39:24 +01:00
parent 0e6a2fc15d
commit 22ce45fb84
2 changed files with 25 additions and 7 deletions

View file

@ -11,6 +11,16 @@ use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
/**
* Generate a stable 4-character hash from a service name for uniqueness
* This is used to differentiate services like "api.test" and "api-test"
* which would otherwise collide when normalized.
*/
function serviceNameHash(string $serviceName): string
{
return substr(md5($serviceName), 0, 4);
}
function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null, ?bool $includePullrequests = false): Collection
{
$containers = collect([]);
@ -467,8 +477,13 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$http_label = "http-{$loop}-{$uuid}";
$https_label = "https-{$loop}-{$uuid}";
if ($service_name) {
$http_label = "http-{$loop}-{$uuid}-{$service_name}";
$https_label = "https-{$loop}-{$uuid}-{$service_name}";
// Normalize service name for Traefik labels by replacing dots with hyphens
// This prevents label parsing issues with service names like "api.test"
// Add a 4-char hash to ensure uniqueness for services like "api.test" vs "api-test"
$normalized_service_name = str($service_name)->replace('.', '-')->value();
$hash = serviceNameHash($service_name);
$http_label = "http-{$loop}-{$uuid}-{$normalized_service_name}-{$hash}";
$https_label = "https-{$loop}-{$uuid}-{$normalized_service_name}-{$hash}";
}
if (str($image)->contains('ghost')) {
$labels->push("traefik.http.middlewares.redir-ghost-{$uuid}.redirectregex.regex=^{$path}/(.*)");

View file

@ -601,24 +601,27 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
if ($resource->build_pack === 'dockercompose') {
// Check if a service with this name actually exists
$serviceExists = false;
$actualServiceName = null;
foreach ($services as $serviceNameKey => $service) {
$transformedServiceName = str($serviceNameKey)->replace('-', '_')->replace('.', '_')->value();
if ($transformedServiceName === $serviceName) {
$serviceExists = true;
$actualServiceName = $serviceNameKey; // Store the ORIGINAL service name
break;
}
}
// Only add domain if the service exists
if ($serviceExists) {
if ($serviceExists && $actualServiceName) {
$domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]);
$domainExists = data_get($domains->get($serviceName), 'domain');
// Use the ORIGINAL service name as the key to avoid collisions
$domainExists = data_get($domains->get($actualServiceName), 'domain');
// Update domain using URL with port if applicable
$domainValue = $port ? $urlWithPort : $url;
if (is_null($domainExists)) {
$domains->put($serviceName, [
$domains->put($actualServiceName, [
'domain' => $domainValue,
]);
$resource->docker_compose_domains = $domains->toJson();
@ -1077,8 +1080,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
if ($resource->build_pack !== 'dockercompose') {
$domains = collect([]);
}
$changedServiceName = str($serviceName)->replace('-', '_')->replace('.', '_')->value();
$fqdns = data_get($domains, "$changedServiceName.domain");
// Use the original service name for lookup (no transformation needed)
$fqdns = data_get($domains, "$serviceName.domain");
// Generate SERVICE_FQDN & SERVICE_URL for dockercompose
if ($resource->build_pack === 'dockercompose') {
foreach ($domains as $forServiceName => $domain) {