diff --git a/bootstrap/helpers/domains.php b/bootstrap/helpers/domains.php index 4e4ad73e6f..28ff41b3df 100644 --- a/bootstrap/helpers/domains.php +++ b/bootstrap/helpers/domains.php @@ -443,6 +443,26 @@ function getComposeServiceDomainString(array|Collection $domains, string $servic return $matches[0]['domain']; } +/** + * Determine whether a compose service already has a domain-map entry, including + * an explicitly empty entry left when a user removes its generated domain. + * + * @param array|Collection $domains + */ +function hasComposeServiceDomainEntry(array|Collection $domains, string $serviceName): bool +{ + $normalized = normalizeComposeServiceName($serviceName); + + foreach (collect($domains)->keys() as $key) { + $key = (string) $key; + if ($key === $serviceName || normalizeComposeServiceName($key) === $normalized) { + return true; + } + } + + return false; +} + function composeDomainEntryString(mixed $entry): ?string { if (is_object($entry)) { diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index 590b68d162..03ebf5af7a 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -525,8 +525,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $originalServiceName = findComposeServiceName($normalizedServiceName, array_keys($services)); if ($originalServiceName !== null) { $domains = json_decode(data_get($resource, 'docker_compose_domains') ?: '[]', true) ?: []; - $domainExists = getComposeServiceDomainString($domains, $originalServiceName); - if (is_null($domainExists)) { + if (! hasComposeServiceDomainEntry($domains, $originalServiceName)) { $serviceNameForDomain = str($parsed['service_name'])->replace('_', '-')->value(); $domainValue = generateUrl(server: $server, random: "$serviceNameForDomain-$uuid"); if ($value && get_class($value) === Stringable::class && $value->startsWith('/')) { @@ -648,12 +647,10 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int // Only add domain if the service exists if ($composeServiceName !== null) { $domains = json_decode(data_get($resource, 'docker_compose_domains') ?: '[]', true) ?: []; - $domainExists = getComposeServiceDomainString($domains, $composeServiceName); - // Update domain using URL with port if applicable $domainValue = $port ? $urlWithPort : $url; - if (is_null($domainExists)) { + if (! hasComposeServiceDomainEntry($domains, $composeServiceName)) { $resource->docker_compose_domains = json_encode(putComposeServiceDomain( $domains, $composeServiceName, diff --git a/tests/Feature/ApplicationDomainsTest.php b/tests/Feature/ApplicationDomainsTest.php index a13b5fdbfd..f2beb22f13 100644 --- a/tests/Feature/ApplicationDomainsTest.php +++ b/tests/Feature/ApplicationDomainsTest.php @@ -111,6 +111,33 @@ it('does not add a single-label hostname as an application domain', function () expect($this->application->fresh()->fqdn)->toBeNull(); }); +it('keeps a compose domain removed when the service declares a magic URL variable', function () { + $this->application->update([ + 'build_pack' => 'dockercompose', + 'docker_compose_raw' => <<<'YAML' +services: + web: + image: nginx:alpine + environment: + SERVICE_URL_WEB: /api +YAML, + 'docker_compose_domains' => json_encode([ + 'web' => ['domain' => 'https://web.example.com/api'], + ]), + ]); + + $component = Livewire::test(Domains::class, ['application' => $this->application->fresh()]); + $domainKey = hash('sha256', 'https://web.example.com/api|web'); + + $component + ->call('removeDomainByKey', $domainKey) + ->assertDispatched('success') + ->assertSet('domainRows', []); + + expect(json_decode($this->application->fresh()->docker_compose_domains, true)) + ->toMatchArray(['web' => ['domain' => null]]); +}); + it('generates a preview domain when the application has no domain', function () { $preview = ApplicationPreview::create([ 'application_id' => $this->application->id,