diff --git a/app/Actions/Proxy/GetProxyConfiguration.php b/app/Actions/Proxy/GetProxyConfiguration.php index de44b476f..159f12252 100644 --- a/app/Actions/Proxy/GetProxyConfiguration.php +++ b/app/Actions/Proxy/GetProxyConfiguration.php @@ -2,10 +2,12 @@ namespace App\Actions\Proxy; +use App\Enums\ProxyTypes; use App\Models\Server; use App\Services\ProxyDashboardCacheService; use Illuminate\Support\Facades\Log; use Lorisleiva\Actions\Concerns\AsAction; +use Symfony\Component\Yaml\Yaml; class GetProxyConfiguration { @@ -24,6 +26,17 @@ public function handle(Server $server, bool $forceRegenerate = false): string // Primary source: database $proxy_configuration = $server->proxy->get('last_saved_proxy_configuration'); + // Validate stored config matches current proxy type + if (! empty(trim($proxy_configuration ?? ''))) { + if (! $this->configMatchesProxyType($proxyType, $proxy_configuration)) { + Log::warning('Stored proxy config does not match current proxy type, will regenerate', [ + 'server_id' => $server->id, + 'proxy_type' => $proxyType, + ]); + $proxy_configuration = null; + } + } + // Backfill: existing servers may not have DB config yet — read from disk once if (empty(trim($proxy_configuration ?? ''))) { $proxy_configuration = $this->backfillFromDisk($server); @@ -55,6 +68,29 @@ public function handle(Server $server, bool $forceRegenerate = false): string return $proxy_configuration; } + /** + * Check that the stored docker-compose YAML contains the expected service + * for the server's current proxy type. Returns false if the config belongs + * to a different proxy type (e.g. Traefik config on a CADDY server). + */ + private function configMatchesProxyType(string $proxyType, string $configuration): bool + { + try { + $yaml = Yaml::parse($configuration); + $services = data_get($yaml, 'services', []); + + return match ($proxyType) { + ProxyTypes::TRAEFIK->value => isset($services['traefik']), + ProxyTypes::CADDY->value => isset($services['caddy']), + ProxyTypes::NGINX->value => isset($services['nginx']), + default => true, + }; + } catch (\Throwable $e) { + // If YAML is unparseable, don't block — let the existing flow handle it + return true; + } + } + /** * Backfill: read config from disk for servers that predate DB storage. * Stores the result in the database so future reads skip SSH entirely. diff --git a/app/Models/Server.php b/app/Models/Server.php index 527c744a5..ce877bd20 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -1471,6 +1471,9 @@ public function changeProxy(string $proxyType, bool $async = true) if ($validProxyTypes->contains(str($proxyType)->lower())) { $this->proxy->set('type', str($proxyType)->upper()); $this->proxy->set('status', 'exited'); + $this->proxy->set('last_saved_proxy_configuration', null); + $this->proxy->set('last_saved_settings', null); + $this->proxy->set('last_applied_settings', null); $this->save(); if ($this->proxySet()) { if ($async) { diff --git a/tests/Unit/ProxyConfigRecoveryTest.php b/tests/Unit/ProxyConfigRecoveryTest.php index 219ec9bca..e10d899fe 100644 --- a/tests/Unit/ProxyConfigRecoveryTest.php +++ b/tests/Unit/ProxyConfigRecoveryTest.php @@ -10,20 +10,26 @@ Cache::spy(); }); -function mockServerWithDbConfig(?string $savedConfig): object +function mockServerWithDbConfig(?string $savedConfig, string $proxyType = 'TRAEFIK'): object { $proxyAttributes = Mockery::mock(SchemalessAttributes::class); $proxyAttributes->shouldReceive('get') ->with('last_saved_proxy_configuration') ->andReturn($savedConfig); + $proxyPath = match ($proxyType) { + 'CADDY' => '/data/coolify/proxy/caddy', + 'NGINX' => '/data/coolify/proxy/nginx', + default => '/data/coolify/proxy/', + }; + $server = Mockery::mock('App\Models\Server'); $server->shouldIgnoreMissing(); $server->shouldReceive('getAttribute')->with('proxy')->andReturn($proxyAttributes); $server->shouldReceive('getAttribute')->with('id')->andReturn(1); $server->shouldReceive('getAttribute')->with('name')->andReturn('Test Server'); - $server->shouldReceive('proxyType')->andReturn('TRAEFIK'); - $server->shouldReceive('proxyPath')->andReturn('/data/coolify/proxy'); + $server->shouldReceive('proxyType')->andReturn($proxyType); + $server->shouldReceive('proxyPath')->andReturn($proxyPath); return $server; } @@ -107,3 +113,61 @@ function mockServerWithDbConfig(?string $savedConfig): object expect($result)->toBe($savedConfig); }); + +it('rejects stored Traefik config when proxy type is CADDY', function () { + Log::swap(new \Illuminate\Log\LogManager(app())); + Log::spy(); + + $traefikConfig = "services:\n traefik:\n image: traefik:v3.6\n"; + $server = mockServerWithDbConfig($traefikConfig, 'CADDY'); + + // Config type mismatch should trigger regeneration, which will try + // backfillFromDisk (instant_remote_process) then generateDefault. + // Both will fail in test env, but the warning log proves mismatch was detected. + try { + GetProxyConfiguration::run($server); + } catch (\Throwable $e) { + // Expected — regeneration requires SSH/full server setup + } + + Log::shouldHaveReceived('warning') + ->withArgs(fn ($message) => str_contains($message, 'does not match current proxy type')) + ->once(); +}); + +it('rejects stored Caddy config when proxy type is TRAEFIK', function () { + Log::swap(new \Illuminate\Log\LogManager(app())); + Log::spy(); + + $caddyConfig = "services:\n caddy:\n image: lucaslorentz/caddy-docker-proxy:2.8-alpine\n"; + $server = mockServerWithDbConfig($caddyConfig, 'TRAEFIK'); + + try { + GetProxyConfiguration::run($server); + } catch (\Throwable $e) { + // Expected — regeneration requires SSH/full server setup + } + + Log::shouldHaveReceived('warning') + ->withArgs(fn ($message) => str_contains($message, 'does not match current proxy type')) + ->once(); +}); + +it('accepts stored Caddy config when proxy type is CADDY', function () { + $caddyConfig = "services:\n caddy:\n image: lucaslorentz/caddy-docker-proxy:2.8-alpine\n"; + $server = mockServerWithDbConfig($caddyConfig, 'CADDY'); + + $result = GetProxyConfiguration::run($server); + + expect($result)->toBe($caddyConfig); +}); + +it('accepts stored config when YAML parsing fails', function () { + $invalidYaml = "this: is: not: [valid yaml: {{{}}}"; + $server = mockServerWithDbConfig($invalidYaml, 'TRAEFIK'); + + // Invalid YAML should not block — configMatchesProxyType returns true on parse failure + $result = GetProxyConfiguration::run($server); + + expect($result)->toBe($invalidYaml); +});