mirror of
https://github.com/coollabsio/coolify.git
synced 2026-09-26 09:20:54 -04:00
fix(proxy): preserve custom config when toggling traffic analytics
Apply analytics settings to existing proxy configurations while retaining custom options, including managed Traefik log rotation and Caddy traffic volumes. Upgrade new Traefik configurations to v3.7.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Actions\Proxy\GetProxyConfiguration;
|
||||
use App\Actions\Proxy\SaveProxyConfiguration;
|
||||
use App\Jobs\RestartProxyJob;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
@@ -19,8 +20,9 @@ class ConfigureTrafficAnalytics
|
||||
$server->settings->save();
|
||||
$server->refresh();
|
||||
|
||||
// Regenerate proxy config so the (Traefik) access-log flags / (Caddy) log labels take effect.
|
||||
GetProxyConfiguration::run($server, forceRegenerate: true);
|
||||
$configuration = GetProxyConfiguration::run($server);
|
||||
$configuration = applyTrafficAnalyticsToProxyConfiguration($server, $configuration);
|
||||
SaveProxyConfiguration::run($server, $configuration);
|
||||
RestartProxyJob::dispatch($server);
|
||||
|
||||
// Recreate Sentinel so it picks up (enabling) or drops (disabling) the traffic env + proxy-log mount.
|
||||
|
||||
+72
-26
@@ -31,6 +31,76 @@ function traefikAccessLogCommands(bool $enabled): array
|
||||
];
|
||||
}
|
||||
|
||||
function applyTrafficAnalyticsToProxyConfiguration(Server $server, string $configuration): string
|
||||
{
|
||||
$config = Yaml::parse($configuration);
|
||||
|
||||
if (! is_array($config)) {
|
||||
throw new RuntimeException('Proxy configuration must be a YAML mapping.');
|
||||
}
|
||||
|
||||
$config = applyTrafficAnalyticsToProxyConfigArray($server, $config);
|
||||
|
||||
return Yaml::dump($config, 12, 2);
|
||||
}
|
||||
|
||||
function applyTrafficAnalyticsToProxyConfigArray(Server $server, array $config): array
|
||||
{
|
||||
$enabled = $server->isTrafficAnalyticsEnabled();
|
||||
|
||||
if ($server->proxyType() === ProxyTypes::TRAEFIK->value) {
|
||||
$managedCommands = traefikAccessLogCommands(true);
|
||||
$commands = data_get($config, 'services.traefik.command', []);
|
||||
|
||||
if (! is_array($commands)) {
|
||||
throw new RuntimeException('Traefik commands must be a YAML list.');
|
||||
}
|
||||
|
||||
$commands = array_values(array_filter(
|
||||
$commands,
|
||||
fn (mixed $command): bool => ! in_array($command, $managedCommands, true)
|
||||
));
|
||||
|
||||
if ($enabled) {
|
||||
$commands = [...$commands, ...$managedCommands];
|
||||
}
|
||||
|
||||
data_set($config, 'services.traefik.command', $commands);
|
||||
unset($config['services']['traefik-logrotate']);
|
||||
|
||||
if ($enabled && ! $server->isSwarm() && ! isDev()) {
|
||||
$proxyPath = $server->proxyPath();
|
||||
$config['services']['traefik-logrotate'] = [
|
||||
'image' => 'alpine:3.20',
|
||||
'restart' => RESTART_MODE,
|
||||
'volumes' => [
|
||||
"{$proxyPath}:/traefik",
|
||||
],
|
||||
'labels' => [
|
||||
'coolify.managed=true',
|
||||
],
|
||||
'entrypoint' => 'sh -c \'apk add --no-cache logrotate >/dev/null 2>&1; printf "/traefik/access.log {\n copytruncate\n size 20M\n rotate 5\n compress\n missingok\n notifempty\n}\n" > /etc/logrotate.d/traefik-access; while true; do logrotate -s /traefik/.logrotate.state /etc/logrotate.d/traefik-access; sleep 3600; done\'',
|
||||
];
|
||||
}
|
||||
} elseif ($server->proxyType() === ProxyTypes::CADDY->value) {
|
||||
$trafficVolume = $server->proxyPath().':/traffic';
|
||||
$volumes = data_get($config, 'services.caddy.volumes', []);
|
||||
|
||||
if (! is_array($volumes)) {
|
||||
throw new RuntimeException('Caddy volumes must be a YAML list.');
|
||||
}
|
||||
|
||||
$volumes = array_values(array_filter($volumes, fn (mixed $volume): bool => $volume !== $trafficVolume));
|
||||
if ($enabled) {
|
||||
$volumes[] = $trafficVolume;
|
||||
}
|
||||
|
||||
data_set($config, 'services.caddy.volumes', $volumes);
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a network name is a Docker predefined system network.
|
||||
* These networks cannot be created, modified, or managed by docker network commands.
|
||||
@@ -302,7 +372,7 @@ function generateDefaultProxyConfiguration(Server $server, array $custom_command
|
||||
'services' => [
|
||||
'traefik' => [
|
||||
'container_name' => 'coolify-proxy',
|
||||
'image' => 'traefik:v3.6',
|
||||
'image' => 'traefik:v3.7',
|
||||
'restart' => RESTART_MODE,
|
||||
'extra_hosts' => [
|
||||
'host.docker.internal:host-gateway',
|
||||
@@ -354,11 +424,6 @@ function generateDefaultProxyConfiguration(Server $server, array $custom_command
|
||||
$config['services']['traefik']['command'][] = '--api.insecure=false';
|
||||
$config['services']['traefik']['volumes'][] = "{$proxy_path}:/traefik";
|
||||
}
|
||||
// Access logging + analytics header capture (JSON log, real-IP/UA/referrer headers)
|
||||
// applies to both dev and production so traffic analytics can be exercised locally.
|
||||
foreach (traefikAccessLogCommands($server->isTrafficAnalyticsEnabled()) as $cmd) {
|
||||
$config['services']['traefik']['command'][] = $cmd;
|
||||
}
|
||||
if ($server->isSwarm()) {
|
||||
data_forget($config, 'services.traefik.container_name');
|
||||
data_forget($config, 'services.traefik.restart');
|
||||
@@ -386,23 +451,6 @@ function generateDefaultProxyConfiguration(Server $server, array $custom_command
|
||||
}
|
||||
}
|
||||
|
||||
// Traefik has no native access-log rotation. Add a minimal logrotate sidecar that
|
||||
// rotates /traefik/access.log in copytruncate mode so the file keeps the same inode
|
||||
// and Sentinel keeps its file handle (the tailer handles len < pos by seeking to 0).
|
||||
// Only for the non-swarm, non-dev production path (dev uses a different access-log path).
|
||||
if ($server->isTrafficAnalyticsEnabled() && ! $server->isSwarm() && ! isDev()) {
|
||||
$config['services']['traefik-logrotate'] = [
|
||||
'image' => 'alpine:3.20',
|
||||
'restart' => RESTART_MODE,
|
||||
'volumes' => [
|
||||
"{$proxy_path}:/traefik",
|
||||
],
|
||||
'labels' => [
|
||||
'coolify.managed=true',
|
||||
],
|
||||
'entrypoint' => 'sh -c \'apk add --no-cache logrotate >/dev/null 2>&1; printf "/traefik/access.log {\n copytruncate\n size 20M\n rotate 5\n compress\n missingok\n notifempty\n}\n" > /etc/logrotate.d/traefik-access; while true; do logrotate -s /traefik/.logrotate.state /etc/logrotate.d/traefik-access; sleep 3600; done\'',
|
||||
];
|
||||
}
|
||||
} elseif ($proxy_type === 'CADDY') {
|
||||
$config = [
|
||||
'networks' => $array_of_networks->toArray(),
|
||||
@@ -437,13 +485,11 @@ function generateDefaultProxyConfiguration(Server $server, array $custom_command
|
||||
],
|
||||
],
|
||||
];
|
||||
if ($server->isTrafficAnalyticsEnabled()) {
|
||||
$config['services']['caddy']['volumes'][] = "{$proxy_path}:/traffic";
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
$config = applyTrafficAnalyticsToProxyConfigArray($server, $config);
|
||||
$config = Yaml::dump($config, 12, 2);
|
||||
SaveProxyConfiguration::run($server, $config);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Actions\Proxy\GetProxyConfiguration;
|
||||
use App\Actions\Proxy\SaveProxyConfiguration;
|
||||
use App\Actions\Server\ConfigureTrafficAnalytics;
|
||||
use App\Actions\Server\StartSentinel;
|
||||
use App\Jobs\RestartProxyJob;
|
||||
@@ -16,12 +17,27 @@ beforeEach(function () {
|
||||
$this->team = $user->teams()->first();
|
||||
});
|
||||
|
||||
it('enables analytics, regenerates proxy config and recreates sentinel', function () {
|
||||
it('enables analytics without replacing custom proxy configuration', function () {
|
||||
Queue::fake();
|
||||
StartSentinel::partialMock()->shouldReceive('handle')->atLeast()->once();
|
||||
GetProxyConfiguration::partialMock()->shouldReceive('handle')->atLeast()->once();
|
||||
GetProxyConfiguration::partialMock()->shouldReceive('handle')->once()->andReturn(<<<'YAML'
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v3.7
|
||||
env_file:
|
||||
- .env
|
||||
command:
|
||||
- '--providers.docker=true'
|
||||
YAML);
|
||||
SaveProxyConfiguration::partialMock()->shouldReceive('handle')->once()->withArgs(
|
||||
fn (Server $server, string $configuration): bool => str_contains($configuration, 'traefik:v3.7')
|
||||
&& str_contains($configuration, 'env_file:')
|
||||
&& str_contains($configuration, '--accesslog=true')
|
||||
);
|
||||
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$server->proxy->set('type', 'TRAEFIK');
|
||||
$server->save();
|
||||
ConfigureTrafficAnalytics::run($server, true);
|
||||
|
||||
expect($server->fresh()->isTrafficAnalyticsEnabled())->toBeTrue();
|
||||
@@ -31,9 +47,12 @@ it('enables analytics, regenerates proxy config and recreates sentinel', functio
|
||||
it('disables analytics', function () {
|
||||
Queue::fake();
|
||||
StartSentinel::partialMock()->shouldReceive('handle')->atLeast()->once();
|
||||
GetProxyConfiguration::partialMock()->shouldReceive('handle')->atLeast()->once();
|
||||
GetProxyConfiguration::partialMock()->shouldReceive('handle')->twice()->andReturn("services:\n traefik:\n command: []\n");
|
||||
SaveProxyConfiguration::partialMock()->shouldReceive('handle')->twice();
|
||||
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$server->proxy->set('type', 'TRAEFIK');
|
||||
$server->save();
|
||||
ConfigureTrafficAnalytics::run($server, true);
|
||||
ConfigureTrafficAnalytics::run($server, false);
|
||||
|
||||
|
||||
@@ -21,6 +21,16 @@ beforeEach(function () {
|
||||
$this->privateKey = PrivateKey::factory()->create(['team_id' => $this->team->id]);
|
||||
});
|
||||
|
||||
it('uses the latest stable traefik branch for new proxy configurations', function () {
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id, 'private_key_id' => $this->privateKey->id]);
|
||||
$server->proxy->set('type', 'TRAEFIK');
|
||||
$server->save();
|
||||
|
||||
$config = Yaml::parse(generateDefaultProxyConfiguration($server->fresh()));
|
||||
|
||||
expect($config['services']['traefik']['image'])->toBe('traefik:v3.7');
|
||||
});
|
||||
|
||||
it('does not add a traefik-logrotate sidecar when traffic analytics is disabled', function () {
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id, 'private_key_id' => $this->privateKey->id]);
|
||||
$server->proxy->set('type', 'TRAEFIK');
|
||||
|
||||
Reference in New Issue
Block a user