- Added a new function to extract custom proxy commands from existing Traefik configurations before regenerating the proxy configuration. - Updated the proxy configuration generation logic to include these custom commands, ensuring they are preserved during regeneration. - Introduced unit tests to validate the extraction of custom commands and handle various scenarios, including invalid YAML and different proxy types.
336 lines
13 KiB
PHP
336 lines
13 KiB
PHP
<?php
|
|
|
|
use App\Actions\Proxy\SaveProxyConfiguration;
|
|
use App\Enums\ProxyTypes;
|
|
use App\Models\Application;
|
|
use App\Models\Server;
|
|
use Symfony\Component\Yaml\Yaml;
|
|
|
|
function collectProxyDockerNetworksByServer(Server $server)
|
|
{
|
|
if (! $server->isFunctional()) {
|
|
return collect();
|
|
}
|
|
$proxyType = $server->proxyType();
|
|
if (is_null($proxyType) || $proxyType === 'NONE') {
|
|
return collect();
|
|
}
|
|
$networks = instant_remote_process(['docker inspect --format="{{json .NetworkSettings.Networks }}" coolify-proxy'], $server, false);
|
|
|
|
return collect($networks)->map(function ($network) {
|
|
return collect(json_decode($network))->keys();
|
|
})->flatten()->unique();
|
|
}
|
|
function collectDockerNetworksByServer(Server $server)
|
|
{
|
|
$allNetworks = collect([]);
|
|
if ($server->isSwarm()) {
|
|
$networks = collect($server->swarmDockers)->map(function ($docker) {
|
|
return $docker['network'];
|
|
});
|
|
} else {
|
|
// Standalone networks
|
|
$networks = collect($server->standaloneDockers)->map(function ($docker) {
|
|
return $docker['network'];
|
|
});
|
|
}
|
|
$allNetworks = $allNetworks->merge($networks);
|
|
// Service networks
|
|
foreach ($server->services()->get() as $service) {
|
|
if ($service->isRunning()) {
|
|
$networks->push($service->networks());
|
|
}
|
|
$allNetworks->push($service->networks());
|
|
}
|
|
// Docker compose based apps
|
|
$docker_compose_apps = $server->dockerComposeBasedApplications();
|
|
foreach ($docker_compose_apps as $app) {
|
|
if ($app->isRunning()) {
|
|
$networks->push($app->uuid);
|
|
}
|
|
$allNetworks->push($app->uuid);
|
|
}
|
|
// Docker compose based preview deployments
|
|
$docker_compose_previews = $server->dockerComposeBasedPreviewDeployments();
|
|
foreach ($docker_compose_previews as $preview) {
|
|
if (! $preview->isRunning()) {
|
|
continue;
|
|
}
|
|
$pullRequestId = $preview->pull_request_id;
|
|
$applicationId = $preview->application_id;
|
|
$application = Application::find($applicationId);
|
|
if (! $application) {
|
|
continue;
|
|
}
|
|
$network = "{$application->uuid}-{$pullRequestId}";
|
|
$networks->push($network);
|
|
$allNetworks->push($network);
|
|
}
|
|
$networks = collect($networks)->flatten()->unique();
|
|
$allNetworks = $allNetworks->flatten()->unique();
|
|
if ($server->isSwarm()) {
|
|
if ($networks->count() === 0) {
|
|
$networks = collect(['coolify-overlay']);
|
|
$allNetworks = collect(['coolify-overlay']);
|
|
}
|
|
} else {
|
|
if ($networks->count() === 0) {
|
|
$networks = collect(['coolify']);
|
|
$allNetworks = collect(['coolify']);
|
|
}
|
|
}
|
|
|
|
return [
|
|
'networks' => $networks,
|
|
'allNetworks' => $allNetworks,
|
|
];
|
|
}
|
|
function connectProxyToNetworks(Server $server)
|
|
{
|
|
['networks' => $networks] = collectDockerNetworksByServer($server);
|
|
if ($server->isSwarm()) {
|
|
$commands = $networks->map(function ($network) {
|
|
return [
|
|
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --driver overlay --attachable $network >/dev/null",
|
|
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
|
|
"echo 'Successfully connected coolify-proxy to $network network.'",
|
|
];
|
|
});
|
|
} else {
|
|
$commands = $networks->map(function ($network) {
|
|
return [
|
|
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null",
|
|
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
|
|
"echo 'Successfully connected coolify-proxy to $network network.'",
|
|
];
|
|
});
|
|
}
|
|
|
|
return $commands->flatten();
|
|
}
|
|
function extractCustomProxyCommands(Server $server, string $existing_config): array
|
|
{
|
|
$custom_commands = [];
|
|
$proxy_type = $server->proxyType();
|
|
|
|
if ($proxy_type !== ProxyTypes::TRAEFIK->value || empty($existing_config)) {
|
|
return $custom_commands;
|
|
}
|
|
|
|
try {
|
|
$yaml = Yaml::parse($existing_config);
|
|
$existing_commands = data_get($yaml, 'services.traefik.command', []);
|
|
|
|
if (empty($existing_commands)) {
|
|
return $custom_commands;
|
|
}
|
|
|
|
// Define default commands that Coolify generates
|
|
$default_command_prefixes = [
|
|
'--ping=',
|
|
'--api.',
|
|
'--entrypoints.http.address=',
|
|
'--entrypoints.https.address=',
|
|
'--entrypoints.http.http.encodequerysemicolons=',
|
|
'--entryPoints.http.http2.maxConcurrentStreams=',
|
|
'--entrypoints.https.http.encodequerysemicolons=',
|
|
'--entryPoints.https.http2.maxConcurrentStreams=',
|
|
'--entrypoints.https.http3',
|
|
'--providers.file.',
|
|
'--certificatesresolvers.',
|
|
'--providers.docker',
|
|
'--providers.swarm',
|
|
'--log.level=',
|
|
'--accesslog.',
|
|
];
|
|
|
|
// Extract commands that don't match default prefixes (these are custom)
|
|
foreach ($existing_commands as $command) {
|
|
$is_default = false;
|
|
foreach ($default_command_prefixes as $prefix) {
|
|
if (str_starts_with($command, $prefix)) {
|
|
$is_default = true;
|
|
break;
|
|
}
|
|
}
|
|
if (! $is_default) {
|
|
$custom_commands[] = $command;
|
|
}
|
|
}
|
|
} catch (\Exception $e) {
|
|
// If we can't parse the config, return empty array
|
|
// Silently fail to avoid breaking the proxy regeneration
|
|
}
|
|
|
|
return $custom_commands;
|
|
}
|
|
function generateDefaultProxyConfiguration(Server $server, array $custom_commands = [])
|
|
{
|
|
$proxy_path = $server->proxyPath();
|
|
$proxy_type = $server->proxyType();
|
|
|
|
if ($server->isSwarm()) {
|
|
$networks = collect($server->swarmDockers)->map(function ($docker) {
|
|
return $docker['network'];
|
|
})->unique();
|
|
if ($networks->count() === 0) {
|
|
$networks = collect(['coolify-overlay']);
|
|
}
|
|
} else {
|
|
$networks = collect($server->standaloneDockers)->map(function ($docker) {
|
|
return $docker['network'];
|
|
})->unique();
|
|
if ($networks->count() === 0) {
|
|
$networks = collect(['coolify']);
|
|
}
|
|
}
|
|
|
|
$array_of_networks = collect([]);
|
|
$filtered_networks = collect([]);
|
|
$networks->map(function ($network) use ($array_of_networks, $filtered_networks) {
|
|
if ($network === 'host') {
|
|
return; // network-scoped alias is supported only for containers in user defined networks
|
|
}
|
|
|
|
$array_of_networks[$network] = [
|
|
'external' => true,
|
|
];
|
|
$filtered_networks->push($network);
|
|
});
|
|
if ($proxy_type === ProxyTypes::TRAEFIK->value) {
|
|
$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',
|
|
];
|
|
$config = [
|
|
'name' => 'coolify-proxy',
|
|
'networks' => $array_of_networks->toArray(),
|
|
'services' => [
|
|
'traefik' => [
|
|
'container_name' => 'coolify-proxy',
|
|
'image' => 'traefik:v3.1',
|
|
'restart' => RESTART_MODE,
|
|
'extra_hosts' => [
|
|
'host.docker.internal:host-gateway',
|
|
],
|
|
'networks' => $filtered_networks->toArray(),
|
|
'ports' => [
|
|
'80:80',
|
|
'443:443',
|
|
'443:443/udp',
|
|
'8080:8080',
|
|
],
|
|
'healthcheck' => [
|
|
'test' => 'wget -qO- http://localhost:80/ping || exit 1',
|
|
'interval' => '4s',
|
|
'timeout' => '2s',
|
|
'retries' => 5,
|
|
],
|
|
'volumes' => [
|
|
'/var/run/docker.sock:/var/run/docker.sock:ro',
|
|
|
|
],
|
|
'command' => [
|
|
'--ping=true',
|
|
'--ping.entrypoint=http',
|
|
'--api.dashboard=true',
|
|
'--entrypoints.http.address=:80',
|
|
'--entrypoints.https.address=:443',
|
|
'--entrypoints.http.http.encodequerysemicolons=true',
|
|
'--entryPoints.http.http2.maxConcurrentStreams=250',
|
|
'--entrypoints.https.http.encodequerysemicolons=true',
|
|
'--entryPoints.https.http2.maxConcurrentStreams=250',
|
|
'--entrypoints.https.http3',
|
|
'--providers.file.directory=/traefik/dynamic/',
|
|
'--providers.file.watch=true',
|
|
'--certificatesresolvers.letsencrypt.acme.httpchallenge=true',
|
|
'--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http',
|
|
'--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json',
|
|
],
|
|
'labels' => $labels,
|
|
],
|
|
],
|
|
];
|
|
if (isDev()) {
|
|
$config['services']['traefik']['command'][] = '--api.insecure=true';
|
|
$config['services']['traefik']['command'][] = '--log.level=debug';
|
|
$config['services']['traefik']['command'][] = '--accesslog.filepath=/traefik/access.log';
|
|
$config['services']['traefik']['command'][] = '--accesslog.bufferingsize=100';
|
|
$config['services']['traefik']['volumes'][] = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/:/traefik';
|
|
} else {
|
|
$config['services']['traefik']['command'][] = '--api.insecure=false';
|
|
$config['services']['traefik']['volumes'][] = "{$proxy_path}:/traefik";
|
|
}
|
|
if ($server->isSwarm()) {
|
|
data_forget($config, 'services.traefik.container_name');
|
|
data_forget($config, 'services.traefik.restart');
|
|
data_forget($config, 'services.traefik.labels');
|
|
|
|
$config['services']['traefik']['command'][] = '--providers.swarm.endpoint=unix:///var/run/docker.sock';
|
|
$config['services']['traefik']['command'][] = '--providers.swarm.exposedbydefault=false';
|
|
$config['services']['traefik']['deploy'] = [
|
|
'labels' => $labels,
|
|
'placement' => [
|
|
'constraints' => [
|
|
'node.role==manager',
|
|
],
|
|
],
|
|
];
|
|
} else {
|
|
$config['services']['traefik']['command'][] = '--providers.docker=true';
|
|
$config['services']['traefik']['command'][] = '--providers.docker.exposedbydefault=false';
|
|
}
|
|
|
|
// Append custom commands (e.g., trustedIPs for Cloudflare)
|
|
if (! empty($custom_commands)) {
|
|
foreach ($custom_commands as $custom_command) {
|
|
$config['services']['traefik']['command'][] = $custom_command;
|
|
}
|
|
}
|
|
} elseif ($proxy_type === 'CADDY') {
|
|
$config = [
|
|
'networks' => $array_of_networks->toArray(),
|
|
'services' => [
|
|
'caddy' => [
|
|
'container_name' => 'coolify-proxy',
|
|
'image' => 'lucaslorentz/caddy-docker-proxy:2.8-alpine',
|
|
'restart' => RESTART_MODE,
|
|
'extra_hosts' => [
|
|
'host.docker.internal:host-gateway',
|
|
],
|
|
'environment' => [
|
|
'CADDY_DOCKER_POLLING_INTERVAL=5s',
|
|
'CADDY_DOCKER_CADDYFILE_PATH=/dynamic/Caddyfile',
|
|
],
|
|
'networks' => $filtered_networks->toArray(),
|
|
'ports' => [
|
|
'80:80',
|
|
'443:443',
|
|
'443:443/udp',
|
|
],
|
|
'labels' => [
|
|
'coolify.managed=true',
|
|
'coolify.proxy=true',
|
|
],
|
|
'volumes' => [
|
|
'/var/run/docker.sock:/var/run/docker.sock:ro',
|
|
"{$proxy_path}/dynamic:/dynamic",
|
|
"{$proxy_path}/config:/config",
|
|
"{$proxy_path}/data:/data",
|
|
],
|
|
],
|
|
],
|
|
];
|
|
} else {
|
|
return null;
|
|
}
|
|
|
|
$config = Yaml::dump($config, 12, 2);
|
|
SaveProxyConfiguration::run($server, $config);
|
|
|
|
return $config;
|
|
}
|