mirror of
https://github.com/coollabsio/coolify.git
synced 2026-09-27 09:45:58 -04:00
fix(proxy): remove legacy Traefik dashboard labels from saved configs
Older Coolify versions saved Traefik configs with a router that sends requests with the proxy container name as Host header (for example traefik-coolify-proxy) on port 80 to api@internal. New installs no longer get these labels, but saved configs keep them after an upgrade. - Replace only the unchanged legacy labels with traefik.enable=false, keep comments and formatting, and skip routers the user customized. - A migration and GetProxyConfiguration fix the saved config in the database only. The proxy is not restarted; the UI asks for a restart. - Change the pending notice to "Your configuration changed, please restart the proxy." - Resolve conflict markers committed in the Server Proxy component, which broke the proxy page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
165d2f3dc8
commit
f92ee342a1
@@ -44,6 +44,10 @@ class GetProxyConfiguration
|
||||
if (empty(trim($proxy_configuration ?? ''))) {
|
||||
$proxy_configuration = $this->backfillFromDisk($server);
|
||||
}
|
||||
|
||||
if (! empty(trim($proxy_configuration ?? '')) && removeLegacyTraefikDashboardExposure($server)) {
|
||||
$proxy_configuration = $server->proxy->get('last_saved_proxy_configuration');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate default configuration as last resort
|
||||
|
||||
@@ -201,7 +201,6 @@ class Proxy extends Component
|
||||
}
|
||||
}
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
public function getTraefikVersionForWarningProperty(): ?string
|
||||
{
|
||||
if ($this->server->detected_traefik_version) {
|
||||
@@ -218,7 +217,8 @@ class Proxy extends Component
|
||||
}
|
||||
|
||||
return $matches[1];
|
||||
=======
|
||||
}
|
||||
|
||||
public function loadTraefikCertificates(): void
|
||||
{
|
||||
$this->traefikCertificates = [];
|
||||
@@ -243,7 +243,6 @@ class Proxy extends Component
|
||||
} catch (\Throwable $e) {
|
||||
handleError($e, $this);
|
||||
}
|
||||
>>>>>>> Stashed changes
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -422,6 +422,97 @@ function extractCustomProxyCommands(Server $server, string $existing_config): ar
|
||||
|
||||
return $custom_commands;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the dashboard router labels that older Coolify versions generated for Traefik.
|
||||
* These labels route requests with the proxy container name as Host header to the Traefik API and dashboard.
|
||||
* Comments and formatting are kept. A router or label set that the user changed is not touched.
|
||||
*/
|
||||
function removeLegacyTraefikDashboardLabels(string $configuration): string
|
||||
{
|
||||
$legacyLabels = [
|
||||
'traefik.enable=true',
|
||||
'traefik.http.routers.traefik.entrypoints=http',
|
||||
'traefik.http.routers.traefik.service=api@internal',
|
||||
'traefik.http.services.traefik.loadbalancer.server.port=8080',
|
||||
];
|
||||
|
||||
try {
|
||||
$yaml = Yaml::parse($configuration);
|
||||
} catch (Throwable) {
|
||||
return $configuration;
|
||||
}
|
||||
|
||||
$expected = $yaml;
|
||||
$changed = false;
|
||||
foreach (['services.traefik.labels', 'services.traefik.deploy.labels'] as $path) {
|
||||
$labels = data_get($yaml, $path);
|
||||
if (! is_array($labels) || ! array_is_list($labels) || array_filter($labels, 'is_string') !== $labels) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$traefikLabels = array_filter($labels, fn (string $label) => str_starts_with($label, 'traefik.'));
|
||||
if (! in_array('traefik.http.routers.traefik.service=api@internal', $traefikLabels, true) || array_diff($traefikLabels, $legacyLabels) !== []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$newLabels = [];
|
||||
foreach ($labels as $label) {
|
||||
if ($label === 'traefik.enable=true') {
|
||||
$newLabels[] = 'traefik.enable=false';
|
||||
} elseif (! in_array($label, $legacyLabels, true)) {
|
||||
$newLabels[] = $label;
|
||||
}
|
||||
}
|
||||
data_set($expected, $path, $newLabels);
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
if (! $changed) {
|
||||
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);
|
||||
}
|
||||
|
||||
// Keep the original when the line edit also changed other parts of the file.
|
||||
try {
|
||||
return Yaml::parse($fixed) === $expected ? $fixed : $configuration;
|
||||
} catch (Throwable) {
|
||||
return $configuration;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the Traefik configuration without the legacy dashboard labels. The next proxy restart applies it.
|
||||
*/
|
||||
function removeLegacyTraefikDashboardExposure(Server $server): bool
|
||||
{
|
||||
$configuration = $server->proxy->get('last_saved_proxy_configuration');
|
||||
if ($server->proxyType() !== ProxyTypes::TRAEFIK->value || ! is_string($configuration) || blank($configuration)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fixed = removeLegacyTraefikDashboardLabels($configuration);
|
||||
if ($fixed === $configuration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The running proxy still uses the old configuration, so the UI must ask for a restart.
|
||||
if (blank($server->proxy->get('last_applied_settings'))) {
|
||||
$server->proxy->last_applied_settings = md5(base64_encode($configuration));
|
||||
}
|
||||
$server->proxy->last_saved_proxy_configuration = $fixed;
|
||||
$server->proxy->last_saved_settings = md5(base64_encode($fixed));
|
||||
$server->save();
|
||||
|
||||
Log::info('Removed legacy Traefik dashboard labels from the proxy configuration', ['server_id' => $server->id]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function generateDefaultProxyConfiguration(Server $server, array $custom_commands = [])
|
||||
{
|
||||
Log::info('Generating default proxy configuration', [
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Remove the Traefik dashboard router labels from saved proxy configurations.
|
||||
* Only the database is changed. The proxy shows a restart notice and applies the fix on the next restart.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Server::query()->chunkById(100, function ($servers) {
|
||||
foreach ($servers as $server) {
|
||||
try {
|
||||
removeLegacyTraefikDashboardExposure($server);
|
||||
} catch (Throwable $e) {
|
||||
Log::warning('Could not remove legacy Traefik dashboard labels', [
|
||||
'server_id' => $server->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
||||
@@ -17,19 +17,16 @@
|
||||
</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-[13px] leading-4 font-semibold text-neutral-950 dark:text-fg">
|
||||
The saved proxy configuration has not been applied
|
||||
</p>
|
||||
<p class="mt-0.5 text-[11px] leading-4 text-neutral-600 dark:text-fg-dim">
|
||||
Restart the proxy to apply these changes.
|
||||
@if ($canRestart)
|
||||
<button type="button"
|
||||
class="ml-0.5 inline-flex items-center gap-0.5 font-semibold text-coollabs transition-colors hover:text-coollabs-100 dark:text-warning dark:hover:text-warning/80"
|
||||
@click="open = false; document.getElementById('server-mobile-restart-proxy-trigger')?.click()">
|
||||
Restart proxy
|
||||
<x-reicon name="arrow-right" class="size-2.5" />
|
||||
</button>
|
||||
@endif
|
||||
Your configuration changed, please restart the proxy.
|
||||
</p>
|
||||
@if ($canRestart)
|
||||
<button type="button"
|
||||
class="mt-0.5 inline-flex items-center gap-0.5 text-[11px] leading-4 font-semibold text-coollabs transition-colors hover:text-coollabs-100 dark:text-warning dark:hover:text-warning/80"
|
||||
@click="open = false; document.getElementById('server-mobile-restart-proxy-trigger')?.click()">
|
||||
Restart proxy
|
||||
<x-reicon name="arrow-right" class="size-2.5" />
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,9 +36,7 @@
|
||||
@if (
|
||||
$server->proxy->last_applied_settings &&
|
||||
$server->proxy->last_saved_settings !== $server->proxy->last_applied_settings)
|
||||
<x-callout type="warning" title="Configuration out of sync">
|
||||
Restart the proxy to apply the saved configuration.
|
||||
</x-callout>
|
||||
<x-callout type="warning" title="Your configuration changed, please restart the proxy." />
|
||||
@else
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
<?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('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 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');
|
||||
});
|
||||
@@ -97,7 +97,7 @@ test('running proxy shows pending configuration warning when saved settings diff
|
||||
|
||||
$component = Livewire::test('server.navbar', ['server' => $server->fresh()])
|
||||
->assertSee('Changes pending')
|
||||
->assertSee('The saved proxy configuration has not been applied')
|
||||
->assertSee('Your configuration changed, please restart the proxy.')
|
||||
->assertSee('Restart proxy');
|
||||
|
||||
$server->refresh();
|
||||
@@ -107,7 +107,7 @@ test('running proxy shows pending configuration warning when saved settings diff
|
||||
|
||||
$component->call('showNotification')
|
||||
->assertDispatched('proxy-configuration-state-changed', pending: false, traefikOutdated: false)
|
||||
->assertDontSee('The saved proxy configuration has not been applied');
|
||||
->assertDontSee('Your configuration changed, please restart the proxy.');
|
||||
});
|
||||
|
||||
test('running proxy hides pending configuration warning when saved settings match applied settings', function () {
|
||||
@@ -123,7 +123,7 @@ test('running proxy hides pending configuration warning when saved settings matc
|
||||
session(['currentTeam' => $team]);
|
||||
|
||||
$component = Livewire::test('server.navbar', ['server' => $server->fresh()])
|
||||
->assertDontSee('The saved proxy configuration has not been applied');
|
||||
->assertDontSee('Your configuration changed, please restart the proxy.');
|
||||
|
||||
$server->refresh();
|
||||
$server->proxy->last_saved_settings = 'new-saved-hash';
|
||||
@@ -132,7 +132,7 @@ test('running proxy hides pending configuration warning when saved settings matc
|
||||
$component->dispatch('refreshServerShow')
|
||||
->assertDispatched('proxy-configuration-state-changed', pending: true, traefikOutdated: false)
|
||||
->assertSee('Changes pending')
|
||||
->assertSee('The saved proxy configuration has not been applied');
|
||||
->assertSee('Your configuration changed, please restart the proxy.');
|
||||
});
|
||||
|
||||
test('admin can stop a proxy while it is starting', function () {
|
||||
|
||||
Reference in New Issue
Block a user