fix(parsers): populate docker_compose_domains for API-created Docker Compose apps (#9300)

This commit is contained in:
Andras Bacsai 2026-07-07 12:17:52 +02:00 committed by GitHub
commit d01e3a9730
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 327 additions and 4 deletions

View file

@ -1141,7 +1141,7 @@ private function create_application(Request $request, $type)
$request->offsetUnset('docker_compose_domains'); $request->offsetUnset('docker_compose_domains');
} }
if ($dockerComposeDomainsJson->count() > 0) { if ($dockerComposeDomainsJson->count() > 0) {
$application->docker_compose_domains = $dockerComposeDomainsJson; $application->docker_compose_domains = json_encode($dockerComposeDomainsJson);
} }
$repository_url_parsed = Url::fromString($request->git_repository); $repository_url_parsed = Url::fromString($request->git_repository);
$git_host = $repository_url_parsed->getHost(); $git_host = $repository_url_parsed->getHost();
@ -1382,7 +1382,7 @@ private function create_application(Request $request, $type)
$request->offsetUnset('docker_compose_domains'); $request->offsetUnset('docker_compose_domains');
} }
if ($dockerComposeDomainsJson->count() > 0) { if ($dockerComposeDomainsJson->count() > 0) {
$application->docker_compose_domains = $dockerComposeDomainsJson; $application->docker_compose_domains = json_encode($dockerComposeDomainsJson);
} }
$application->fqdn = $fqdn; $application->fqdn = $fqdn;
$application->git_repository = str($gitRepository)->trim()->toString(); $application->git_repository = str($gitRepository)->trim()->toString();
@ -1595,7 +1595,7 @@ private function create_application(Request $request, $type)
$request->offsetUnset('docker_compose_domains'); $request->offsetUnset('docker_compose_domains');
} }
if ($dockerComposeDomainsJson->count() > 0) { if ($dockerComposeDomainsJson->count() > 0) {
$application->docker_compose_domains = $dockerComposeDomainsJson; $application->docker_compose_domains = json_encode($dockerComposeDomainsJson);
} }
$application->fqdn = $fqdn; $application->fqdn = $fqdn;
$application->private_key_id = $privateKey->id; $application->private_key_id = $privateKey->id;

View file

@ -501,6 +501,40 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
'is_preview' => false, 'is_preview' => false,
]); ]);
} }
}
// Also populate docker_compose_domains for dockercompose apps from direct SERVICE_* declarations.
if ($resource->build_pack === 'dockercompose' && ($key->startsWith('SERVICE_FQDN_') || $key->startsWith('SERVICE_URL_'))) {
$parsed = parseServiceEnvironmentVariable($key->value());
$normalizedServiceName = str($parsed['service_name'])->replace('-', '_')->replace('.', '_')->value();
$serviceExists = false;
foreach (array_keys($services) as $serviceNameKey) {
if (str($serviceNameKey)->replace('-', '_')->replace('.', '_')->value() === $normalizedServiceName) {
$serviceExists = true;
break;
}
}
if ($serviceExists) {
$domains = collect(json_decode(data_get($resource, 'docker_compose_domains') ?: '[]'));
$domainExists = data_get($domains->get($normalizedServiceName), 'domain');
if (is_null($domainExists)) {
$serviceNameForDomain = str($parsed['service_name'])->replace('_', '-')->value();
$domainValue = generateUrl(server: $server, random: "$serviceNameForDomain-$uuid");
if ($value && get_class($value) === Illuminate\Support\Stringable::class && $value->startsWith('/')) {
$path = $value->value();
if ($path !== '/') {
$domainValue = "$domainValue$path";
}
}
if ($parsed['port'] && is_numeric($parsed['port'])) {
$domainValue = "$domainValue:{$parsed['port']}";
}
$domains->put($normalizedServiceName, ['domain' => $domainValue]);
$resource->docker_compose_domains = $domains->toJson();
$resource->save();
}
}
} }
} }
@ -608,7 +642,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
// Only add domain if the service exists // Only add domain if the service exists
if ($serviceExists) { if ($serviceExists) {
$domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]); $domains = collect(json_decode(data_get($resource, 'docker_compose_domains') ?: '[]'));
$domainExists = data_get($domains->get($serviceName), 'domain'); $domainExists = data_get($domains->get($serviceName), 'domain');
// Update domain using URL with port if applicable // Update domain using URL with port if applicable

View file

@ -0,0 +1,289 @@
<?php
use App\Models\Application;
use App\Models\Environment;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use App\Models\ServerSetting;
use App\Models\StandaloneDocker;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use phpseclib3\Crypt\EC;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->user = User::factory()->create();
$this->team = Team::factory()->create();
$this->user->teams()->attach($this->team);
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
$this->environment = Environment::factory()->create([
'project_id' => $this->project->id,
]);
$ecKey = EC::createKey('Ed25519');
$privateKeyContent = $ecKey->toString('OpenSSH');
$privateKey = PrivateKey::create([
'name' => 'test-key',
'private_key' => $privateKeyContent,
'team_id' => $this->team->id,
]);
$this->server = Server::factory()->create([
'team_id' => $this->team->id,
'private_key_id' => $privateKey->id,
]);
ServerSetting::create([
'server_id' => $this->server->id,
'wildcard_domain' => 'http://127.0.0.1.sslip.io',
]);
$this->destination = StandaloneDocker::factory()->create([
'server_id' => $this->server->id,
'network' => 'test-network-'.fake()->uuid(),
]);
});
test('applicationParser populates docker_compose_domains for KEY-based SERVICE_FQDN variables', function () {
$dockerCompose = <<<'YAML'
services:
backend:
image: myapp/backend:latest
environment:
- SERVICE_FQDN_BACKEND_8000=${BACKEND_URL}
frontend:
image: myapp/frontend:latest
environment:
- SERVICE_FQDN_FRONTEND=${FRONTEND_URL}
YAML;
$application = Application::factory()->create([
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => StandaloneDocker::class,
'build_pack' => 'dockercompose',
'docker_compose_raw' => $dockerCompose,
'fqdn' => null,
'docker_compose_domains' => null,
]);
applicationParser($application);
$application->refresh();
$domains = json_decode($application->docker_compose_domains, true);
expect($domains)->not->toBeNull()
->and($domains)->toBeArray()
->and($domains)->toHaveKey('backend')
->and($domains['backend'])->toHaveKey('domain')
->and($domains['backend']['domain'])->toStartWith('http://')
->and($domains['backend']['domain'])->not->toBeEmpty();
});
test('applicationParser populates docker_compose_domains for KEY-based SERVICE_FQDN without port', function () {
$dockerCompose = <<<'YAML'
services:
frontend:
image: myapp/frontend:latest
environment:
- SERVICE_FQDN_FRONTEND=${FRONTEND_URL}
YAML;
$application = Application::factory()->create([
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => StandaloneDocker::class,
'build_pack' => 'dockercompose',
'docker_compose_raw' => $dockerCompose,
'fqdn' => null,
'docker_compose_domains' => null,
]);
applicationParser($application);
$application->refresh();
$domains = json_decode($application->docker_compose_domains, true);
expect($domains)->not->toBeNull()
->and($domains)->toHaveKey('frontend')
->and($domains['frontend'])->toHaveKey('domain');
});
test('applicationParser does not populate docker_compose_domains for non-dockercompose build_pack', function () {
$dockerCompose = <<<'YAML'
services:
backend:
image: myapp/backend:latest
environment:
- SERVICE_FQDN_BACKEND_8000=${BACKEND_URL}
YAML;
$application = Application::factory()->create([
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => StandaloneDocker::class,
'build_pack' => 'dockerfile',
'docker_compose_raw' => $dockerCompose,
'fqdn' => null,
'docker_compose_domains' => null,
]);
applicationParser($application);
$application->refresh();
// For non-dockercompose, docker_compose_domains should remain empty
$domains = json_decode($application->docker_compose_domains, true);
expect($domains)->toBeNull();
});
test('applicationParser populates docker_compose_domains for KEY-based SERVICE_URL variables', function () {
$dockerCompose = <<<'YAML'
services:
frontend:
image: myapp/frontend:latest
environment:
- SERVICE_URL_FRONTEND=/ui
YAML;
$application = Application::factory()->create([
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => StandaloneDocker::class,
'build_pack' => 'dockercompose',
'docker_compose_raw' => $dockerCompose,
'fqdn' => null,
'docker_compose_domains' => null,
]);
applicationParser($application);
$application->refresh();
$domains = json_decode($application->docker_compose_domains, true);
expect($domains)->not->toBeNull()
->and($domains)->toHaveKey('frontend')
->and($domains['frontend']['domain'])->toStartWith('http://')
->and($domains['frontend']['domain'])->toEndWith('/ui');
});
test('applicationParser preserves existing docker_compose_domains entries', function () {
$dockerCompose = <<<'YAML'
services:
backend:
image: myapp/backend:latest
environment:
- SERVICE_FQDN_BACKEND_8000=${BACKEND_URL}
frontend:
image: myapp/frontend:latest
environment:
- SERVICE_URL_FRONTEND=/ui
YAML;
$application = Application::factory()->create([
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => StandaloneDocker::class,
'build_pack' => 'dockercompose',
'docker_compose_raw' => $dockerCompose,
'fqdn' => null,
'docker_compose_domains' => json_encode([
'frontend' => ['domain' => 'https://existing.example.com'],
]),
]);
applicationParser($application);
$application->refresh();
$domains = json_decode($application->docker_compose_domains, true);
expect($domains['frontend']['domain'])->toBe('https://existing.example.com')
->and($domains)->toHaveKey('backend')
->and($domains['backend']['domain'])->toStartWith('http://');
});
test('applicationParser handles other docker compose domain shapes without regressions', function () {
$createApplication = function (string $dockerCompose, ?string $dockerComposeDomains = null): Application {
return Application::factory()->create([
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => StandaloneDocker::class,
'build_pack' => 'dockercompose',
'docker_compose_raw' => $dockerCompose,
'fqdn' => null,
'docker_compose_domains' => $dockerComposeDomains,
]);
};
$valueBasedCompose = <<<'YAML'
services:
backend:
image: myapp/backend:latest
frontend:
image: myapp/frontend:latest
environment:
API_URL: ${SERVICE_URL_BACKEND}
YAML;
$valueBasedApplication = $createApplication($valueBasedCompose);
applicationParser($valueBasedApplication);
$valueBasedApplication->refresh();
$valueBasedDomains = json_decode($valueBasedApplication->docker_compose_domains, true);
expect($valueBasedDomains)->toHaveKey('backend')
->and($valueBasedDomains['backend']['domain'])->toStartWith('http://')
->and($valueBasedDomains)->not->toHaveKey('frontend');
$mapStyleCompose = <<<'YAML'
services:
frontend:
image: myapp/frontend:latest
environment:
SERVICE_URL_FRONTEND: /ui
YAML;
$mapStyleApplication = $createApplication($mapStyleCompose);
applicationParser($mapStyleApplication);
$mapStyleApplication->refresh();
$mapStyleDomains = json_decode($mapStyleApplication->docker_compose_domains, true);
expect($mapStyleDomains)->toHaveKey('frontend')
->and($mapStyleDomains['frontend']['domain'])->toEndWith('/ui');
$missingServiceCompose = <<<'YAML'
services:
worker:
image: myapp/worker:latest
environment:
SERVICE_URL_API: /api
YAML;
$missingServiceApplication = $createApplication($missingServiceCompose);
applicationParser($missingServiceApplication);
$missingServiceApplication->refresh();
expect(json_decode($missingServiceApplication->docker_compose_domains, true))->toBeNull();
$plainCompose = <<<'YAML'
services:
worker:
image: myapp/worker:latest
environment:
FOO: bar
YAML;
$plainApplication = $createApplication($plainCompose);
applicationParser($plainApplication);
$plainApplication->refresh();
expect(json_decode($plainApplication->docker_compose_domains, true))->toBeNull();
});