fix(domains): use Compose service ports for internal routing
Detect Docker Compose service ports for domain internal ports and proxy labels, with application ports as a fallback.
This commit is contained in:
parent
1d0d6b4a79
commit
986ece457d
7 changed files with 129 additions and 6 deletions
|
|
@ -500,7 +500,7 @@ protected function domainRowFromStored(string $url, ?string $service, array $sto
|
|||
{
|
||||
$key = $this->domainDnsStatusKey($url, $service);
|
||||
$entry = $stored[$key] ?? null;
|
||||
$port = $this->effectiveDomainInternalPort($url);
|
||||
$port = $this->effectiveDomainInternalPort($url, $service);
|
||||
|
||||
$row = [
|
||||
'url' => $url,
|
||||
|
|
@ -532,7 +532,7 @@ protected function domainRowFromStored(string $url, ?string $service, array $sto
|
|||
/**
|
||||
* @return array{internal_port: ?int, has_port_override: bool}
|
||||
*/
|
||||
protected function effectiveDomainInternalPort(string $url): array
|
||||
protected function effectiveDomainInternalPort(string $url, ?string $service = null): array
|
||||
{
|
||||
$canonical = DomainPortOverrides::withoutPort($url);
|
||||
$overrides = $this->application->domain_port_overrides ?? [];
|
||||
|
|
@ -561,6 +561,14 @@ protected function effectiveDomainInternalPort(string $url): array
|
|||
];
|
||||
}
|
||||
|
||||
$composePort = dockerComposeServicePort($this->application->docker_compose_raw, $service);
|
||||
if ($composePort !== null) {
|
||||
return [
|
||||
'internal_port' => $composePort,
|
||||
'has_port_override' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$exposed = $this->application->ports_exposes_array;
|
||||
$defaultPort = isset($exposed[0]) && is_numeric($exposed[0]) && (int) $exposed[0] > 0
|
||||
? (int) $exposed[0]
|
||||
|
|
|
|||
|
|
@ -439,7 +439,7 @@ private function validatedDomain(array $parts, string $errorKey): ?string
|
|||
private function makeRow(string $url, ?string $service, array $statuses = []): array
|
||||
{
|
||||
$status = $statuses[$this->statusKey($url, $service)] ?? [];
|
||||
$port = $this->effectiveDomainInternalPort($url);
|
||||
$port = $this->effectiveDomainInternalPort($url, $service);
|
||||
|
||||
return [
|
||||
'url' => $url,
|
||||
|
|
@ -500,7 +500,7 @@ private function openPortWarning(?int $port, string $action): void
|
|||
/**
|
||||
* @return array{internal_port: ?int, has_port_override: bool}
|
||||
*/
|
||||
private function effectiveDomainInternalPort(string $url): array
|
||||
private function effectiveDomainInternalPort(string $url, ?string $service = null): array
|
||||
{
|
||||
$canonical = DomainPortOverrides::withoutPort($url);
|
||||
$overrides = $this->preview->domain_port_overrides ?? [];
|
||||
|
|
@ -529,6 +529,14 @@ private function effectiveDomainInternalPort(string $url): array
|
|||
];
|
||||
}
|
||||
|
||||
$composePort = dockerComposeServicePort($this->preview->application->docker_compose_raw, $service);
|
||||
if ($composePort !== null) {
|
||||
return [
|
||||
'internal_port' => $composePort,
|
||||
'has_port_override' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$exposed = $this->preview->application->ports_exposes_array;
|
||||
$defaultPort = isset($exposed[0]) && is_numeric($exposed[0]) && (int) $exposed[0] > 0
|
||||
? (int) $exposed[0]
|
||||
|
|
|
|||
|
|
@ -601,6 +601,39 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
|
|||
return $labels->sort();
|
||||
}
|
||||
|
||||
function firstDockerComposeServicePort(mixed $service): ?int
|
||||
{
|
||||
$portDefinitions = collect(data_get($service, 'expose', []))
|
||||
->merge(data_get($service, 'ports', []));
|
||||
|
||||
foreach ($portDefinitions as $definition) {
|
||||
$port = is_array($definition)
|
||||
? data_get($definition, 'target')
|
||||
: str((string) $definition)->before('/')->afterLast(':')->value();
|
||||
|
||||
if (is_numeric($port) && (int) $port >= 1 && (int) $port <= 65535) {
|
||||
return (int) $port;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function dockerComposeServicePort(?string $compose, ?string $serviceName): ?int
|
||||
{
|
||||
if (blank($compose) || blank($serviceName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$services = data_get(Yaml::parse($compose), 'services', []);
|
||||
} catch (Throwable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return firstDockerComposeServicePort(is_array($services) ? ($services[$serviceName] ?? null) : null);
|
||||
}
|
||||
|
||||
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both', bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, bool $escape_redirect_replacement_for_compose = true, array $domainPortOverrides = [])
|
||||
{
|
||||
$labels = collect([]);
|
||||
|
|
|
|||
|
|
@ -1358,7 +1358,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||
? ($previewForPorts?->domain_port_overrides ?? [])
|
||||
: ($originalResource->domain_port_overrides ?? []);
|
||||
$exposedPorts = $originalResource->settings->is_static ? [80] : $originalResource->ports_exposes_array;
|
||||
$onlyPort = count($exposedPorts) > 0 ? $exposedPorts[0] : null;
|
||||
$onlyPort = firstDockerComposeServicePort($service) ?? ($exposedPorts[0] ?? null);
|
||||
if (! $use_network_mode && (! $shouldGenerateLabelsExactly || $server->proxyType() === ProxyTypes::TRAEFIK->value)) {
|
||||
$serviceLabels = addTraefikDockerNetworkLabel($serviceLabels, $baseNetwork->first());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3916,7 +3916,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
|||
? ($resource->domain_port_overrides ?? [])
|
||||
: ($preview?->domain_port_overrides ?? []);
|
||||
$exposedPorts = $resource->settings->is_static ? [80] : $resource->ports_exposes_array;
|
||||
$onlyPort = count($exposedPorts) > 0 ? $exposedPorts[0] : null;
|
||||
$onlyPort = firstDockerComposeServicePort($service) ?? ($exposedPorts[0] ?? null);
|
||||
if ($shouldGenerateLabelsExactly) {
|
||||
switch ($server->proxyType()) {
|
||||
case ProxyTypes::TRAEFIK->value:
|
||||
|
|
|
|||
|
|
@ -2311,6 +2311,48 @@
|
|||
->assertDontSee('Custom internal port for this domain', false);
|
||||
});
|
||||
|
||||
it('shows the detected compose service port as the inherited internal port', function () {
|
||||
$this->application->update([
|
||||
'build_pack' => 'dockercompose',
|
||||
'ports_exposes' => '3000',
|
||||
'docker_compose_raw' => "services:\n web:\n image: nginx:alpine\n expose:\n - '8069'\n",
|
||||
'docker_compose_domains' => json_encode([
|
||||
'web' => ['domain' => 'https://example.com'],
|
||||
]),
|
||||
'fqdn' => null,
|
||||
'domain_port_overrides' => null,
|
||||
]);
|
||||
|
||||
Livewire::test(Domains::class, ['application' => $this->application->fresh()])
|
||||
->assertSet('domainRows.0.internal_port', 8069)
|
||||
->assertSet('domainRows.0.has_port_override', false)
|
||||
->assertSee('Internal port 8069')
|
||||
->assertDontSee('Internal port 3000');
|
||||
});
|
||||
|
||||
it('shows the detected compose service port for preview domains', function () {
|
||||
$this->application->update([
|
||||
'build_pack' => 'dockercompose',
|
||||
'ports_exposes' => '3000',
|
||||
'docker_compose_raw' => "services:\n web:\n image: nginx:alpine\n ports:\n - '18069:8069'\n",
|
||||
]);
|
||||
|
||||
$preview = ApplicationPreview::create([
|
||||
'application_id' => $this->application->id,
|
||||
'pull_request_id' => 8069,
|
||||
'pull_request_html_url' => 'https://github.com/coollabsio/coolify/pull/8069',
|
||||
'docker_compose_domains' => json_encode([
|
||||
'web' => ['domain' => 'https://preview.example.com'],
|
||||
]),
|
||||
]);
|
||||
|
||||
Livewire::test(PreviewDomains::class, ['preview' => $preview])
|
||||
->assertSet('domainRows.0.internal_port', 8069)
|
||||
->assertSet('domainRows.0.has_port_override', false)
|
||||
->assertSee('Internal port 8069')
|
||||
->assertDontSee('Internal port 3000');
|
||||
});
|
||||
|
||||
it('keeps a legacy port-bearing url port in the edit field as an internal port override', function () {
|
||||
$this->application->update([
|
||||
'ports_exposes' => '3000,8080',
|
||||
|
|
|
|||
|
|
@ -599,3 +599,35 @@ function disableExactProxyLabels(Application $application): Application
|
|||
->and($labels->contains(fn (string $label): bool => str_contains($label, 'Host(`frontend.example.com`)')))
|
||||
->toBeTrue();
|
||||
});
|
||||
|
||||
test('applicationParser compose labels prefer the service exposed port over application ports_exposes', function (string $portConfiguration) {
|
||||
$application = disableExactProxyLabels(Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => StandaloneDocker::class,
|
||||
'build_pack' => 'dockercompose',
|
||||
'ports_exposes' => '3000',
|
||||
'docker_compose_raw' => <<<YAML
|
||||
services:
|
||||
frontend:
|
||||
image: myapp/frontend:latest
|
||||
{$portConfiguration}
|
||||
YAML,
|
||||
'fqdn' => null,
|
||||
'domain_port_overrides' => null,
|
||||
'docker_compose_domains' => json_encode([
|
||||
'frontend' => ['domain' => 'https://frontend.example.com'],
|
||||
]),
|
||||
]));
|
||||
|
||||
$labels = collect(data_get(applicationParser($application->fresh()), 'services.frontend.labels'));
|
||||
|
||||
expect($labels->contains(fn (string $label): bool => str_ends_with($label, '.loadbalancer.server.port=8069')))
|
||||
->toBeTrue()
|
||||
->and($labels->contains(fn (string $label): bool => str_contains($label, 'reverse_proxy={{upstreams 8069}}')))
|
||||
->toBeTrue();
|
||||
})->with([
|
||||
'expose' => " expose:\n - '8069'",
|
||||
'short port syntax' => " ports:\n - '18069:8069'",
|
||||
'long port syntax' => " ports:\n - target: 8069\n published: 18069",
|
||||
]);
|
||||
|
|
|
|||
Loading…
Reference in a new issue