diff --git a/.ai/lessons.md b/.ai/lessons.md index 84d6bc1992..5cde3ef34e 100644 --- a/.ai/lessons.md +++ b/.ai/lessons.md @@ -101,3 +101,7 @@ - In remote commands, use absolute paths (`docker compose -f /docker-compose.yml`, `--project-directory `), `echo ... | tee > /dev/null`, and `find` instead of globs. scp also runs as the SSH user; stage files outside `/data/coolify`. - The parsers add sudo only to line starts (and after `&&`, `||`, `|`, `$(`). Put `if`/`else` branches on their own lines. When a redirect or `cd` must stay, make the whole line one `sh -c '...'` script; it runs as `sudo sh -c` without inner sudo or bash. - Tests that replace `DatabaseStartCommandExecutor` or use root servers miss non-root bugs. Test the parsed commands of a non-root server. + +## Cached and stored state +- `StandaloneDocker`/`SwarmDocker` `server` uses `Server::findCached()`, a per-process identity map that is flushed only when the `servers` row updates. A `ServerSetting` save does not flush it, so pass the current `Server` instance (`setRelation('server', $server)`) when generated output depends on just-saved settings. +- Application deployments use the stored `custom_labels` when they exist. When a server-level setting changes the generated labels, regenerate the stored labels of read-only (`is_container_label_readonly_enabled`) applications, or a redeploy keeps the old labels. diff --git a/app/Actions/Server/ConfigureTrafficAnalytics.php b/app/Actions/Server/ConfigureTrafficAnalytics.php index 287df678a1..78f85689c4 100644 --- a/app/Actions/Server/ConfigureTrafficAnalytics.php +++ b/app/Actions/Server/ConfigureTrafficAnalytics.php @@ -4,7 +4,9 @@ namespace App\Actions\Server; use App\Actions\Proxy\GetProxyConfiguration; use App\Actions\Proxy\SaveProxyConfiguration; +use App\Enums\ProxyTypes; use App\Jobs\RestartProxyJob; +use App\Models\Application; use App\Models\Server; use App\Services\ProxyPortParser; use Lorisleiva\Actions\Concerns\AsAction; @@ -41,6 +43,10 @@ class ConfigureTrafficAnalytics $server->settings->save(); $server->refresh(); + if ($server->proxyType() === ProxyTypes::CADDY->value) { + $this->regenerateReadonlyApplicationLabels($server); + } + // A proxy the user stopped stays stopped; its next start applies the saved configuration. $restartProxy = $hasProxy && ! $this->proxyIsStopped($server); if ($restartProxy) { @@ -79,6 +85,23 @@ class ConfigureTrafficAnalytics } } + /** + * Caddy logs a site only when its labels contain the log directives. Deployments use the stored labels, + * so read-only (Coolify-managed) labels must be regenerated, or a redeploy keeps the old logging state. + */ + private function regenerateReadonlyApplicationLabels(Server $server): void + { + $server->applications() + ->filter(fn (Application $application): bool => (bool) $application->settings?->is_container_label_readonly_enabled + && $application->destination?->server_id === $server->id) + ->each(function (Application $application) use ($server): void { + // The cached server of the destination can still have the old analytics setting. + $application->destination->setRelation('server', $server); + $application->custom_labels = base64_encode(implode("\n", generateLabelsApplication($application))); + $application->save(); + }); + } + private function proxyIsStopped(Server $server): bool { return (bool) $server->proxy->get('force_stop') diff --git a/tests/Feature/TrafficAnalytics/ConfigureTrafficAnalyticsTest.php b/tests/Feature/TrafficAnalytics/ConfigureTrafficAnalyticsTest.php index 765f4aa5dc..36d17cd39c 100644 --- a/tests/Feature/TrafficAnalytics/ConfigureTrafficAnalyticsTest.php +++ b/tests/Feature/TrafficAnalytics/ConfigureTrafficAnalyticsTest.php @@ -5,7 +5,11 @@ use App\Actions\Proxy\SaveProxyConfiguration; use App\Actions\Server\ConfigureTrafficAnalytics; use App\Actions\Server\StartSentinel; use App\Jobs\RestartProxyJob; +use App\Models\Application; +use App\Models\Environment; +use App\Models\Project; use App\Models\Server; +use App\Models\StandaloneDocker; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Queue; @@ -184,3 +188,37 @@ it('restarts a running proxy to apply the configuration', function (string $prox expect($server->fresh()->isTrafficAnalyticsEnabled())->toBeTrue(); Queue::assertPushed(RestartProxyJob::class); })->with(['TRAEFIK', 'CADDY']); + +it('regenerates the stored Caddy labels of applications with read-only labels', function () { + Queue::fake(); + StartSentinel::partialMock()->shouldReceive('handle'); + GetProxyConfiguration::partialMock()->shouldReceive('handle')->andReturn("services:\n caddy:\n volumes: []\n"); + SaveProxyConfiguration::partialMock()->shouldReceive('handle'); + + $server = trafficAnalyticsProxyServer($this, 'CADDY'); + $environment = Environment::factory()->create(['project_id' => Project::factory()->create(['team_id' => $this->team->id])->id]); + $destination = StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail(); + $createApplication = fn (): Application => Application::factory()->createOne([ + 'environment_id' => $environment->id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + 'fqdn' => 'https://example.com', + ]); + $readonly = $createApplication(); + $readonly->update(['custom_labels' => base64_encode(implode("\n", generateLabelsApplication($readonly->fresh())))]); + $editable = $createApplication(); + $editable->settings->update(['is_container_label_readonly_enabled' => false]); + $editable->update(['custom_labels' => base64_encode('caddy_0=https://custom.example.com')]); + $logsTraffic = fn (Application $application): bool => str_contains(base64_decode($application->fresh()->custom_labels), 'log.output=file /traffic/access.log'); + + expect($logsTraffic($readonly))->toBeFalse(); + + ConfigureTrafficAnalytics::run($server, true); + + expect($logsTraffic($readonly))->toBeTrue() + ->and(base64_decode($editable->fresh()->custom_labels))->toBe('caddy_0=https://custom.example.com'); + + ConfigureTrafficAnalytics::run($server->fresh(), false); + + expect($logsTraffic($readonly))->toBeFalse(); +});