From 2edf2338de07cff658c63a881ef39bb787327b6c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:41:25 +0100 Subject: [PATCH] fix: enhance getRequiredPort to support map-style environment variables for SERVICE_URL and SERVICE_FQDN --- app/Models/ServiceApplication.php | 21 ++++- tests/Unit/ServiceRequiredPortTest.php | 117 +++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 3 deletions(-) diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index e457dbccd..aef74b402 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -208,10 +208,25 @@ public function getRequiredPort(): ?int // 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 $envVar) { - if (is_string($envVar)) { + 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($envVar)->before('=')->trim(); + $envVarName = str($value)->before('=')->trim(); + + // 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); // Only process direct declarations if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) { diff --git a/tests/Unit/ServiceRequiredPortTest.php b/tests/Unit/ServiceRequiredPortTest.php index 70bf2bca2..2ad345c44 100644 --- a/tests/Unit/ServiceRequiredPortTest.php +++ b/tests/Unit/ServiceRequiredPortTest.php @@ -151,3 +151,120 @@ expect($result)->toBeFalse(); }); + +it('detects port from map-style SERVICE_URL environment variable', function () { + $yaml = <<<'YAML' +services: + trigger: + environment: + SERVICE_URL_TRIGGER_3000: "" + OTHER_VAR: value +YAML; + + $service = Mockery::mock(Service::class)->makePartial(); + $service->docker_compose_raw = $yaml; + $service->shouldReceive('getRequiredPort')->andReturn(null); + + $app = Mockery::mock(ServiceApplication::class)->makePartial(); + $app->name = 'trigger'; + $app->shouldReceive('getAttribute')->with('service')->andReturn($service); + $app->service = $service; + + // Call the actual getRequiredPort method + $result = $app->getRequiredPort(); + + expect($result)->toBe(3000); +}); + +it('detects port from map-style SERVICE_FQDN environment variable', function () { + $yaml = <<<'YAML' +services: + langfuse: + environment: + SERVICE_FQDN_LANGFUSE_3000: localhost + DATABASE_URL: postgres://... +YAML; + + $service = Mockery::mock(Service::class)->makePartial(); + $service->docker_compose_raw = $yaml; + $service->shouldReceive('getRequiredPort')->andReturn(null); + + $app = Mockery::mock(ServiceApplication::class)->makePartial(); + $app->name = 'langfuse'; + $app->shouldReceive('getAttribute')->with('service')->andReturn($service); + $app->service = $service; + + $result = $app->getRequiredPort(); + + expect($result)->toBe(3000); +}); + +it('returns null for map-style environment without port', function () { + $yaml = <<<'YAML' +services: + db: + environment: + SERVICE_FQDN_DB: localhost + SERVICE_URL_DB: http://localhost +YAML; + + $service = Mockery::mock(Service::class)->makePartial(); + $service->docker_compose_raw = $yaml; + $service->shouldReceive('getRequiredPort')->andReturn(null); + + $app = Mockery::mock(ServiceApplication::class)->makePartial(); + $app->name = 'db'; + $app->shouldReceive('getAttribute')->with('service')->andReturn($service); + $app->service = $service; + + $result = $app->getRequiredPort(); + + expect($result)->toBeNull(); +}); + +it('handles list-style environment with port', function () { + $yaml = <<<'YAML' +services: + umami: + environment: + - SERVICE_URL_UMAMI_3000 + - DATABASE_URL=postgres://db/umami +YAML; + + $service = Mockery::mock(Service::class)->makePartial(); + $service->docker_compose_raw = $yaml; + $service->shouldReceive('getRequiredPort')->andReturn(null); + + $app = Mockery::mock(ServiceApplication::class)->makePartial(); + $app->name = 'umami'; + $app->shouldReceive('getAttribute')->with('service')->andReturn($service); + $app->service = $service; + + $result = $app->getRequiredPort(); + + expect($result)->toBe(3000); +}); + +it('prioritizes first port found in environment', function () { + $yaml = <<<'YAML' +services: + multi: + environment: + SERVICE_URL_MULTI_3000: "" + SERVICE_URL_MULTI_8080: "" +YAML; + + $service = Mockery::mock(Service::class)->makePartial(); + $service->docker_compose_raw = $yaml; + $service->shouldReceive('getRequiredPort')->andReturn(null); + + $app = Mockery::mock(ServiceApplication::class)->makePartial(); + $app->name = 'multi'; + $app->shouldReceive('getAttribute')->with('service')->andReturn($service); + $app->service = $service; + + $result = $app->getRequiredPort(); + + // Should return one of the ports (depends on array iteration order) + expect($result)->toBeIn([3000, 8080]); +});