From 6decad2e967d5c232542bd87a07c6a26db203ac4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:15:53 +0100 Subject: [PATCH] refactor: streamline required port retrieval in EditDomain and ServiceApplicationView; add environment_variables method in ServiceApplication --- app/Livewire/Project/Service/EditDomain.php | 5 +- .../Service/ServiceApplicationView.php | 5 +- app/Models/ServiceApplication.php | 78 +++++++++++++++++++ bootstrap/helpers/parsers.php | 41 ++++++++-- 4 files changed, 118 insertions(+), 11 deletions(-) diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php index a9a7de878..7158b6e40 100644 --- a/app/Livewire/Project/Service/EditDomain.php +++ b/app/Livewire/Project/Service/EditDomain.php @@ -39,7 +39,7 @@ public function mount() { $this->application = ServiceApplication::ownedByCurrentTeam()->findOrFail($this->applicationId); $this->authorize('view', $this->application); - $this->requiredPort = $this->application->service->getRequiredPort(); + $this->requiredPort = $this->application->getRequiredPort(); $this->syncData(); } @@ -113,8 +113,7 @@ public function submit() // Check for required port if (! $this->forceRemovePort) { - $service = $this->application->service; - $requiredPort = $service->getRequiredPort(); + $requiredPort = $this->application->getRequiredPort(); if ($requiredPort !== null) { // Check if all FQDNs have a port diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php index 1d8d8b247..259b9dbec 100644 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ b/app/Livewire/Project/Service/ServiceApplicationView.php @@ -135,7 +135,7 @@ public function mount() try { $this->parameters = get_route_parameters(); $this->authorize('view', $this->application); - $this->requiredPort = $this->application->service->getRequiredPort(); + $this->requiredPort = $this->application->getRequiredPort(); $this->syncData(); } catch (\Throwable $e) { return handleError($e, $this); @@ -268,8 +268,7 @@ public function submit() // Check for required port if (! $this->forceRemovePort) { - $service = $this->application->service; - $requiredPort = $service->getRequiredPort(); + $requiredPort = $this->application->getRequiredPort(); if ($requiredPort !== null) { // Check if all FQDNs have a port diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index 49bd56206..fd5c4afdb 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -109,6 +109,11 @@ public function fileStorages() return $this->morphMany(LocalFileVolume::class, 'resource'); } + public function environment_variables() + { + return $this->morphMany(EnvironmentVariable::class, 'resourceable'); + } + public function fqdns(): Attribute { return Attribute::make( @@ -174,4 +179,77 @@ public function isBackupSolutionAvailable() { return false; } + + /** + * Get the required port for this service application. + * Extracts port from SERVICE_URL_* or SERVICE_FQDN_* environment variables + * stored at the Service level, filtering by normalized container name. + * Falls back to service-level port if no port-specific variable is found. + */ + public function getRequiredPort(): ?int + { + try { + // Normalize container name same way as variable creation + // (uppercase, replace - and . with _) + $normalizedName = str($this->name) + ->upper() + ->replace('-', '_') + ->replace('.', '_') + ->value(); + // Get all environment variables from the service + $serviceEnvVars = $this->service->environment_variables()->get(); + + // Look for SERVICE_FQDN_* or SERVICE_URL_* variables that match this container + foreach ($serviceEnvVars as $envVar) { + $key = str($envVar->key); + + // Check if this is a SERVICE_FQDN_* or SERVICE_URL_* variable + if (! $key->startsWith('SERVICE_FQDN_') && ! $key->startsWith('SERVICE_URL_')) { + continue; + } + // Extract the part after SERVICE_FQDN_ or SERVICE_URL_ + if ($key->startsWith('SERVICE_FQDN_')) { + $suffix = $key->after('SERVICE_FQDN_'); + } else { + $suffix = $key->after('SERVICE_URL_'); + } + + // Check if this variable starts with our normalized container name + // Format: {NORMALIZED_NAME}_{PORT} or just {NORMALIZED_NAME} + if (! $suffix->startsWith($normalizedName)) { + \Log::debug('[ServiceApplication::getRequiredPort] Suffix does not match container', [ + 'expected_start' => $normalizedName, + 'actual_suffix' => $suffix->value(), + ]); + + continue; + } + + // Check if there's a port suffix after the container name + // The suffix should be exactly NORMALIZED_NAME or NORMALIZED_NAME_PORT + $afterName = $suffix->after($normalizedName)->value(); + + // If there's content after the name, it should start with underscore + if ($afterName !== '' && str($afterName)->startsWith('_')) { + // Extract port: _3210 -> 3210 + $port = str($afterName)->after('_')->value(); + // Validate that the extracted port is numeric + if (is_numeric($port)) { + \Log::debug('[ServiceApplication::getRequiredPort] MATCH FOUND - Returning port', [ + 'port' => (int) $port, + ]); + + return (int) $port; + } + } + } + + // Fall back to service-level port if no port-specific variable is found + $fallbackPort = $this->service->getRequiredPort(); + + return $fallbackPort; + } catch (\Throwable $e) { + return null; + } + } } diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index 1deec45d7..bd3a6e9e1 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -563,12 +563,25 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int } } } elseif ($command->value() === 'URL') { - $urlFor = $key->after('SERVICE_URL_')->lower()->value(); + // SERVICE_URL_APP or SERVICE_URL_APP_3000 + // Detect if there's a port suffix + if (substr_count(str($key)->value(), '_') === 3) { + $urlFor = $key->after('SERVICE_URL_')->beforeLast('_')->lower()->value(); + $port = $key->afterLast('_')->value(); + } else { + $urlFor = $key->after('SERVICE_URL_')->lower()->value(); + $port = null; + } $originalUrlFor = str($urlFor)->replace('_', '-'); if (str($urlFor)->contains('-')) { $urlFor = str($urlFor)->replace('-', '_')->replace('.', '_'); } $url = generateUrl(server: $server, random: "$originalUrlFor-$uuid"); + // Append port if specified + $urlWithPort = $url; + if ($port && is_numeric($port)) { + $urlWithPort = "$url:$port"; + } $resource->environment_variables()->firstOrCreate([ 'key' => $key->value(), 'resourceable_type' => get_class($resource), @@ -595,12 +608,12 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $envExists = $resource->environment_variables()->where('key', $key->value())->first(); if ($domainExists !== $envExists->value) { $envExists->update([ - 'value' => $url, + 'value' => $urlWithPort, ]); } if (is_null($domainExists)) { $domains->put((string) $urlFor, [ - 'domain' => $url, + 'domain' => $urlWithPort, ]); $resource->docker_compose_domains = $domains->toJson(); $resource->save(); @@ -1642,8 +1655,17 @@ function serviceParser(Service $resource): Collection $url = generateUrl(server: $server, random: str($fqdnFor)->replace('_', '-')->value()."-$uuid"); $envExists = $resource->environment_variables()->where('key', $key->value())->first(); + // Also check if a port-suffixed version exists (e.g., SERVICE_FQDN_UMAMI_3000) + $portSuffixedExists = $resource->environment_variables() + ->where('key', 'LIKE', $key->value().'_%') + ->whereRaw('key ~ ?', ['^'.$key->value().'_[0-9]+$']) + ->exists(); $serviceExists = ServiceApplication::where('name', str($fqdnFor)->replace('_', '-')->value())->where('service_id', $resource->id)->first(); - if (! $envExists && (data_get($serviceExists, 'name') === str($fqdnFor)->replace('_', '-')->value())) { + // Check if FQDN already has a port set (contains ':' after the domain) + $fqdnHasPort = $serviceExists && str($serviceExists->fqdn)->contains(':') && str($serviceExists->fqdn)->afterLast(':')->isMatch('/^\d+$/'); + // Only set FQDN if it's for the current service being processed (prevent race conditions) + $isCurrentService = $serviceExists && $serviceExists->id === $savedService->id; + if (! $envExists && ! $portSuffixedExists && ! $fqdnHasPort && $isCurrentService && (data_get($serviceExists, 'name') === str($fqdnFor)->replace('_', '-')->value())) { // Save URL otherwise it won't work. $serviceExists->fqdn = $url; $serviceExists->save(); @@ -1662,8 +1684,17 @@ function serviceParser(Service $resource): Collection $url = generateUrl(server: $server, random: str($urlFor)->replace('_', '-')->value()."-$uuid"); $envExists = $resource->environment_variables()->where('key', $key->value())->first(); + // Also check if a port-suffixed version exists (e.g., SERVICE_URL_DASHBOARD_6791) + $portSuffixedExists = $resource->environment_variables() + ->where('key', 'LIKE', $key->value().'_%') + ->whereRaw('key ~ ?', ['^'.$key->value().'_[0-9]+$']) + ->exists(); $serviceExists = ServiceApplication::where('name', str($urlFor)->replace('_', '-')->value())->where('service_id', $resource->id)->first(); - if (! $envExists && (data_get($serviceExists, 'name') === str($urlFor)->replace('_', '-')->value())) { + // Check if FQDN already has a port set (contains ':' after the domain) + $fqdnHasPort = $serviceExists && str($serviceExists->fqdn)->contains(':') && str($serviceExists->fqdn)->afterLast(':')->isMatch('/^\d+$/'); + // Only set FQDN if it's for the current service being processed (prevent race conditions) + $isCurrentService = $serviceExists && $serviceExists->id === $savedService->id; + if (! $envExists && ! $portSuffixedExists && ! $fqdnHasPort && $isCurrentService && (data_get($serviceExists, 'name') === str($urlFor)->replace('_', '-')->value())) { $serviceExists->fqdn = $url; $serviceExists->save(); }