Files
coolify/tests/Feature/Proxy/LegacyTraefikDashboardLabelsTest.php
Andras Bacsai d6f89d1feb 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.
2026-10-02 17:03:55 +02:00

189 lines
8.7 KiB
PHP

<?php
use App\Actions\Proxy\GetProxyConfiguration;
use App\Enums\ProxyTypes;
use App\Models\InstanceSettings;
use App\Models\Server;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Symfony\Component\Yaml\Yaml;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->withoutDefer();
InstanceSettings::forceCreate(['id' => 0]);
});
/**
* The Traefik configuration that Coolify generated before the dashboard exposure fix.
*/
function legacyTraefikConfiguration(array $extraLabels = [], bool $swarm = false): string
{
$labels = [
'traefik.enable=true',
'traefik.http.routers.traefik.entrypoints=http',
'traefik.http.routers.traefik.service=api@internal',
'traefik.http.services.traefik.loadbalancer.server.port=8080',
'coolify.managed=true',
'coolify.proxy=true',
...$extraLabels,
];
$service = [
'image' => 'traefik:v3.6',
'ports' => ['80:80', '443:443', '8080:8080'],
'command' => ['--api.dashboard=true', '--api.insecure=false', '--providers.docker.exposedbydefault=false'],
];
if ($swarm) {
$service['deploy'] = ['labels' => $labels];
} else {
$service['container_name'] = 'coolify-proxy';
$service['labels'] = $labels;
}
return "# my custom comment\n".Yaml::dump(['name' => 'coolify-proxy', 'services' => ['traefik' => $service]], 10, 2);
}
function traefikServerWithConfiguration(string $configuration, bool $applied = true): Server
{
$server = Server::factory()->create(['team_id' => Team::factory()->create()->id]);
$server->proxy->type = ProxyTypes::TRAEFIK->value;
$server->proxy->status = 'running';
$server->proxy->last_saved_proxy_configuration = $configuration;
$server->proxy->last_saved_settings = md5(base64_encode($configuration));
$server->proxy->last_applied_settings = $applied ? $server->proxy->last_saved_settings : null;
$server->save();
return $server->fresh();
}
test('the legacy dashboard router labels are replaced with traefik.enable=false', function (bool $swarm) {
$configuration = legacyTraefikConfiguration(swarm: $swarm);
$fixed = removeLegacyTraefikDashboardLabels($configuration);
$labels = data_get(Yaml::parse($fixed), $swarm ? 'services.traefik.deploy.labels' : 'services.traefik.labels');
expect($labels)->toBe(['traefik.enable=false', 'coolify.managed=true', 'coolify.proxy=true'])
->and($fixed)->toContain('# my custom comment')
->and($fixed)->not->toContain('api@internal')
->and(removeLegacyTraefikDashboardLabels($fixed))->toBe($fixed);
})->with(['standalone' => false, 'swarm' => true]);
test('quoted legacy labels are also replaced', function () {
$configuration = str_replace(
['- traefik.enable=true', '- traefik.http.routers.traefik.service=api@internal'],
["- 'traefik.enable=true'", '- "traefik.http.routers.traefik.service=api@internal"'],
legacyTraefikConfiguration(),
);
$labels = data_get(Yaml::parse(removeLegacyTraefikDashboardLabels($configuration)), 'services.traefik.labels');
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);
expect(removeLegacyTraefikDashboardLabels($configuration))->toBe($configuration);
})->with([
'custom rule' => [['traefik.http.routers.traefik.rule=Host(`traefik.example.com`)']],
'auth middleware' => [['traefik.http.routers.traefik.middlewares=auth']],
'other router on the proxy' => [['traefik.http.routers.other.rule=Host(`other.example.com`)']],
]);
test('configurations without the legacy dashboard router are not changed', function () {
$configuration = Yaml::dump(['services' => ['traefik' => ['labels' => ['traefik.enable=false', 'coolify.managed=true', 'coolify.proxy=true']]]], 10, 2);
expect(removeLegacyTraefikDashboardLabels($configuration))->toBe($configuration)
->and(removeLegacyTraefikDashboardLabels("services:\n traefik: [\n"))->toBe("services:\n traefik: [\n");
});
test('loading a legacy configuration saves the fix and marks the proxy for restart', function () {
$server = traefikServerWithConfiguration(legacyTraefikConfiguration());
expect($server->hasPendingProxyConfiguration())->toBeFalse();
$configuration = GetProxyConfiguration::run($server);
$server->refresh();
expect($configuration)->not->toContain('api@internal')
->and($server->proxy->last_saved_proxy_configuration)->toBe($configuration)
->and($server->proxy->last_saved_settings)->toBe(md5(base64_encode($configuration)))
->and($server->hasPendingProxyConfiguration())->toBeTrue();
});
test('the fix marks a running proxy for restart when no applied settings were recorded', function () {
$legacy = legacyTraefikConfiguration();
$server = traefikServerWithConfiguration($legacy, applied: false);
GetProxyConfiguration::run($server);
$server->refresh();
expect($server->proxy->last_applied_settings)->toBe(md5(base64_encode($legacy)))
->and($server->hasPendingProxyConfiguration())->toBeTrue();
});
test('the migration fixes saved legacy configurations without connecting to servers', function () {
$legacy = traefikServerWithConfiguration(legacyTraefikConfiguration());
$custom = traefikServerWithConfiguration(legacyTraefikConfiguration(['traefik.http.routers.traefik.middlewares=auth']));
$caddy = Server::factory()->create(['team_id' => Team::factory()->create()->id]);
$caddy->proxy->type = ProxyTypes::CADDY->value;
$caddy->proxy->last_saved_proxy_configuration = 'services: {}';
$caddy->save();
(require database_path('migrations/2026_09_25_210000_remove_legacy_traefik_dashboard_labels.php'))->up();
expect($legacy->fresh()->proxy->last_saved_proxy_configuration)->not->toContain('api@internal')
->and($legacy->fresh()->hasPendingProxyConfiguration())->toBeTrue()
->and($custom->fresh()->proxy->last_saved_proxy_configuration)->toContain('api@internal')
->and($custom->fresh()->hasPendingProxyConfiguration())->toBeFalse()
->and($caddy->fresh()->proxy->last_saved_proxy_configuration)->toBe('services: {}');
});
test('the migration saves each server outside one shared transaction', function () {
// In Postgres one failed update aborts a shared transaction, so later servers could not be saved.
$migration = require database_path('migrations/2026_09_25_210000_remove_legacy_traefik_dashboard_labels.php');
expect($migration->withinTransaction)->toBeFalse();
});
test('the pending proxy notice asks the user to restart the proxy', function () {
$team = Team::factory()->create();
$user = User::factory()->create();
$user->teams()->attach($team, ['role' => 'admin']);
$server = Server::factory()->create(['team_id' => $team->id]);
$server->settings()->update(['is_reachable' => true, 'is_usable' => true]);
$server->proxy->type = ProxyTypes::TRAEFIK->value;
$server->proxy->status = 'running';
$server->proxy->last_saved_settings = 'new-hash';
$server->proxy->last_applied_settings = 'old-hash';
$server->save();
$this->actingAs($user);
session(['currentTeam' => $team]);
Livewire::test('server.navbar', ['server' => $server->fresh()])
->assertSee('Your configuration changed, please restart the proxy.')
->assertDontSee('The saved proxy configuration has not been applied');
});