From 8ebc17cf73a21709ec6231a9f87ea23b8865a625 Mon Sep 17 00:00:00 2001 From: Roly Gutierrez Date: Fri, 30 Jan 2026 11:56:10 -0400 Subject: [PATCH 1/2] FOUR-29068 Add NAYRA_REST_API_HOST support to ScriptDockerNayraTrait When NAYRA_REST_API_HOST is set in .env, the trait uses that URL directly instead of Docker discovery, enabling Nayra script execution where automatic discovery fails (macOS/Docker Desktop, port conflicts, external Nayra services). Problem (without this change): - ScriptDockerNayraTrait uses Docker discovery (docker inspect, hostname -i) to build http://{ip}:{port}/run_script - On macOS/Docker Desktop: IPv6 from hostname -i produces invalid URLs; container IP may be unreachable from host - Port 8080 conflict: nginx or other service responds instead of Nayra - External Nayra: no way to configure a fixed URL Solution: - Add getNayraBaseUrl(): returns NAYRA_REST_API_HOST if set, else getNayraInstanceUrl() - Update handleNayraDocker() to use getNayraBaseUrl() - Update ensureNayraServerIsRunning(): when NAYRA_REST_API_HOST is set and connection fails, throw ScriptException instead of bringUpNayra() Configuration (.env): NAYRA_REST_API_HOST=http://localhost:8081 Backward compatible: if not set, original Docker discovery flow is used. Affected file: ProcessMaker/Models/ScriptDockerNayraTrait.php --- .../Models/ScriptDockerNayraTrait.php | 33 +++- .../Models/ScriptDockerNayraTraitTest.php | 167 ++++++++++++++++++ 2 files changed, 193 insertions(+), 7 deletions(-) create mode 100644 tests/unit/ProcessMaker/Models/ScriptDockerNayraTraitTest.php diff --git a/ProcessMaker/Models/ScriptDockerNayraTrait.php b/ProcessMaker/Models/ScriptDockerNayraTrait.php index 8a2aa83625..c9a05f438a 100644 --- a/ProcessMaker/Models/ScriptDockerNayraTrait.php +++ b/ProcessMaker/Models/ScriptDockerNayraTrait.php @@ -45,12 +45,9 @@ public function handleNayraDocker(string $code, array $data, array $config, $tim 'timeout' => $timeout, ]; $body = json_encode($params); - $servers = self::getNayraAddresses(); - if (!$servers) { - $this->bringUpNayra(); - } - $baseUrl = $this->getNayraInstanceUrl(); - $url = $baseUrl . '/run_script'; + + $baseUrl = $this->getNayraBaseUrl(); + $url = rtrim($baseUrl, '/') . '/run_script'; $this->ensureNayraServerIsRunning($baseUrl); $ch = curl_init($url); @@ -78,9 +75,28 @@ public function handleNayraDocker(string $code, array $data, array $config, $tim return $result; } - private function getNayraInstanceUrl() + /** + * Get the base URL for the Nayra service. + * Uses NAYRA_REST_API_HOST if set, otherwise Docker discovery. + */ + private function getNayraBaseUrl(): string + { + $restApiHost = config('app.nayra_rest_api_host'); + if (!empty($restApiHost)) { + return rtrim($restApiHost, '/'); + } + + return $this->getNayraInstanceUrl(); + } + + private function getNayraInstanceUrl(): string { $servers = self::getNayraAddresses(); + if (!$servers) { + $this->bringUpNayra(); + $servers = self::getNayraAddresses(); + } + return $this->schema . '://' . $servers[0] . ':' . $this->getNayraPort(); } @@ -106,6 +122,9 @@ private function ensureNayraServerIsRunning(string $url) { $header = @get_headers($url); if (!$header) { + if (!empty(config('app.nayra_rest_api_host'))) { + throw new ScriptException('Could not connect to the nayra container'); + } $this->bringUpNayra(true); } } diff --git a/tests/unit/ProcessMaker/Models/ScriptDockerNayraTraitTest.php b/tests/unit/ProcessMaker/Models/ScriptDockerNayraTraitTest.php new file mode 100644 index 0000000000..2822059d11 --- /dev/null +++ b/tests/unit/ProcessMaker/Models/ScriptDockerNayraTraitTest.php @@ -0,0 +1,167 @@ +create([ + 'language' => Base::NAYRA_LANG, + ]); + + return new class($scriptExecutor) extends Base { + public function config($code, array $dockerConfig): array + { + return []; + } + }; + } + + protected function tearDown(): void + { + Base::clearNayraAddresses(); + parent::tearDown(); + } + + public function testGetNayraAddressesReturnsNullWhenNotSet(): void + { + Base::clearNayraAddresses(); + $this->assertNull(Base::getNayraAddresses()); + } + + public function testSetAndGetNayraAddresses(): void + { + $addresses = ['192.168.1.100', '192.168.1.101']; + Base::setNayraAddresses($addresses); + $this->assertEquals($addresses, Base::getNayraAddresses()); + } + + public function testClearNayraAddresses(): void + { + Base::setNayraAddresses(['192.168.1.100']); + Base::clearNayraAddresses(); + $this->assertNull(Base::getNayraAddresses()); + } + + public function testGetNayraBaseUrlReturnsNayraRestApiHostWhenConfigured(): void + { + Config::set('app.nayra_rest_api_host', 'http://nayra.example.com:9000'); + $runner = $this->createNayraRunner(); + + // getNayraBaseUrl is private - test via handleNayraDocker which uses it. + // When NAYRA_REST_API_HOST is set and server is unreachable, ensureNayraServerIsRunning + // throws immediately (does not try bringUpNayra). + $this->expectException(ScriptException::class); + $this->expectExceptionMessage('Could not connect to the nayra container'); + + $runner->handleNayraDocker( + 'createNayraRunner(); + + // URL used should be http://nayra.example.com/run_script (no double slash) + // We verify by checking the exception - if URL were wrong we might get different error. + // The key: NAYRA_REST_API_HOST with trailing slash is trimmed. + $this->expectException(ScriptException::class); + $this->expectExceptionMessage('Could not connect to the nayra container'); + + $runner->handleNayraDocker( + 'createNayraRunner(); + + $this->expectException(ScriptException::class); + $this->expectExceptionMessage('Could not connect to the nayra container'); + + $runner->handleNayraDocker( + 'createNayraRunner(); + + // Verifies that NAYRA_REST_API_HOST is used (ensureNayraServerIsRunning throws + // immediately instead of trying Docker bringUpNayra) + $this->expectException(ScriptException::class); + $this->expectExceptionMessage('Could not connect to the nayra container'); + + $runner->handleNayraDocker( + 'createNayraRunner(); + + // Just verify it reaches ensureNayraServerIsRunning (env parsing happens before) + $this->expectException(ScriptException::class); + $this->expectExceptionMessage('Could not connect to the nayra container'); + + $runner->handleNayraDocker( + ' 'value'], + ['config' => 'data'], + 120, + ['API_TOKEN=secret', 'HOST_URL=http://localhost'] + ); + } + + public function testCachedAddressesPersistAcrossCalls(): void + { + Base::clearNayraAddresses(); + $this->assertNull(Base::getNayraAddresses()); + + Base::setNayraAddresses(['10.0.0.5']); + $this->assertEquals(['10.0.0.5'], Base::getNayraAddresses()); + + Base::setNayraAddresses(['192.168.1.1', '192.168.1.2']); + $this->assertEquals(['192.168.1.1', '192.168.1.2'], Base::getNayraAddresses()); + } +} From 4ddaa3677322ca9959f881def318df4d5a8a328c Mon Sep 17 00:00:00 2001 From: Roly Gutierrez Date: Fri, 30 Jan 2026 12:12:58 -0400 Subject: [PATCH 2/2] FOUR-29068 Fix php-cs-fixer observations. --- ProcessMaker/Models/ScriptDockerNayraTrait.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ProcessMaker/Models/ScriptDockerNayraTrait.php b/ProcessMaker/Models/ScriptDockerNayraTrait.php index c9a05f438a..18b27778e2 100644 --- a/ProcessMaker/Models/ScriptDockerNayraTrait.php +++ b/ProcessMaker/Models/ScriptDockerNayraTrait.php @@ -11,9 +11,9 @@ use ProcessMaker\Exception\ScriptException; use ProcessMaker\Facades\Docker; use ProcessMaker\ScriptRunners\Base; -use RuntimeException; -use Psr\Container\NotFoundExceptionInterface; use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; +use RuntimeException; use UnexpectedValueException; /** @@ -21,7 +21,6 @@ */ trait ScriptDockerNayraTrait { - private $schema = 'http'; /** @@ -72,6 +71,7 @@ public function handleNayraDocker(string $code, array $data, array $config, $tim ]); throw new ScriptException($result); } + return $result; } @@ -108,6 +108,7 @@ private function getDockerLogs($instanceName) if ($status) { return 'Error getting logs from Nayra Docker: ' . implode("\n", $logs); } + return implode("\n", $logs); } @@ -225,7 +226,7 @@ private static function findNayraAddresses($docker, $instanceName, $times): bool . ($nayraDockerNetwork ? "'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'" : "'{{ .NetworkSettings.IPAddress }}'" - ) + ) . " {$instanceName}_nayra 2>/dev/null", $output, $status @@ -236,6 +237,7 @@ private static function findNayraAddresses($docker, $instanceName, $times): bool } if ($ip) { self::setNayraAddresses([$ip]); + return true; } } @@ -299,6 +301,7 @@ public static function clearNayraAddresses() private static function isCacheArrayStore(): bool { $cacheDriver = Cache::getFacadeRoot()->getStore(); + return $cacheDriver instanceof ArrayStore; }