mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-15 18:15:49 +00:00
fix(compose): normalize service-name keys for domains and env vars (#11040)
This commit is contained in:
1 parent
7b18777f06
commit
0b843bb07c
18 files changed
+1443
-220
No files matched your search
@@ -211,6 +211,86 @@ YAML;
|
||||
->and($domains['backend']['domain'])->toStartWith('http://');
|
||||
});
|
||||
|
||||
test('applicationParser stores domains under original hyphenated compose service names', function () {
|
||||
$dockerCompose = <<<'YAML'
|
||||
services:
|
||||
another-service:
|
||||
image: myapp/api:latest
|
||||
environment:
|
||||
- SERVICE_FQDN_ANOTHER_SERVICE=${API_URL}
|
||||
analytics:
|
||||
image: myapp/analytics:latest
|
||||
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)->toBeArray()
|
||||
->and($domains)->toHaveKey('another-service')
|
||||
->and($domains)->not->toHaveKey('another_service')
|
||||
->and($domains['another-service']['domain'])->toStartWith('http://');
|
||||
});
|
||||
|
||||
test('applicationParser preserves legacy underscore domain keys by matching hyphenated services', function () {
|
||||
$dockerCompose = <<<'YAML'
|
||||
services:
|
||||
another-service:
|
||||
image: myapp/api:latest
|
||||
environment:
|
||||
- SERVICE_FQDN_ANOTHER_SERVICE=${API_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' => json_encode([
|
||||
'another_service' => ['domain' => 'https://legacy.example.com'],
|
||||
]),
|
||||
]);
|
||||
|
||||
applicationParser($application);
|
||||
|
||||
$application->refresh();
|
||||
$domains = json_decode($application->docker_compose_domains, true);
|
||||
|
||||
// Existing domain is preserved (not overwritten) even when stored under legacy underscore key.
|
||||
expect(getComposeServiceDomainString($domains, 'another-service'))->toBe('https://legacy.example.com');
|
||||
});
|
||||
|
||||
test('compose domain reconciliation preserves stored domains when parsing returns no services', function () {
|
||||
$storedDomains = json_encode([
|
||||
'frontend' => ['domain' => 'https://frontend.example.com'],
|
||||
]);
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => StandaloneDocker::class,
|
||||
'build_pack' => 'dockercompose',
|
||||
'docker_compose_domains' => $storedDomains,
|
||||
]);
|
||||
|
||||
$method = new ReflectionMethod($application, 'reconcileDockerComposeDomains');
|
||||
$method->invoke($application, ['services' => []]);
|
||||
|
||||
expect($application->fresh()->docker_compose_domains)->toBe($storedDomains);
|
||||
});
|
||||
|
||||
test('applicationParser handles other docker compose domain shapes without regressions', function () {
|
||||
$createApplication = function (string $dockerCompose, ?string $dockerComposeDomains = null): Application {
|
||||
return Application::factory()->create([
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
@@ -57,6 +62,52 @@ it('populates fqdn with multiple domains from multiple services', function () {
|
||||
expect($preview->fqdn)->toContain('api.example.com');
|
||||
});
|
||||
|
||||
it('preserves distinct services whose normalized names collide when generating preview domains', function () {
|
||||
$dockerCompose = <<<'YAML'
|
||||
services:
|
||||
api-test:
|
||||
image: nginx:alpine
|
||||
api.test:
|
||||
image: nginx:alpine
|
||||
YAML;
|
||||
|
||||
$team = Team::factory()->create();
|
||||
$project = Project::factory()->create(['team_id' => $team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
$server = Server::factory()->create(['team_id' => $team->id]);
|
||||
$destination = StandaloneDocker::query()->where('server_id', $server->id)->first()
|
||||
?? StandaloneDocker::factory()->create(['server_id' => $server->id]);
|
||||
|
||||
$application = Application::factory()->create([
|
||||
'build_pack' => 'dockercompose',
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => StandaloneDocker::class,
|
||||
'docker_compose_raw' => $dockerCompose,
|
||||
'docker_compose_domains' => json_encode([
|
||||
'api-test' => ['domain' => 'https://hyphen.example.com'],
|
||||
'api.test' => ['domain' => 'https://dot.example.com'],
|
||||
]),
|
||||
]);
|
||||
|
||||
$preview = ApplicationPreview::create([
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => 17,
|
||||
'pull_request_html_url' => 'https://github.com/example/repo/pull/17',
|
||||
'docker_compose_domains' => $application->docker_compose_domains,
|
||||
]);
|
||||
|
||||
$preview->generate_preview_fqdn_compose();
|
||||
$preview->refresh();
|
||||
|
||||
$domains = json_decode($preview->docker_compose_domains, true);
|
||||
|
||||
expect($domains)->toHaveKeys(['api-test', 'api.test'])
|
||||
->and($domains)->toHaveCount(2)
|
||||
->and($domains['api-test']['domain'])->toContain('hyphen.example.com')
|
||||
->and($domains['api.test']['domain'])->toContain('dot.example.com');
|
||||
});
|
||||
|
||||
it('sets fqdn to null when no domains are configured', function () {
|
||||
$application = Application::factory()->create([
|
||||
'build_pack' => 'dockercompose',
|
||||
@@ -78,3 +129,64 @@ it('sets fqdn to null when no domains are configured', function () {
|
||||
|
||||
expect($preview->fqdn)->toBeNull();
|
||||
});
|
||||
|
||||
it('collapses dashed and underscore twin domain keys into one preview service', function () {
|
||||
$application = Application::factory()->create([
|
||||
'build_pack' => 'dockercompose',
|
||||
'docker_compose_domains' => json_encode([
|
||||
'web-api' => ['domain' => 'https://web-api.example.com'],
|
||||
'web_api' => ['domain' => ''],
|
||||
]),
|
||||
]);
|
||||
|
||||
$preview = ApplicationPreview::create([
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => 58,
|
||||
'pull_request_html_url' => 'https://github.com/example/repo/pull/58',
|
||||
// Existing dual-key preview state from older Coolify versions / mixed write paths.
|
||||
'docker_compose_domains' => json_encode([
|
||||
'web_api' => ['domain' => 'https://old-preview.example.com'],
|
||||
'web-api' => ['domain' => ''],
|
||||
]),
|
||||
]);
|
||||
|
||||
$preview->generate_preview_fqdn_compose();
|
||||
$preview->refresh();
|
||||
|
||||
$domains = json_decode($preview->docker_compose_domains, true);
|
||||
|
||||
expect($domains)->toHaveKey('web-api')
|
||||
->and($domains)->not->toHaveKey('web_api')
|
||||
->and($domains)->toHaveCount(1)
|
||||
->and($domains['web-api']['domain'])->toContain('web-api.example.com')
|
||||
->and($domains['web-api']['domain'])->toContain('58')
|
||||
->and($preview->fqdn)->toContain('web-api.example.com');
|
||||
});
|
||||
|
||||
it('reads legacy underscore application domains when generating previews for dashed services', function () {
|
||||
$application = Application::factory()->create([
|
||||
'build_pack' => 'dockercompose',
|
||||
'docker_compose_domains' => json_encode([
|
||||
'web_api' => ['domain' => 'https://legacy.example.com'],
|
||||
]),
|
||||
]);
|
||||
|
||||
$preview = ApplicationPreview::create([
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => 12,
|
||||
'pull_request_html_url' => 'https://github.com/example/repo/pull/12',
|
||||
'docker_compose_domains' => json_encode([
|
||||
'web-api' => ['domain' => ''],
|
||||
]),
|
||||
]);
|
||||
|
||||
$preview->generate_preview_fqdn_compose();
|
||||
$preview->refresh();
|
||||
|
||||
$domains = json_decode($preview->docker_compose_domains, true);
|
||||
|
||||
expect($domains)->toHaveKey('web-api')
|
||||
->and($domains)->not->toHaveKey('web_api')
|
||||
->and($domains['web-api']['domain'])->toContain('legacy.example.com')
|
||||
->and($preview->fqdn)->toContain('legacy.example.com');
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Feature regression for #8798 / #8980: saving a service domain that includes a
|
||||
* required port must not corrupt SERVICE_URL values.
|
||||
*
|
||||
* EditDomain saves fqdn then calls updateCompose() (Spatie-based) and parse().
|
||||
* updateCompose is the path that writes SERVICE_URL_* from the saved FQDN and is
|
||||
* fully exerciseable under SQLite; getFqdnWithoutPort coverage lives in unit tests.
|
||||
*/
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->destination = StandaloneDocker::where('server_id', $this->server->id)->first()
|
||||
?? StandaloneDocker::factory()->create(['server_id' => $this->server->id]);
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||
});
|
||||
|
||||
test('updateCompose keeps valid SERVICE_URL values when domain includes required port', function () {
|
||||
$template = <<<'YAML'
|
||||
services:
|
||||
gitlab:
|
||||
image: gitlab/gitlab-ce:latest
|
||||
environment:
|
||||
- SERVICE_URL_GITLAB_80
|
||||
- EXTERNAL_URL=$SERVICE_URL_GITLAB
|
||||
YAML;
|
||||
|
||||
$service = Service::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'server_id' => $this->server->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
'docker_compose_raw' => $template,
|
||||
'compose_parsing_version' => '5',
|
||||
]);
|
||||
|
||||
$serviceApp = ServiceApplication::create([
|
||||
'name' => 'gitlab',
|
||||
'service_id' => $service->id,
|
||||
'image' => 'gitlab/gitlab-ce:latest',
|
||||
'fqdn' => 'http://git.example.com:80',
|
||||
]);
|
||||
|
||||
// EditDomain::submit calls updateCompose after saving fqdn
|
||||
updateCompose($serviceApp);
|
||||
|
||||
$serviceApp->refresh();
|
||||
|
||||
expect($serviceApp->fqdn)->toBe('http://git.example.com:80')
|
||||
->and($serviceApp->fqdn)->not->toContain('//:80')
|
||||
->and(fn () => Url::fromString($serviceApp->fqdn))->not->toThrow(Throwable::class);
|
||||
|
||||
$baseUrl = $service->environment_variables()->where('key', 'SERVICE_URL_GITLAB')->first();
|
||||
$portUrl = $service->environment_variables()->where('key', 'SERVICE_URL_GITLAB_80')->first();
|
||||
|
||||
expect($baseUrl)->not->toBeNull()
|
||||
->and($portUrl)->not->toBeNull()
|
||||
->and($baseUrl->value)->toBe('http://git.example.com')
|
||||
->and($portUrl->value)->toBe('http://git.example.com:80')
|
||||
->and(fn () => Url::fromString($baseUrl->value))->not->toThrow(Throwable::class)
|
||||
->and(fn () => Url::fromString($portUrl->value))->not->toThrow(Throwable::class);
|
||||
});
|
||||
|
||||
test('getFqdnWithoutPort used by serviceParser strip is safe for ported service domains', function () {
|
||||
// Mirrors serviceParser when $savedService->fqdn already has a port
|
||||
$savedFqdn = 'http://git.example.com:80';
|
||||
$firstFqdn = trim((string) str($savedFqdn)->explode(',')->first());
|
||||
$url = getFqdnWithoutPort($firstFqdn);
|
||||
$port = '80';
|
||||
$urlWithPort = "$url:$port";
|
||||
|
||||
expect($url)->toBe('http://git.example.com')
|
||||
->and($urlWithPort)->toBe('http://git.example.com:80')
|
||||
->and($urlWithPort)->not->toBe('http://git.example.com/:80')
|
||||
->and(fn () => Url::fromString($urlWithPort))->not->toThrow(Throwable::class);
|
||||
});
|
||||
Reference in new issue
Block a user