diff --git a/ProcessMaker/Console/Commands/BuildScriptExecutors.php b/ProcessMaker/Console/Commands/BuildScriptExecutors.php index 07bb9b40eb..3e919c1efb 100644 --- a/ProcessMaker/Console/Commands/BuildScriptExecutors.php +++ b/ProcessMaker/Console/Commands/BuildScriptExecutors.php @@ -4,10 +4,12 @@ use Exception; use Illuminate\Console\Command; +use Illuminate\Support\Facades\Cache; use ProcessMaker\Events\BuildScriptExecutor; use ProcessMaker\Exception\InvalidDockerImageException; use ProcessMaker\Facades\Docker; use ProcessMaker\Models\ScriptExecutor; +use UnexpectedValueException; class BuildScriptExecutors extends Command { @@ -157,6 +159,36 @@ public function buildExecutor() $command = Docker::command() . " build --build-arg SDK_DIR=./sdk -t {$image} -f {$packagePath}/Dockerfile.custom {$packagePath}"; + $this->execCommand($command); + + $isNayra = $scriptExecutor->language === 'nayra'; + if ($isNayra) { + $instanceName = config('app.instance'); + $this->info('Stop existing nayra container'); + $this->execCommand(Docker::command() . " stop {$instanceName}_nayra 2>&1 || true"); + $this->execCommand(Docker::command() . " rm {$instanceName}_nayra 2>&1 || true"); + $this->info('Bring up the nayra container'); + $this->execCommand(Docker::command() . ' run -d --name ' . $instanceName . '_nayra ' . $image); + $this->info('Get IP address of the nayra container'); + $ip = ''; + for ($i = 0; $i < 10; $i++) { + $ip = exec(Docker::command() . " inspect --format '{{ .NetworkSettings.IPAddress }}' {$instanceName}_nayra"); + if ($ip) { + $this->info('Nayra container IP: ' . $ip); + Cache::forever('nayra_ips', [$ip]); + $this->sendEvent(0, 'done'); + break; + } + sleep(1); + } + if (!$ip) { + throw new UnexpectedValueException('Could not get IP address of the nayra container'); + } + } + } + + private function execCommand(string $command) + { if ($this->userId) { $this->runProc( $command, diff --git a/ProcessMaker/Models/ScriptDockerNayraTrait.php b/ProcessMaker/Models/ScriptDockerNayraTrait.php new file mode 100644 index 0000000000..fc50e26e07 --- /dev/null +++ b/ProcessMaker/Models/ScriptDockerNayraTrait.php @@ -0,0 +1,145 @@ + uniqid('script_', true), + 'script' => $code, + 'data' => $data, + 'config' => $config, + 'envVariables' => $envVariables, + 'timeout' => $timeout, + ]; + $body = json_encode($params); + $servers = Cache::get('nayra_ips'); + if (!$servers) { + $url = config('app.nayra_rest_api_host') . '/run_script'; + } else { + $index = array_rand($servers); + $url = 'http://' . $servers[$index] . ':8080/run_script'; + $this->ensureNayraServerIsRunning('http://' . $servers[$index] . ':8080'); + } + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Content-Length: ' . strlen($body), + ]); + $result = curl_exec($ch); + curl_close($ch); + $httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($httpStatus !== 200) { + Log::error('Error executing script with Nayra Docker', [ + 'url' => $url, + 'httpStatus' => $httpStatus, + 'result' => $result, + ]); + throw new ScriptException($result); + } + return $result; + } + + /** + * Ensure that the Nayra server is running. + * + * @param string $url URL of the Nayra server + * @return void + * @throws ScriptException If cannot connect to Nayra Service + */ + private function ensureNayraServerIsRunning(string $url) + { + $header = @get_headers($url); + if (!$header) { + $this->bringUpNayra($url); + } + } + + /** + * Bring up Nayra and check the provided URL. + * + * @param string $url The URL to check + * @return void + */ + private function bringUpNayra(string $url) + { + $docker = Docker::command(); + $instanceName = config('app.instance'); + $image = $this->scriptExecutor->dockerImageName(); + exec($docker . " stop {$instanceName}_nayra 2>&1 || true"); + exec($docker . " rm {$instanceName}_nayra 2>&1 || true"); + exec($docker . ' run -d --name ' . $instanceName . '_nayra ' . $image . ' &', $output, $status); + if ($status) { + Log::error('Error starting Nayra Docker', [ + 'output' => $output, + 'status' => $status, + ]); + throw new ScriptException('Error starting Nayra Docker'); + } + $this->waitContainerNetwork($docker, $instanceName); + $this->nayraServiceIsRunning($url); + } + + /** + * Waits for the container network to be ready. + * + * @param Docker $docker The Docker instance. + * @param string $instanceName The name of the container instance. + * @return string The IP of the container. + */ + private function waitContainerNetwork($docker, $instanceName): string + { + $ip = ''; + for ($i = 0; $i < 30; $i++) { + $ip = exec($docker . " inspect --format '{{ .NetworkSettings.IPAddress }}' {$instanceName}_nayra"); + if ($ip) { + Cache::forever('nayra_ips', [$ip]); + return $ip; + } + sleep(1); + } + throw new ScriptException('Could not get address of the nayra container'); + } + + /** + * Checks if the Nayra service is running. + * + * @param string $url The URL of the Nayra service. + * @return bool Returns true if the Nayra service is running, false otherwise. + */ + private function nayraServiceIsRunning($url): bool + { + for ($i = 0; $i < 30; $i++) { + $status = @get_headers($url); + if ($status) { + return true; + } + sleep(1); + } + throw new ScriptException('Could not connect to the nayra container'); + } +} diff --git a/ProcessMaker/ScriptRunners/Base.php b/ProcessMaker/ScriptRunners/Base.php index cf1232d8e9..f58456227b 100644 --- a/ProcessMaker/ScriptRunners/Base.php +++ b/ProcessMaker/ScriptRunners/Base.php @@ -2,11 +2,14 @@ namespace ProcessMaker\ScriptRunners; +use Carbon\Carbon; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; use ProcessMaker\GenerateAccessToken; use ProcessMaker\Models\EnvironmentVariable; use ProcessMaker\Models\ScriptDockerBindingFilesTrait; use ProcessMaker\Models\ScriptDockerCopyingFilesTrait; +use ProcessMaker\Models\ScriptDockerNayraTrait; use ProcessMaker\Models\ScriptExecutor; use ProcessMaker\Models\User; use RuntimeException; @@ -15,6 +18,9 @@ abstract class Base { use ScriptDockerCopyingFilesTrait; use ScriptDockerBindingFilesTrait; + use ScriptDockerNayraTrait; + + const NAYRA_LANG = 'php-nayra'; private $tokenId = ''; @@ -38,7 +44,7 @@ abstract public function config($code, array $dockerConfig); /** * Set the script executor * - * @var \ProcessMaker\Models\User + * @var \ProcessMaker\Models\ScriptExecutor */ private $scriptExecutor; @@ -70,13 +76,25 @@ public function run($code, array $data, array $config, $timeout, ?User $user) // Create tokens for the SDK if a user is set $token = null; if ($user) { - $token = new GenerateAccessToken($user); - $environmentVariables[] = 'API_TOKEN=' . $token->getToken(); + $expires = Carbon::now()->addWeek(); + $accessToken = Cache::remember('script-runner-' . $user->id, $expires, function () use ($user) { + $user->removeOldRunScriptTokens(); + $token = new GenerateAccessToken($user); + return $token->getToken(); + }); + $environmentVariables[] = 'API_TOKEN=' . $accessToken; $environmentVariables[] = 'API_HOST=' . config('app.docker_host_url') . '/api/1.0'; $environmentVariables[] = 'APP_URL=' . config('app.docker_host_url'); $environmentVariables[] = 'API_SSL_VERIFY=' . (config('app.api_ssl_verify') ? '1' : '0'); } + // Nayra Executor + $isNayra = $this->scriptExecutor->language === self::NAYRA_LANG; + if ($isNayra) { + $response = $this->handleNayraDocker($code, $data, $config, $timeout, $environmentVariables); + return json_decode($response, true); + } + if ($environmentVariables) { $parameters = '-e ' . implode(' -e ', $environmentVariables); } else { diff --git a/config/script-runners.php b/config/script-runners.php index 694ce78356..8451df1da8 100644 --- a/config/script-runners.php +++ b/config/script-runners.php @@ -12,4 +12,15 @@ | https://github.com/ProcessMaker/docker-executor-node | */ + 'php-nayra' => [ + 'name' => 'PHP (µService)', + 'runner' => 'PhpRunner', + 'mime_type' => 'application/x-php', + 'options' => ['invokerPackage' => 'ProcessMaker\\Client'], + 'init_dockerfile' => [ + ], + 'package_path' => base_path('/docker-services/nayra'), + 'package_version' => '1.0.0', + 'sdk' => '', + ], ]; diff --git a/docker-services/nayra/Dockerfile b/docker-services/nayra/Dockerfile new file mode 100644 index 0000000000..e949c2f5dc --- /dev/null +++ b/docker-services/nayra/Dockerfile @@ -0,0 +1,2 @@ +FROM processmaker4dev/nayra-engine:next +# The tag :next is temporal until the official release of the engine diff --git a/resources/js/processes/scripts/components/ScriptEditor.vue b/resources/js/processes/scripts/components/ScriptEditor.vue index f599b65302..08060d18f8 100644 --- a/resources/js/processes/scripts/components/ScriptEditor.vue +++ b/resources/js/processes/scripts/components/ScriptEditor.vue @@ -298,9 +298,9 @@
{{ preview.error.exception }}
-
- {{ preview.error.message }} -
+
{{
+                            preview.error.message
+                          }}
@@ -552,7 +552,7 @@ export default { return this.packageAi && this.newCode !== "" && !this.changesApplied && !this.isDiffEditor; }, language() { - return this.scriptExecutor.language; + return this.scriptExecutor.language === 'php-nayra' ? 'php' : this.scriptExecutor.language; }, autosaveApiCall() { return () => { @@ -887,6 +887,7 @@ export default { if (this.script.code === "[]") { switch (this.script.language) { case "php": + case "php-nayra": this.code = Vue.filter("php")(this.boilerPlateTemplate); break; case "lua": diff --git a/upgrades/2024_06_12_150527_create_nayra_script_executor.php b/upgrades/2024_06_12_150527_create_nayra_script_executor.php new file mode 100644 index 0000000000..c76bd1faaa --- /dev/null +++ b/upgrades/2024_06_12_150527_create_nayra_script_executor.php @@ -0,0 +1,45 @@ +exists(); + if (!$exists) { + $scriptExecutor = new ScriptExecutor(); + $scriptExecutor->language = Base::NAYRA_LANG; + $scriptExecutor->title = 'Nayra (µService)'; + $scriptExecutor->save(); + } + } + + /** + * Reverse the upgrade migration. + * + * @return void + */ + public function down() + { + try { + $existsScriptsUsingNayra = Script::where('language', Base::NAYRA_LANG)->exists(); + if (!$existsScriptsUsingNayra) { + ScriptExecutor::where('language', Base::NAYRA_LANG)->delete(); + } else { + Log::error('There are scripts using Nayra, so the Nayra script executor cannot be deleted.'); + } + } catch (Exception $e) { + Log::error('Cannot delete Nayra script executor: ' . $e->getMessage()); + } + } +}