mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 21:18:11 -04:00
Merge branch 'main' into feat/add-sqlite-database
This commit is contained in:
@@ -43,6 +43,19 @@ function traefikSafeServiceNameSegment(string $serviceName): string
|
||||
return $normalized.'-'.traefikServiceNameHash($serviceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Key that Sentinel uses to group Caddy access-log lines. It is the same key that Sentinel gets
|
||||
* from the Traefik router name (`https-{i}-{KEY}@docker`), so both proxies report a resource the same way.
|
||||
* Only [A-Za-z0-9-] can occur, so the value is safe in a Docker label and in a Caddyfile.
|
||||
*/
|
||||
function caddyTrafficAppKey(string $uuid, ?string $serviceName = null): string
|
||||
{
|
||||
// Same truthiness check as fqdnLabelsForTraefik(), so the keys stay equal.
|
||||
$key = $serviceName ? $uuid.'-'.traefikSafeServiceNameSegment($serviceName) : $uuid;
|
||||
|
||||
return (string) preg_replace('/[^A-Za-z0-9-]+/', '-', $key);
|
||||
}
|
||||
|
||||
function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null, ?bool $includePullrequests = false): Collection
|
||||
{
|
||||
$containers = collect([]);
|
||||
@@ -559,6 +572,8 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
|
||||
$hashedPassword = password_hash($http_basic_auth_password, PASSWORD_BCRYPT, ['cost' => 10]);
|
||||
}
|
||||
|
||||
$trafficAppKey = caddyTrafficAppKey($uuid, $service_name);
|
||||
|
||||
foreach ($domains as $loop => $domain) {
|
||||
$url = Url::fromString($domain);
|
||||
$host = $url->getHost();
|
||||
@@ -623,7 +638,7 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
|
||||
$labels->push("caddy_{$loop}.log.format=json");
|
||||
// Only Caddy 2.8+ knows log_append; see Server::caddySupportsLogAppend().
|
||||
if ($supports_log_append) {
|
||||
$labels->push("caddy_{$loop}.log_append=coolify_app_id {$uuid}");
|
||||
$labels->push("caddy_{$loop}.log_append=coolify_app_id {$trafficAppKey}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1510,6 +1510,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||
? ($previewForPorts?->domain_port_overrides ?? [])
|
||||
: ($originalResource->domain_port_overrides ?? []);
|
||||
$onlyPort = firstDockerComposeServicePort($service);
|
||||
$isTrafficAnalyticsEnabled = (bool) $server?->isTrafficAnalyticsEnabled();
|
||||
$supportsLogAppend = (bool) $server?->caddySupportsLogAppend();
|
||||
if (! $use_network_mode && (! $shouldGenerateLabelsExactly || $server->proxyType() === ProxyTypes::TRAEFIK->value)) {
|
||||
$serviceLabels = addTraefikDockerNetworkLabel($serviceLabels, $baseNetwork->first());
|
||||
}
|
||||
@@ -1547,6 +1549,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||
noindex_domains: $noindexDomains,
|
||||
redirect_direction: $redirectDirection,
|
||||
domainPortOverrides: $domainPortOverrides,
|
||||
is_traffic_analytics_enabled: $isTrafficAnalyticsEnabled,
|
||||
supports_log_append: $supportsLogAppend,
|
||||
));
|
||||
break;
|
||||
}
|
||||
@@ -1580,6 +1584,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||
noindex_domains: $noindexDomains,
|
||||
redirect_direction: $redirectDirection,
|
||||
domainPortOverrides: $domainPortOverrides,
|
||||
is_traffic_analytics_enabled: $isTrafficAnalyticsEnabled,
|
||||
supports_log_append: $supportsLogAppend,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -2766,6 +2772,8 @@ function serviceParser(Service $resource): Collection
|
||||
$onlyPort = $originalResource instanceof ServiceApplication
|
||||
? $originalResource->getRequiredPort()
|
||||
: $predefinedPort;
|
||||
$isTrafficAnalyticsEnabled = (bool) $server?->isTrafficAnalyticsEnabled();
|
||||
$supportsLogAppend = (bool) $server?->caddySupportsLogAppend();
|
||||
if (! $use_network_mode && (! $shouldGenerateLabelsExactly || $server->proxyType() === ProxyTypes::TRAEFIK->value)) {
|
||||
$serviceLabels = addTraefikDockerNetworkLabel($serviceLabels, $baseNetwork->first());
|
||||
}
|
||||
@@ -2802,7 +2810,9 @@ function serviceParser(Service $resource): Collection
|
||||
predefinedPort: $onlyPort,
|
||||
domainPortOverrides: $originalResource->domain_port_overrides ?? [],
|
||||
noindex_domains: $noindexDomains,
|
||||
redirect_direction: $redirectDirection
|
||||
redirect_direction: $redirectDirection,
|
||||
is_traffic_analytics_enabled: $isTrafficAnalyticsEnabled,
|
||||
supports_log_append: $supportsLogAppend,
|
||||
));
|
||||
break;
|
||||
}
|
||||
@@ -2835,7 +2845,9 @@ function serviceParser(Service $resource): Collection
|
||||
predefinedPort: $onlyPort,
|
||||
domainPortOverrides: $originalResource->domain_port_overrides ?? [],
|
||||
noindex_domains: $noindexDomains,
|
||||
redirect_direction: $redirectDirection
|
||||
redirect_direction: $redirectDirection,
|
||||
is_traffic_analytics_enabled: $isTrafficAnalyticsEnabled,
|
||||
supports_log_append: $supportsLogAppend,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
+200
-13
@@ -45,43 +45,139 @@ function applyTrafficAnalyticsToProxyConfiguration(Server $server, string $confi
|
||||
return Yaml::dump($config, 12, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server proxy attribute that remembers the user's own `--accesslog*` Traefik flags while
|
||||
* traffic analytics replaces them with the managed set, so disabling can restore them exactly.
|
||||
*/
|
||||
const TRAEFIK_USER_ACCESSLOG_COMMANDS_KEY = 'traffic_analytics_user_accesslog_commands';
|
||||
|
||||
function isTraefikAccessLogCommand(mixed $command): bool
|
||||
{
|
||||
if (! is_string($command)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$flag = strtolower(explode('=', $command, 2)[0]);
|
||||
|
||||
return $flag === '--accesslog' || str_starts_with($flag, '--accesslog.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates the Traefik access log with BusyBox tools from the Alpine base image only (no package
|
||||
* install, no network). Uses copytruncate semantics: Traefik keeps its file handle and Sentinel's
|
||||
* tailer handles the truncation. Keeps at most 5 gzip-compressed rotations (access.log.1.gz..5.gz).
|
||||
* The environment overrides exist for tests; the sidecar does not set them.
|
||||
*/
|
||||
function traefikAccessLogRotationScript(): string
|
||||
{
|
||||
return <<<'SH'
|
||||
log="${TRAEFIK_ACCESS_LOG:-/traefik/access.log}"
|
||||
max_bytes="${TRAEFIK_ACCESS_LOG_MAX_BYTES:-20971520}"
|
||||
interval="${TRAEFIK_ACCESS_LOG_ROTATE_INTERVAL:-60}"
|
||||
keep=5
|
||||
while true; do
|
||||
size=$(stat -c %s "$log" 2>/dev/null || echo 0)
|
||||
if [ "$size" -gt "$max_bytes" ]; then
|
||||
rm -f "$log.$keep.gz"
|
||||
i=$((keep - 1))
|
||||
while [ "$i" -ge 1 ]; do
|
||||
if [ -f "$log.$i.gz" ]; then mv -f "$log.$i.gz" "$log.$((i + 1)).gz"; fi
|
||||
i=$((i - 1))
|
||||
done
|
||||
if cp "$log" "$log.1"; then
|
||||
: > "$log"
|
||||
gzip -f "$log.1"
|
||||
fi
|
||||
fi
|
||||
sleep "$interval"
|
||||
done
|
||||
SH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the user's own access log flags with the managed set while analytics is enabled, and
|
||||
* restore them exactly when it is disabled. Flags that were never added by Coolify are kept.
|
||||
*
|
||||
* @param array<int, mixed> $commands
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
function applyTraefikAccessLogCommands(Server $server, array $commands, bool $enabled): array
|
||||
{
|
||||
$managedCommands = traefikAccessLogCommands(true);
|
||||
$storedUserCommands = $server->proxy->get(TRAEFIK_USER_ACCESSLOG_COMMANDS_KEY);
|
||||
$hasStoredUserCommands = is_array($storedUserCommands);
|
||||
$userCommands = $hasStoredUserCommands ? array_values($storedUserCommands) : [];
|
||||
|
||||
// Managed flags are Coolify's when their user flags were remembered on enable, or (analytics
|
||||
// enabled before flags were remembered) when the complete managed set is present. Otherwise a
|
||||
// matching flag such as `--accesslog=true` belongs to the user and is kept.
|
||||
$managedCommandsAddedByCoolify = $hasStoredUserCommands || array_diff($managedCommands, $commands) === [];
|
||||
if ($managedCommandsAddedByCoolify) {
|
||||
$commands = array_values(array_filter(
|
||||
$commands,
|
||||
fn (mixed $command): bool => ! in_array($command, $managedCommands, true)
|
||||
));
|
||||
}
|
||||
|
||||
if ($enabled) {
|
||||
foreach ($commands as $command) {
|
||||
if (isTraefikAccessLogCommand($command) && ! in_array($command, $userCommands, true)) {
|
||||
$userCommands[] = $command;
|
||||
}
|
||||
}
|
||||
|
||||
$commands = [
|
||||
...array_filter($commands, fn (mixed $command): bool => ! isTraefikAccessLogCommand($command)),
|
||||
...$managedCommands,
|
||||
];
|
||||
|
||||
if ($storedUserCommands !== $userCommands) {
|
||||
$server->proxy->set(TRAEFIK_USER_ACCESSLOG_COMMANDS_KEY, $userCommands);
|
||||
$server->save();
|
||||
}
|
||||
} elseif ($hasStoredUserCommands) {
|
||||
foreach ($userCommands as $command) {
|
||||
if (! in_array($command, $commands, true)) {
|
||||
$commands[] = $command;
|
||||
}
|
||||
}
|
||||
|
||||
$server->proxy->forget(TRAEFIK_USER_ACCESSLOG_COMMANDS_KEY);
|
||||
$server->save();
|
||||
}
|
||||
|
||||
return array_values($commands);
|
||||
}
|
||||
|
||||
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);
|
||||
data_set($config, 'services.traefik.command', applyTraefikAccessLogCommands($server, $commands, $enabled));
|
||||
unset($config['services']['traefik-logrotate']);
|
||||
|
||||
if ($enabled && ! $server->isSwarm() && ! isDev()) {
|
||||
$proxyPath = $server->proxyPath();
|
||||
$config['services']['traefik-logrotate'] = [
|
||||
'container_name' => 'coolify-proxy-logrotate',
|
||||
'image' => 'alpine:3.20',
|
||||
'image' => 'alpine:3.24',
|
||||
'restart' => RESTART_MODE,
|
||||
'network_mode' => 'none',
|
||||
'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\'',
|
||||
// Docker Compose interpolates `$VAR`, so every `$` is escaped as `$$`.
|
||||
'entrypoint' => ['/bin/sh', '-c', str_replace('$', '$$', traefikAccessLogRotationScript())],
|
||||
];
|
||||
}
|
||||
} elseif ($server->proxyType() === ProxyTypes::CADDY->value) {
|
||||
@@ -326,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', [
|
||||
|
||||
@@ -25,7 +25,8 @@ function remote_process(
|
||||
?Model $model = null,
|
||||
bool $ignore_errors = false,
|
||||
$callEventOnFinish = null,
|
||||
$callEventData = null
|
||||
$callEventData = null,
|
||||
array $properties = [],
|
||||
): Activity {
|
||||
$type = $type ?? ActivityTypes::INLINE->value;
|
||||
$command = $command instanceof Collection ? $command->toArray() : $command;
|
||||
@@ -46,6 +47,7 @@ function remote_process(
|
||||
SshMultiplexingHelper::ensureMultiplexedConnection($server);
|
||||
|
||||
$properties = [
|
||||
...$properties,
|
||||
'server_uuid' => $server->uuid,
|
||||
'command' => $command_string,
|
||||
'type' => $type,
|
||||
|
||||
@@ -3191,6 +3191,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
noindex_domains: $noindexDomains,
|
||||
redirect_direction: $redirectDirection,
|
||||
domainPortOverrides: $domainPortOverrides,
|
||||
is_traffic_analytics_enabled: $resource->server?->isTrafficAnalyticsEnabled() ?? false,
|
||||
supports_log_append: $resource->server?->caddySupportsLogAppend() ?? false,
|
||||
));
|
||||
break;
|
||||
}
|
||||
@@ -3224,6 +3226,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
noindex_domains: $noindexDomains,
|
||||
redirect_direction: $redirectDirection,
|
||||
domainPortOverrides: $domainPortOverrides,
|
||||
is_traffic_analytics_enabled: $resource->server?->isTrafficAnalyticsEnabled() ?? false,
|
||||
supports_log_append: $resource->server?->caddySupportsLogAppend() ?? false,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -3995,6 +3999,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
domains: $fqdns,
|
||||
serviceLabels: $serviceLabels,
|
||||
image: data_get($service, 'image'),
|
||||
service_name: $serviceName,
|
||||
is_force_https_enabled: $resource->isForceHttpsEnabled(),
|
||||
is_gzip_enabled: $resource->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $resource->isStripprefixEnabled(),
|
||||
@@ -4002,6 +4007,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
noindex_domains: $noindexDomains,
|
||||
redirect_direction: $redirectDirection,
|
||||
domainPortOverrides: $domainPortOverrides,
|
||||
is_traffic_analytics_enabled: $server?->isTrafficAnalyticsEnabled() ?? false,
|
||||
supports_log_append: $server?->caddySupportsLogAppend() ?? false,
|
||||
)
|
||||
);
|
||||
break;
|
||||
@@ -4030,6 +4037,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
domains: $fqdns,
|
||||
serviceLabels: $serviceLabels,
|
||||
image: data_get($service, 'image'),
|
||||
service_name: $serviceName,
|
||||
is_force_https_enabled: $resource->isForceHttpsEnabled(),
|
||||
is_gzip_enabled: $resource->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $resource->isStripprefixEnabled(),
|
||||
@@ -4037,6 +4045,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
noindex_domains: $noindexDomains,
|
||||
redirect_direction: $redirectDirection,
|
||||
domainPortOverrides: $domainPortOverrides,
|
||||
is_traffic_analytics_enabled: $server?->isTrafficAnalyticsEnabled() ?? false,
|
||||
supports_log_append: $server?->caddySupportsLogAppend() ?? false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user