From d6f89d1feb8ef65c93066a7519891c65f02144b3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 2 Oct 2026 17:03:55 +0200 Subject: [PATCH] fix(proxy): remove legacy Traefik dashboard labels only in the traefik service The line edits ran on the whole proxy compose file. When another service had the same labels, the safety check failed and the dashboard fix was skipped without a warning. Limit the edits to the traefik service block, so other services stay unchanged and the fix is applied. --- bootstrap/helpers/proxy.php | 44 +++++++++++++++++-- .../LegacyTraefikDashboardLabelsTest.php | 19 ++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index 90344e2466..a4b98ca5fc 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -566,10 +566,17 @@ function removeLegacyTraefikDashboardLabels(string $configuration): string return $configuration; } - $fixed = preg_replace('/^([ \t]*-[ \t]*([\'"]?))traefik\.enable=true(\2[ \t]*)(?=\R|\z)/m', '$1traefik.enable=false$3', $configuration); - foreach (array_slice($legacyLabels, 1) as $label) { - $fixed = preg_replace('/^[ \t]*-[ \t]*([\'"]?)'.preg_quote($label, '/').'\1[ \t]*(?:\R|\z)/m', '', $fixed); + // Edit only the lines of the traefik service, so equal labels of other services stay unchanged. + $block = traefikServiceBlockRange($configuration); + if ($block === null) { + return $configuration; } + [$start, $length] = $block; + $traefikBlock = preg_replace('/^([ \t]*-[ \t]*([\'"]?))traefik\.enable=true(\2[ \t]*)(?=\R|\z)/m', '$1traefik.enable=false$3', substr($configuration, $start, $length)); + foreach (array_slice($legacyLabels, 1) as $label) { + $traefikBlock = preg_replace('/^[ \t]*-[ \t]*([\'"]?)'.preg_quote($label, '/').'\1[ \t]*(?:\R|\z)/m', '', $traefikBlock); + } + $fixed = substr($configuration, 0, $start).$traefikBlock.substr($configuration, $start + $length); // Keep the original when the line edit also changed other parts of the file. try { @@ -579,6 +586,37 @@ function removeLegacyTraefikDashboardLabels(string $configuration): string } } +/** + * The byte offset and length of the `traefik` service block under the top-level `services` key, + * or null when the block cannot be found in the text. + * + * @return array{0: int, 1: int}|null + */ +function traefikServiceBlockRange(string $configuration): ?array +{ + if (! preg_match('/^services:[ \t]*(?:#.*)?$/m', $configuration, $services, PREG_OFFSET_CAPTURE)) { + return null; + } + $offset = $services[0][1] + strlen($services[0][0]); + + // The services are the lines with the indent of the first content line below `services:`. + if (! preg_match('/^([ \t]+)[^\s#]/m', $configuration, $firstChild, PREG_OFFSET_CAPTURE, $offset)) { + return null; + } + $indent = preg_quote($firstChild[1][0], '/'); + if (! preg_match('/^'.$indent.'([\'"]?)traefik\1:[ \t]*(?:#.*)?(?:\R|\z)/m', $configuration, $traefik, PREG_OFFSET_CAPTURE, $offset)) { + return null; + } + $start = $traefik[0][1] + strlen($traefik[0][0]); + + // The block ends at the next content line that is not indented deeper than the service key. + $end = preg_match('/^(?!'.$indent.'[ \t])(?=[ \t]*[^\s#])/m', $configuration, $next, PREG_OFFSET_CAPTURE, $start) + ? $next[0][1] + : strlen($configuration); + + return [$start, $end - $start]; +} + /** * Saves the Traefik configuration without the legacy dashboard labels. The next proxy restart applies it. */ diff --git a/tests/Feature/Proxy/LegacyTraefikDashboardLabelsTest.php b/tests/Feature/Proxy/LegacyTraefikDashboardLabelsTest.php index dc8fefc3af..2350cd981e 100644 --- a/tests/Feature/Proxy/LegacyTraefikDashboardLabelsTest.php +++ b/tests/Feature/Proxy/LegacyTraefikDashboardLabelsTest.php @@ -83,6 +83,25 @@ test('quoted legacy labels are also replaced', function () { expect($labels)->toBe(['traefik.enable=false', 'coolify.managed=true', 'coolify.proxy=true']); }); +test('only the traefik service is changed when another proxy service has the same labels', function (bool $swarm, bool $whoamiFirst) { + $whoami = Yaml::dump(['whoami' => ['image' => 'traefik/whoami', 'labels' => [ + 'traefik.enable=true', + 'traefik.http.routers.whoami.rule=Host(`whoami.example.com`)', + 'traefik.http.services.traefik.loadbalancer.server.port=8080', + ]]], 10, 2); + $whoami = preg_replace('/^/m', ' ', rtrim($whoami))."\n"; + $configuration = $whoamiFirst + ? str_replace("services:\n", "services:\n".$whoami, legacyTraefikConfiguration(swarm: $swarm)) + : legacyTraefikConfiguration(swarm: $swarm).$whoami; + + $fixed = removeLegacyTraefikDashboardLabels($configuration); + $parsed = Yaml::parse($fixed); + + expect(data_get($parsed, $swarm ? 'services.traefik.deploy.labels' : 'services.traefik.labels'))->toBe(['traefik.enable=false', 'coolify.managed=true', 'coolify.proxy=true']) + ->and(data_get($parsed, 'services.whoami'))->toBe(data_get(Yaml::parse($configuration), 'services.whoami')) + ->and($fixed)->toContain('# my custom comment'); +})->with(['standalone' => false, 'swarm' => true])->with(['whoami after traefik' => false, 'whoami before traefik' => true]); + test('a dashboard router that the user changed is kept', function (array $extraLabels) { $configuration = legacyTraefikConfiguration($extraLabels);