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);