diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php
index 8786d8a18..9d44e08f9 100644
--- a/app/Jobs/PushServerUpdateJob.php
+++ b/app/Jobs/PushServerUpdateJob.php
@@ -105,6 +105,20 @@ public function __construct(public Server $server, public $data)
public function handle()
{
+ // Defensive initialization for Collection properties to handle queue deserialization edge cases
+ $this->serviceContainerStatuses ??= collect();
+ $this->applicationContainerStatuses ??= collect();
+ $this->foundApplicationIds ??= collect();
+ $this->foundDatabaseUuids ??= collect();
+ $this->foundServiceApplicationIds ??= collect();
+ $this->foundApplicationPreviewsIds ??= collect();
+ $this->foundServiceDatabaseIds ??= collect();
+ $this->allApplicationIds ??= collect();
+ $this->allDatabaseUuids ??= collect();
+ $this->allTcpProxyUuids ??= collect();
+ $this->allServiceApplicationIds ??= collect();
+ $this->allServiceDatabaseIds ??= collect();
+
// TODO: Swarm is not supported yet
if (! $this->data) {
throw new \Exception('No data provided');
diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php
index fd5c4afdb..aef74b402 100644
--- a/app/Models/ServiceApplication.php
+++ b/app/Models/ServiceApplication.php
@@ -189,65 +189,66 @@ public function isBackupSolutionAvailable()
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();
+ // Parse the Docker Compose to find SERVICE_URL/SERVICE_FQDN variables DIRECTLY DECLARED
+ // for this specific service container (not just referenced from other containers)
+ $dockerComposeRaw = data_get($this->service, 'docker_compose_raw');
+ if (! $dockerComposeRaw) {
+ // Fall back to service-level port if no compose file
+ return $this->service->getRequiredPort();
+ }
- // Look for SERVICE_FQDN_* or SERVICE_URL_* variables that match this container
- foreach ($serviceEnvVars as $envVar) {
- $key = str($envVar->key);
+ $dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw);
+ $serviceConfig = data_get($dockerCompose, "services.{$this->name}");
+ if (! $serviceConfig) {
+ return $this->service->getRequiredPort();
+ }
- // 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_');
- }
+ $environment = data_get($serviceConfig, 'environment', []);
- // 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(),
- ]);
+ // Extract SERVICE_URL and SERVICE_FQDN variables DIRECTLY DECLARED in this service's environment
+ // (not variables that are merely referenced with ${VAR} syntax)
+ $portFound = null;
+ foreach ($environment as $key => $value) {
+ if (is_int($key) && is_string($value)) {
+ // List-style: "- SERVICE_URL_APP_3000" or "- SERVICE_URL_APP_3000=value"
+ // Extract variable name (before '=' if present)
+ $envVarName = str($value)->before('=')->trim();
- continue;
- }
+ // Only process direct declarations
+ if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) {
+ // Parse to check if it has a port suffix
+ $parsed = parseServiceEnvironmentVariable($envVarName->value());
+ if ($parsed['has_port'] && $parsed['port']) {
+ // Found a port-specific variable for this service
+ $portFound = (int) $parsed['port'];
+ break;
+ }
+ }
+ } elseif (is_string($key)) {
+ // Map-style: "SERVICE_URL_APP_3000: value" or "SERVICE_FQDN_DB: localhost"
+ $envVarName = str($key);
- // 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;
+ // Only process direct declarations
+ if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) {
+ // Parse to check if it has a port suffix
+ $parsed = parseServiceEnvironmentVariable($envVarName->value());
+ if ($parsed['has_port'] && $parsed['port']) {
+ // Found a port-specific variable for this service
+ $portFound = (int) $parsed['port'];
+ break;
+ }
}
}
}
- // Fall back to service-level port if no port-specific variable is found
- $fallbackPort = $this->service->getRequiredPort();
+ // If a port was found in the template, return it
+ if ($portFound !== null) {
+ return $portFound;
+ }
- return $fallbackPort;
+ // No port-specific variables found for this service, return null
+ // (DO NOT fall back to service-level port, as that applies to all services)
+ return null;
} catch (\Throwable $e) {
return null;
}
diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php
index 4e0709e49..dfcc3e190 100644
--- a/bootstrap/helpers/parsers.php
+++ b/bootstrap/helpers/parsers.php
@@ -514,84 +514,96 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
$key = str($key);
$value = replaceVariables($value);
$command = parseCommandFromMagicEnvVariable($key);
- if ($command->value() === 'FQDN') {
- $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value();
- $originalFqdnFor = str($fqdnFor)->replace('_', '-');
- if (str($fqdnFor)->contains('-')) {
- $fqdnFor = str($fqdnFor)->replace('-', '_')->replace('.', '_');
+ if ($command->value() === 'FQDN' || $command->value() === 'URL') {
+ // ALWAYS create BOTH SERVICE_URL and SERVICE_FQDN pairs regardless of which one is in template
+ $parsed = parseServiceEnvironmentVariable($key->value());
+ $serviceName = $parsed['service_name'];
+ $port = $parsed['port'];
+
+ // Extract case-preserved service name from template
+ $strKey = str($key->value());
+ if ($parsed['has_port']) {
+ if ($strKey->startsWith('SERVICE_URL_')) {
+ $serviceNamePreserved = $strKey->after('SERVICE_URL_')->beforeLast('_')->value();
+ } else {
+ $serviceNamePreserved = $strKey->after('SERVICE_FQDN_')->beforeLast('_')->value();
+ }
+ } else {
+ if ($strKey->startsWith('SERVICE_URL_')) {
+ $serviceNamePreserved = $strKey->after('SERVICE_URL_')->value();
+ } else {
+ $serviceNamePreserved = $strKey->after('SERVICE_FQDN_')->value();
+ }
}
- // Generated FQDN & URL
- $fqdn = generateFqdn(server: $server, random: "$originalFqdnFor-$uuid", parserVersion: $resource->compose_parsing_version);
- $url = generateUrl(server: $server, random: "$originalFqdnFor-$uuid");
+
+ $originalServiceName = str($serviceName)->replace('_', '-')->value();
+ // Always normalize service names to match docker_compose_domains lookup
+ $serviceName = str($serviceName)->replace('-', '_')->replace('.', '_')->value();
+
+ // Generate BOTH FQDN & URL
+ $fqdn = generateFqdn(server: $server, random: "$originalServiceName-$uuid", parserVersion: $resource->compose_parsing_version);
+ $url = generateUrl(server: $server, random: "$originalServiceName-$uuid");
+
+ // IMPORTANT: SERVICE_FQDN env vars should NOT contain scheme (host only)
+ // But $fqdn variable itself may contain scheme (used for database domain field)
+ // Strip scheme for environment variable values
+ $fqdnValueForEnv = str($fqdn)->after('://')->value();
+
+ // Append port if specified
+ $urlWithPort = $url;
+ $fqdnValueForEnvWithPort = $fqdnValueForEnv;
+ if ($port && is_numeric($port)) {
+ $urlWithPort = "$url:$port";
+ $fqdnValueForEnvWithPort = "$fqdnValueForEnv:$port";
+ }
+
+ // ALWAYS create base SERVICE_FQDN variable (host only, no scheme)
$resource->environment_variables()->firstOrCreate([
- 'key' => $key->value(),
+ 'key' => "SERVICE_FQDN_{$serviceNamePreserved}",
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
], [
- 'value' => $fqdn,
+ 'value' => $fqdnValueForEnv,
'is_preview' => false,
]);
- if ($resource->build_pack === 'dockercompose') {
- // Check if a service with this name actually exists
- $serviceExists = false;
- foreach ($services as $serviceName => $service) {
- $transformedServiceName = str($serviceName)->replace('-', '_')->replace('.', '_')->value();
- if ($transformedServiceName === $fqdnFor) {
- $serviceExists = true;
- break;
- }
- }
- // Only add domain if the service exists
- if ($serviceExists) {
- $domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]);
- $domainExists = data_get($domains->get($fqdnFor), 'domain');
- $envExists = $resource->environment_variables()->where('key', $key->value())->first();
- if (str($domainExists)->replace('http://', '')->replace('https://', '')->value() !== $envExists->value) {
- $envExists->update([
- 'value' => $url,
- ]);
- }
- if (is_null($domainExists)) {
- // Put URL in the domains array instead of FQDN
- $domains->put((string) $fqdnFor, [
- 'domain' => $url,
- ]);
- $resource->docker_compose_domains = $domains->toJson();
- $resource->save();
- }
- }
- }
- } elseif ($command->value() === 'URL') {
- // SERVICE_URL_APP or SERVICE_URL_APP_3000
- // Detect if there's a port suffix
- $parsed = parseServiceEnvironmentVariable($key->value());
- $urlFor = $parsed['service_name'];
- $port = $parsed['port'];
- $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";
- }
+ // ALWAYS create base SERVICE_URL variable (with scheme)
$resource->environment_variables()->firstOrCreate([
- 'key' => $key->value(),
+ 'key' => "SERVICE_URL_{$serviceNamePreserved}",
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
], [
'value' => $url,
'is_preview' => false,
]);
+
+ // If port-specific, ALSO create port-specific pairs
+ if ($parsed['has_port'] && $port) {
+ $resource->environment_variables()->firstOrCreate([
+ 'key' => "SERVICE_FQDN_{$serviceNamePreserved}_{$port}",
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'value' => $fqdnValueForEnvWithPort,
+ 'is_preview' => false,
+ ]);
+
+ $resource->environment_variables()->firstOrCreate([
+ 'key' => "SERVICE_URL_{$serviceNamePreserved}_{$port}",
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'value' => $urlWithPort,
+ 'is_preview' => false,
+ ]);
+ }
+
if ($resource->build_pack === 'dockercompose') {
// Check if a service with this name actually exists
$serviceExists = false;
- foreach ($services as $serviceName => $service) {
- $transformedServiceName = str($serviceName)->replace('-', '_')->replace('.', '_')->value();
- if ($transformedServiceName === $urlFor) {
+ foreach ($services as $serviceNameKey => $service) {
+ $transformedServiceName = str($serviceNameKey)->replace('-', '_')->replace('.', '_')->value();
+ if ($transformedServiceName === $serviceName) {
$serviceExists = true;
break;
}
@@ -600,16 +612,14 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
// Only add domain if the service exists
if ($serviceExists) {
$domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]);
- $domainExists = data_get($domains->get($urlFor), 'domain');
- $envExists = $resource->environment_variables()->where('key', $key->value())->first();
- if ($domainExists !== $envExists->value) {
- $envExists->update([
- 'value' => $urlWithPort,
- ]);
- }
+ $domainExists = data_get($domains->get($serviceName), 'domain');
+
+ // Update domain using URL with port if applicable
+ $domainValue = $port ? $urlWithPort : $url;
+
if (is_null($domainExists)) {
- $domains->put((string) $urlFor, [
- 'domain' => $urlWithPort,
+ $domains->put($serviceName, [
+ 'domain' => $domainValue,
]);
$resource->docker_compose_domains = $domains->toJson();
$resource->save();
@@ -1584,109 +1594,115 @@ function serviceParser(Service $resource): Collection
}
// Get magic environments where we need to preset the FQDN / URL
if ($key->startsWith('SERVICE_FQDN_') || $key->startsWith('SERVICE_URL_')) {
- // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000
+ // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 or SERVICE_URL_APP or SERVICE_URL_APP_3000
+ // ALWAYS create BOTH SERVICE_URL and SERVICE_FQDN pairs regardless of which one is in template
$parsed = parseServiceEnvironmentVariable($key->value());
- if ($key->startsWith('SERVICE_FQDN_')) {
- $urlFor = null;
- $fqdnFor = $parsed['service_name'];
- }
- if ($key->startsWith('SERVICE_URL_')) {
- $fqdnFor = null;
- $urlFor = $parsed['service_name'];
+
+ // Extract service name preserving original case from template
+ $strKey = str($key->value());
+ if ($parsed['has_port']) {
+ if ($strKey->startsWith('SERVICE_URL_')) {
+ $serviceName = $strKey->after('SERVICE_URL_')->beforeLast('_')->value();
+ } elseif ($strKey->startsWith('SERVICE_FQDN_')) {
+ $serviceName = $strKey->after('SERVICE_FQDN_')->beforeLast('_')->value();
+ } else {
+ continue;
+ }
+ } else {
+ if ($strKey->startsWith('SERVICE_URL_')) {
+ $serviceName = $strKey->after('SERVICE_URL_')->value();
+ } elseif ($strKey->startsWith('SERVICE_FQDN_')) {
+ $serviceName = $strKey->after('SERVICE_FQDN_')->value();
+ } else {
+ continue;
+ }
}
+
$port = $parsed['port'];
+ $fqdnFor = $parsed['service_name'];
// Only ServiceApplication has fqdn column, ServiceDatabase does not
$isServiceApplication = $savedService instanceof ServiceApplication;
if ($isServiceApplication && blank($savedService->fqdn)) {
- if ($fqdnFor) {
- $fqdn = generateFqdn(server: $server, random: "$fqdnFor-$uuid", parserVersion: $resource->compose_parsing_version);
- } else {
- $fqdn = generateFqdn(server: $server, random: "{$savedService->name}-$uuid", parserVersion: $resource->compose_parsing_version);
- }
- if ($urlFor) {
- $url = generateUrl($server, "$urlFor-$uuid");
- } else {
- $url = generateUrl($server, "{$savedService->name}-$uuid");
- }
+ $fqdn = generateFqdn(server: $server, random: "$fqdnFor-$uuid", parserVersion: $resource->compose_parsing_version);
+ $url = generateUrl($server, "$fqdnFor-$uuid");
} elseif ($isServiceApplication) {
$fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value();
$url = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value();
} else {
// For ServiceDatabase, generate fqdn/url without saving to the model
- if ($fqdnFor) {
- $fqdn = generateFqdn(server: $server, random: "$fqdnFor-$uuid", parserVersion: $resource->compose_parsing_version);
- } else {
- $fqdn = generateFqdn(server: $server, random: "{$savedService->name}-$uuid", parserVersion: $resource->compose_parsing_version);
- }
- if ($urlFor) {
- $url = generateUrl($server, "$urlFor-$uuid");
- } else {
- $url = generateUrl($server, "{$savedService->name}-$uuid");
- }
+ $fqdn = generateFqdn(server: $server, random: "$fqdnFor-$uuid", parserVersion: $resource->compose_parsing_version);
+ $url = generateUrl($server, "$fqdnFor-$uuid");
}
+ // IMPORTANT: SERVICE_FQDN env vars should NOT contain scheme (host only)
+ // But $fqdn variable itself may contain scheme (used for database domain field)
+ // Strip scheme for environment variable values
+ $fqdnValueForEnv = str($fqdn)->after('://')->value();
+
if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) {
$path = $value->value();
if ($path !== '/') {
$fqdn = "$fqdn$path";
$url = "$url$path";
+ $fqdnValueForEnv = "$fqdnValueForEnv$path";
}
}
- $fqdnWithPort = $fqdn;
+
$urlWithPort = $url;
+ $fqdnValueForEnvWithPort = $fqdnValueForEnv;
if ($fqdn && $port) {
- $fqdnWithPort = "$fqdn:$port";
+ $fqdnValueForEnvWithPort = "$fqdnValueForEnv:$port";
}
if ($url && $port) {
$urlWithPort = "$url:$port";
}
+
// Only save fqdn to ServiceApplication, not ServiceDatabase
if ($isServiceApplication && is_null($savedService->fqdn)) {
+ // Save URL (with scheme) to database, not FQDN
if ((int) $resource->compose_parsing_version >= 5 && version_compare(config('constants.coolify.version'), '4.0.0-beta.420.7', '>=')) {
- if ($fqdnFor) {
- $savedService->fqdn = $fqdnWithPort;
- }
- if ($urlFor) {
- $savedService->fqdn = $urlWithPort;
- }
+ $savedService->fqdn = $urlWithPort;
} else {
- $savedService->fqdn = $fqdnWithPort;
+ $savedService->fqdn = $urlWithPort;
}
$savedService->save();
}
- if (! $parsed['has_port']) {
+
+ // ALWAYS create BOTH base SERVICE_URL and SERVICE_FQDN pairs (without port)
+ $resource->environment_variables()->updateOrCreate([
+ 'key' => "SERVICE_FQDN_{$serviceName}",
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'value' => $fqdnValueForEnv,
+ 'is_preview' => false,
+ ]);
+
+ $resource->environment_variables()->updateOrCreate([
+ 'key' => "SERVICE_URL_{$serviceName}",
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'value' => $url,
+ 'is_preview' => false,
+ ]);
+
+ // For port-specific variables, ALSO create port-specific pairs
+ // If template variable has port, create both URL and FQDN with port suffix
+ if ($parsed['has_port'] && $port) {
$resource->environment_variables()->updateOrCreate([
- 'key' => $key->value(),
+ 'key' => "SERVICE_FQDN_{$serviceName}_{$port}",
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
], [
- 'value' => $fqdn,
+ 'value' => $fqdnValueForEnvWithPort,
'is_preview' => false,
]);
+
$resource->environment_variables()->updateOrCreate([
- 'key' => $key->value(),
- 'resourceable_type' => get_class($resource),
- 'resourceable_id' => $resource->id,
- ], [
- 'value' => $url,
- 'is_preview' => false,
- ]);
- }
- if ($parsed['has_port']) {
- // For port-specific variables (e.g., SERVICE_FQDN_UMAMI_3000),
- // keep the port suffix in the key and use the URL with port
- $resource->environment_variables()->updateOrCreate([
- 'key' => $key->value(),
- 'resourceable_type' => get_class($resource),
- 'resourceable_id' => $resource->id,
- ], [
- 'value' => $fqdnWithPort,
- 'is_preview' => false,
- ]);
- $resource->environment_variables()->updateOrCreate([
- 'key' => $key->value(),
+ 'key' => "SERVICE_URL_{$serviceName}_{$port}",
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
], [
diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php
index a6d427a6b..3fff2c090 100644
--- a/bootstrap/helpers/services.php
+++ b/bootstrap/helpers/services.php
@@ -115,65 +115,170 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
$resource->save();
}
- $serviceName = str($resource->name)->upper()->replace('-', '_')->replace('.', '_');
- $resource->service->environment_variables()->where('key', 'LIKE', "SERVICE_FQDN_{$serviceName}%")->delete();
- $resource->service->environment_variables()->where('key', 'LIKE', "SERVICE_URL_{$serviceName}%")->delete();
+ // Extract SERVICE_URL and SERVICE_FQDN variable names from the compose template
+ // to ensure we use the exact names defined in the template (which may be abbreviated)
+ // IMPORTANT: Only extract variables that are DIRECTLY DECLARED for this service,
+ // not variables that are merely referenced from other services
+ $serviceConfig = data_get($dockerCompose, "services.{$name}");
+ $environment = data_get($serviceConfig, 'environment', []);
+ $templateVariableNames = [];
+
+ foreach ($environment as $key => $value) {
+ if (is_int($key) && is_string($value)) {
+ // List-style: "- SERVICE_URL_APP_3000" or "- SERVICE_URL_APP_3000=value"
+ // Extract variable name (before '=' if present)
+ $envVarName = str($value)->before('=')->trim();
+ // Only include if it's a direct declaration (not a reference like ${VAR})
+ // Direct declarations look like: SERVICE_URL_APP or SERVICE_URL_APP_3000
+ // References look like: NEXT_PUBLIC_URL=${SERVICE_URL_APP}
+ if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) {
+ $templateVariableNames[] = $envVarName->value();
+ }
+ } elseif (is_string($key)) {
+ // Map-style: "SERVICE_URL_APP_3000: value" or "SERVICE_FQDN_DB: localhost"
+ $envVarName = str($key);
+ if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) {
+ $templateVariableNames[] = $envVarName->value();
+ }
+ }
+ // DO NOT extract variables that are only referenced with ${VAR_NAME} syntax
+ // Those belong to other services and will be updated when THOSE services are updated
+ }
+
+ // Remove duplicates
+ $templateVariableNames = array_unique($templateVariableNames);
+
+ // Extract unique service names to process (preserving the original case from template)
+ // This allows us to create both URL and FQDN pairs regardless of which one is in the template
+ $serviceNamesToProcess = [];
+ foreach ($templateVariableNames as $templateVarName) {
+ $parsed = parseServiceEnvironmentVariable($templateVarName);
+
+ // Extract the original service name with case preserved from the template
+ $strKey = str($templateVarName);
+ if ($parsed['has_port']) {
+ // For port-specific variables, get the name between SERVICE_URL_/SERVICE_FQDN_ and the last underscore
+ if ($strKey->startsWith('SERVICE_URL_')) {
+ $serviceName = $strKey->after('SERVICE_URL_')->beforeLast('_')->value();
+ } elseif ($strKey->startsWith('SERVICE_FQDN_')) {
+ $serviceName = $strKey->after('SERVICE_FQDN_')->beforeLast('_')->value();
+ } else {
+ continue;
+ }
+ } else {
+ // For base variables, get everything after SERVICE_URL_/SERVICE_FQDN_
+ if ($strKey->startsWith('SERVICE_URL_')) {
+ $serviceName = $strKey->after('SERVICE_URL_')->value();
+ } elseif ($strKey->startsWith('SERVICE_FQDN_')) {
+ $serviceName = $strKey->after('SERVICE_FQDN_')->value();
+ } else {
+ continue;
+ }
+ }
+
+ // Use lowercase key for array indexing (to group case variations together)
+ $serviceKey = str($serviceName)->lower()->value();
+
+ // Track both base service name and port-specific variant
+ if (! isset($serviceNamesToProcess[$serviceKey])) {
+ $serviceNamesToProcess[$serviceKey] = [
+ 'base' => $serviceName, // Preserve original case
+ 'ports' => [],
+ ];
+ }
+
+ // If this variable has a port, track it
+ if ($parsed['has_port'] && $parsed['port']) {
+ $serviceNamesToProcess[$serviceKey]['ports'][] = $parsed['port'];
+ }
+ }
+
+ // Delete all existing SERVICE_URL and SERVICE_FQDN variables for these service names
+ // We need to delete both URL and FQDN variants, with and without ports
+ foreach ($serviceNamesToProcess as $serviceInfo) {
+ $serviceName = $serviceInfo['base'];
+
+ // Delete base variables
+ $resource->service->environment_variables()->where('key', "SERVICE_URL_{$serviceName}")->delete();
+ $resource->service->environment_variables()->where('key', "SERVICE_FQDN_{$serviceName}")->delete();
+
+ // Delete port-specific variables
+ foreach ($serviceInfo['ports'] as $port) {
+ $resource->service->environment_variables()->where('key', "SERVICE_URL_{$serviceName}_{$port}")->delete();
+ $resource->service->environment_variables()->where('key', "SERVICE_FQDN_{$serviceName}_{$port}")->delete();
+ }
+ }
if ($resource->fqdn) {
$resourceFqdns = str($resource->fqdn)->explode(',');
$resourceFqdns = $resourceFqdns->first();
- $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_')->replace('.', '_');
$url = Url::fromString($resourceFqdns);
$port = $url->getPort();
$path = $url->getPath();
+
+ // Prepare URL value (with scheme and host)
$urlValue = $url->getScheme().'://'.$url->getHost();
$urlValue = ($path === '/') ? $urlValue : $urlValue.$path;
- $resource->service->environment_variables()->updateOrCreate([
- 'resourceable_type' => Service::class,
- 'resourceable_id' => $resource->service_id,
- 'key' => $variableName,
- ], [
- 'value' => $urlValue,
- 'is_preview' => false,
- ]);
- if ($port) {
- $variableName = $variableName."_$port";
+
+ // Prepare FQDN value (host only, no scheme)
+ $fqdnHost = $url->getHost();
+ $fqdnValue = str($fqdnHost)->after('://');
+ if ($path !== '/') {
+ $fqdnValue = $fqdnValue.$path;
+ }
+
+ // For each service name found in template, create BOTH SERVICE_URL and SERVICE_FQDN pairs
+ foreach ($serviceNamesToProcess as $serviceInfo) {
+ $serviceName = $serviceInfo['base'];
+ $ports = array_unique($serviceInfo['ports']);
+
+ // ALWAYS create base pair (without port)
$resource->service->environment_variables()->updateOrCreate([
'resourceable_type' => Service::class,
'resourceable_id' => $resource->service_id,
- 'key' => $variableName,
+ 'key' => "SERVICE_URL_{$serviceName}",
], [
'value' => $urlValue,
'is_preview' => false,
]);
- }
- $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_')->replace('.', '_');
- $fqdn = Url::fromString($resourceFqdns);
- $port = $fqdn->getPort();
- $path = $fqdn->getPath();
- $fqdn = $fqdn->getHost();
- $fqdnValue = str($fqdn)->after('://');
- if ($path !== '/') {
- $fqdnValue = $fqdnValue.$path;
- }
- $resource->service->environment_variables()->updateOrCreate([
- 'resourceable_type' => Service::class,
- 'resourceable_id' => $resource->service_id,
- 'key' => $variableName,
- ], [
- 'value' => $fqdnValue,
- 'is_preview' => false,
- ]);
- if ($port) {
- $variableName = $variableName."_$port";
+
$resource->service->environment_variables()->updateOrCreate([
'resourceable_type' => Service::class,
'resourceable_id' => $resource->service_id,
- 'key' => $variableName,
+ 'key' => "SERVICE_FQDN_{$serviceName}",
], [
'value' => $fqdnValue,
'is_preview' => false,
]);
+
+ // Create port-specific pairs for each port found in template or FQDN
+ $allPorts = $ports;
+ if ($port && ! in_array($port, $allPorts)) {
+ $allPorts[] = $port;
+ }
+
+ foreach ($allPorts as $portNum) {
+ $urlWithPort = $urlValue.':'.$portNum;
+ $fqdnWithPort = $fqdnValue.':'.$portNum;
+
+ $resource->service->environment_variables()->updateOrCreate([
+ 'resourceable_type' => Service::class,
+ 'resourceable_id' => $resource->service_id,
+ 'key' => "SERVICE_URL_{$serviceName}_{$portNum}",
+ ], [
+ 'value' => $urlWithPort,
+ 'is_preview' => false,
+ ]);
+
+ $resource->service->environment_variables()->updateOrCreate([
+ 'resourceable_type' => Service::class,
+ 'resourceable_id' => $resource->service_id,
+ 'key' => "SERVICE_FQDN_{$serviceName}_{$portNum}",
+ ], [
+ 'value' => $fqdnWithPort,
+ 'is_preview' => false,
+ ]);
+ }
}
}
} catch (\Throwable $e) {
diff --git a/resources/views/livewire/global-search.blade.php b/resources/views/livewire/global-search.blade.php
index 7a9868c06..01d3131f6 100644
--- a/resources/views/livewire/global-search.blade.php
+++ b/resources/views/livewire/global-search.blade.php
@@ -254,7 +254,8 @@
class="fixed top-0 left-0 z-99 flex items-start justify-center w-screen h-screen pt-[10vh]">
-
-
+
@@ -311,8 +311,8 @@ class="mt-2 bg-white dark:bg-coolgray-100 rounded-lg shadow-xl ring-1 ring-neutr
class="text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white">
@@ -327,13 +327,11 @@ class="text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:
@if ($loadingServers)
-
-
@if ($loadingDestinations)
-
-
-
+
+
+
@@ -422,25 +416,22 @@ class="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-coolgray-200 rounded-lg
@elseif (count($availableDestinations) > 0)
@foreach ($availableDestinations as $index => $destination)
-