fix: avoid inherited compose ports and defer archive inspection

Prevent multi-service Compose domains from inheriting the application port, and defer PostgreSQL custom-format archive inspection to pg_restore.
This commit is contained in:
Andras Bacsai
2026-09-05 14:19:38 +02:00
parent 16295e1ab3
commit 208f7720bc
8 changed files with 130 additions and 14 deletions
+65
View File
@@ -2436,6 +2436,25 @@ it('shows the detected compose service port as the inherited internal port', fun
->assertDontSee('Internal port 3000');
});
it('does not show an application port as the inherited port for a compose service without a declared port', function () {
$this->application->update([
'build_pack' => 'dockercompose',
'ports_exposes' => '3000',
'docker_compose_raw' => "services:\n backend:\n build: ./backend\n frontend:\n build: ./frontend\n",
'docker_compose_domains' => json_encode([
'backend' => ['domain' => 'https://api.example.com'],
'frontend' => ['domain' => 'https://app.example.com'],
]),
'fqdn' => null,
'domain_port_overrides' => null,
]);
Livewire::test(Domains::class, ['application' => $this->application->fresh()])
->assertSet('domainRows.0.internal_port', null)
->assertSet('domainRows.1.internal_port', null)
->assertDontSee('Internal port 3000');
});
it('shows the detected compose service port for preview domains', function () {
$this->application->update([
'build_pack' => 'dockercompose',
@@ -2459,6 +2478,27 @@ it('shows the detected compose service port for preview domains', function () {
->assertDontSee('Internal port 3000');
});
it('does not show an application port for a preview compose service without a declared port', function () {
$this->application->update([
'build_pack' => 'dockercompose',
'ports_exposes' => '3000',
'docker_compose_raw' => "services:\n backend:\n build: ./backend\n frontend:\n build: ./frontend\n",
]);
$preview = ApplicationPreview::create([
'application_id' => $this->application->id,
'pull_request_id' => 8070,
'pull_request_html_url' => 'https://github.com/coollabsio/coolify/pull/8070',
'docker_compose_domains' => json_encode([
'frontend' => ['domain' => 'https://preview.example.com'],
]),
]);
Livewire::test(PreviewDomains::class, ['preview' => $preview])
->assertSet('domainRows.0.internal_port', null)
->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',
@@ -2514,6 +2554,31 @@ it('stores compose domain port overrides without wiping other services', functio
->toHaveKey('https://api.example.com', 4000);
});
it('saves an unrecognized compose domain port after confirming the warning', function () {
$this->application->update([
'build_pack' => 'dockercompose',
'fqdn' => null,
'ports_exposes' => '3000',
'docker_compose_raw' => "services:\n frontend:\n build: ./frontend\n",
'docker_compose_domains' => json_encode([
'frontend' => ['domain' => 'https://app.example.com'],
]),
'domain_port_overrides' => null,
]);
Livewire::test(Domains::class, ['application' => $this->application->fresh()])
->call('startEdit', 0)
->set('editingDomainParts.port', '80')
->call('updateDomain')
->assertSet('showPortWarningModal', true)
->call('confirmUseUnknownPort')
->assertSet('showPortWarningModal', false)
->assertDispatched('success');
expect($this->application->fresh()->domain_port_overrides)
->toBe(['https://app.example.com' => 80]);
});
it('prunes a compose domain port override when that domain is removed', function () {
$this->application->update([
'build_pack' => 'dockercompose',
@@ -631,3 +631,37 @@ YAML,
'short port syntax' => " ports:\n - '18069:8069'",
'long port syntax' => " ports:\n - target: 8069\n published: 18069",
]);
test('applicationParser does not apply an application port to compose services without a declared port', function () {
$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:
postgres:
image: postgres:16-alpine
backend:
build: ./backend
frontend:
build: ./frontend
YAML,
'fqdn' => null,
'domain_port_overrides' => null,
'docker_compose_domains' => json_encode([
'backend' => ['domain' => 'https://api.example.com'],
'frontend' => ['domain' => 'https://app.example.com'],
]),
]));
$services = data_get(applicationParser($application->fresh()), 'services');
$backendLabels = collect(data_get($services, 'backend.labels'));
$frontendLabels = collect(data_get($services, 'frontend.labels'));
expect($backendLabels->contains(fn (string $label): bool => str_contains($label, '.loadbalancer.server.port=')))
->toBeFalse()
->and($frontendLabels->contains(fn (string $label): bool => str_contains($label, '.loadbalancer.server.port=')))
->toBeFalse();
});
@@ -220,16 +220,16 @@ test('file scanner allows ordinary gzipped dumps', function () {
expect(DatabaseBackupFileValidator::fileContainsPostgresqlProgramExecution($gzClean))->toBeFalse();
});
test('file scanner detects program execution payloads inside custom format archives', function () {
test('file scanner defers custom format archives to pg_restore inspection', function () {
$archive = writeScanPayload("PGDMP\0binary COPY records FROM PROGRAM payload");
expect(DatabaseBackupFileValidator::fileContainsPostgresqlProgramExecution($archive))->toBeTrue();
expect(DatabaseBackupFileValidator::fileContainsPostgresqlProgramExecution($archive))->toBeFalse();
});
test('file scanner detects program execution payloads inside gzipped custom format archives', function () {
test('file scanner defers gzipped custom format archives to pg_restore inspection', function () {
$archive = writeScanPayload("PGDMP\0binary COPY records FROM PROGRAM payload", gzip: true);
expect(DatabaseBackupFileValidator::fileContainsPostgresqlProgramExecution($archive))->toBeTrue();
expect(DatabaseBackupFileValidator::fileContainsPostgresqlProgramExecution($archive))->toBeFalse();
});
test('file scanner allows custom format archives without program execution', function () {