Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions ProcessMaker/Enums/ScriptExecutorType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace ProcessMaker\Enums;

enum ScriptExecutorType:string
{
case System = 'system';
case Custom = 'custom';
}
2 changes: 1 addition & 1 deletion ProcessMaker/Events/ScriptResponseEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ private function cacheResponse()
public function broadcastWith()
{
$date = new Carbon();
$response = $this->cacheResponse($this->response);
$response = $this->cacheResponse();

return [
'type' => '.' . \get_class($this),
Expand Down
7 changes: 7 additions & 0 deletions ProcessMaker/Http/Controllers/Api/ScriptController.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use ProcessMaker\Models\ScriptCategory;
use ProcessMaker\Models\User;
use ProcessMaker\Query\SyntaxError;
use ProcessMaker\Services\ScriptMicroserviceService;
use ProcessMaker\Traits\ProjectAssetTrait;

class ScriptController extends Controller
Expand Down Expand Up @@ -286,6 +287,12 @@ public function execution($key)
return response()->json(Cache::get("srn.{$key}"));
}

public function microserviceExecution(Request $request)
{
$scriptMicroserviceService = new ScriptMicroserviceService();
$scriptMicroserviceService->handle($request);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @gusys should we add error handling to catch any potential exceptions from the microservice execution?


/**
* Get a single script in a process.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ public function index(Request $request)
*/
public function store(Request $request)
{
$request->request->add(['type' => 'custom']);
$this->checkAuth($request);
$request->validate(ScriptExecutor::rules());

Expand Down
28 changes: 22 additions & 6 deletions ProcessMaker/Jobs/RunScriptTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\Log;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Exception\ConfigurationException;
use ProcessMaker\Exception\ScriptException;
use ProcessMaker\Facades\WorkflowManager;
Expand Down Expand Up @@ -32,9 +33,9 @@ class RunScriptTask extends BpmnAction implements ShouldQueue
/**
* Create a new job instance.
*
* @param \ProcessMaker\Models\Process $definitions
* @param \ProcessMaker\Models\ProcessRequest $instance
* @param \ProcessMaker\Models\ProcessRequestToken $token
* @param Definitions $definitions
* @param ProcessRequest $instance
* @param ProcessRequestToken $token
* @param array $data
*/
public function __construct(Definitions $definitions, ProcessRequest $instance, ProcessRequestToken $token, array $data, $attemptNum = 1)
Expand Down Expand Up @@ -68,6 +69,7 @@ public function action(ProcessRequestToken $token = null, ScriptTaskInterface $e
}

$errorHandling = null;
$scriptExecutor = null;
try {
if (empty($scriptRef)) {
$code = $element->getScript();
Expand All @@ -86,6 +88,7 @@ public function action(ProcessRequestToken $token = null, ScriptTaskInterface $e
if (!$script) {
throw new ConfigurationException(__('Script ":id" not found', ['id' => $scriptRef]));
}
$scriptExecutor = $script->scriptExecutor;
$script = $script->versionFor($instance);
}

Expand All @@ -95,9 +98,22 @@ public function action(ProcessRequestToken $token = null, ScriptTaskInterface $e
$this->unlock();
$dataManager = new DataManager();
$data = $dataManager->getData($token);
$response = $script->runScript($data, $configuration, $token->getId(), $errorHandling->timeout());

$this->updateData($response);
$metadata = [
'script_task' => [
'script_id' => $scriptRef,
'definition_id' => $this->definitionsId,
'instance_id' => $this->instanceId,
'token_id' => $this->tokenId,
'data' => $data,
'attempts' => $this->attemptNum,
],
];
$response = $script->runScript($data, $configuration, $token->getId(), $errorHandling->timeout(), 0, $metadata);

@nolanpro nolanpro Jan 15, 2025 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gusys if this is an asynchronous execution, what endpoint are we using for the callback? Is it /scripts/microservice/execution? Is that also the callback when executed from TestScript.php?


if (!config('script-runner-microservice.enabled') ||
($scriptExecutor && $scriptExecutor->type === ScriptExecutorType::Custom)) {
$this->updateData($response);
}
} catch (ConfigurationException $exception) {
$this->unlock();
$this->updateData(['output' => $exception->getMessageForData($token)]);
Expand Down
14 changes: 12 additions & 2 deletions ProcessMaker/Jobs/TestScript.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Events\ScriptResponseEvent;
use ProcessMaker\Models\Script;
use ProcessMaker\Models\User;
Expand Down Expand Up @@ -60,8 +61,17 @@ public function handle()
try {
// Just set the code but do not save the object (preview only)
$this->script->code = $this->code;
$response = $this->script->runScript($this->data, $this->configuration);
$this->sendResponse(200, $response);
$metadata = [
'nonce' => $this->nonce,
'current_user' => $this->current_user?->id,
];
$response = $this->script->runScript($this->data, $this->configuration, '', null, 0, $metadata);
\Log::debug('Response api microservice: ' . print_r($response, true));

if (!config('script-runner-microservice.enabled') ||
$this->script->scriptExecutor && $this->script->scriptExecutor->type === ScriptExecutorType::Custom) {
$this->sendResponse(200, $response);
}
} catch (Throwable $exception) {
$this->sendResponse(500, [
'exception' => get_class($exception),
Expand Down
6 changes: 3 additions & 3 deletions ProcessMaker/Models/Script.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ public static function rules($existing = null)
* @param array $data
* @param array $config
*/
public function runScript(array $data, array $config, $tokenId = '', $timeout = null)
public function runScript(array $data, array $config, $tokenId = '', $timeout = null, $sync = 1, $metadata = [])

@nolanpro nolanpro Jan 15, 2025 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gusys could you change $sync = 1 to bool $sync = true? You can change it in ScriptMicroserviceRunner if you need to send it as an integer 'sync' => $sync ? 1 : 0,

There are a few other places we call runScript, like RunServiceTask.php. Do those need to be updated?

{
if (!$timeout) {
$timeout = $this->timeout;
Expand All @@ -158,14 +158,14 @@ public function runScript(array $data, array $config, $tokenId = '', $timeout =
if (!$this->scriptExecutor) {
throw new ScriptLanguageNotSupported($this->language);
}
$runner = new ScriptRunner($this->scriptExecutor);
$runner = new ScriptRunner($this);
$runner->setTokenId($tokenId);
$user = User::find($this->run_as_user_id);
if (!$user) {
throw new ConfigurationException('A user is required to run scripts');
}

return $runner->run($this->code, $data, $config, $timeout, $user);
return $runner->run($this->code, $data, $config, $timeout, $user, $sync, $metadata);
}

/**
Expand Down
9 changes: 8 additions & 1 deletion ProcessMaker/Models/ScriptExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Exception\ScriptLanguageNotSupported;
use ProcessMaker\Facades\Docker;
use ProcessMaker\Traits\Exportable;
Expand Down Expand Up @@ -56,7 +58,7 @@ class ScriptExecutor extends ProcessMakerModel
use HideSystemResources;

protected $fillable = [
'title', 'description', 'language', 'config', 'is_system',
'title', 'description', 'language', 'config', 'is_system', 'type',
];

public static function install($params)
Expand Down Expand Up @@ -146,6 +148,11 @@ public static function rules($existing = null)
'required',
Rule::in(Script::scriptFormatValues()),
],
'type' => [
'sometimes',
new Enum(ScriptExecutorType::class),
'nullable',
],
];
}

Expand Down
2 changes: 1 addition & 1 deletion ProcessMaker/Models/ScriptExecutorVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
class ScriptExecutorVersion extends ProcessMakerModel
{
protected $fillable = [
'title', 'description', 'language', 'config', 'draft', 'is_system',
'title', 'description', 'language', 'config', 'draft', 'is_system', 'type',
];

/**
Expand Down
4 changes: 2 additions & 2 deletions ProcessMaker/Models/ScriptVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,15 @@ public function parent()
* @param array $data
* @param array $config
*/
public function runScript(array $data, array $config, $tokenId = '', $timeout = null)
public function runScript(array $data, array $config, $tokenId = '', $timeout = null, $sync = 1, $metadata = [])
{
$script = $this->parent->replicate();
$except = $script->getGuarded();
foreach (collect($script->getAttributes())->except($except)->keys() as $prop) {
$script->$prop = $this->$prop;
}

return $script->runScript($data, $config, $tokenId, $timeout);
return $script->runScript($data, $config, $tokenId, $timeout, $sync, $metadata);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion ProcessMaker/ScriptRunners/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public function __construct(ScriptExecutor $scriptExecutor)
* @return array
* @throws RuntimeException
*/
public function run($code, array $data, array $config, $timeout, ?User $user)
public function run($code, array $data, array $config, $timeout, ?User $user, $sync, $metadata)
{
$isNayra = $this->scriptExecutor->language === self::NAYRA_LANG;

Expand Down
164 changes: 164 additions & 0 deletions ProcessMaker/ScriptRunners/ScriptMicroserviceRunner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php

namespace ProcessMaker\ScriptRunners;

use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use ProcessMaker\Exception\ConfigurationException;
use ProcessMaker\Exception\ScriptException;
use ProcessMaker\GenerateAccessToken;
use ProcessMaker\Models\EnvironmentVariable;
use ProcessMaker\Models\Script;
use ProcessMaker\Models\User;
use stdClass;

class ScriptMicroserviceRunner
{
private string $tokenId = '';

private string $language;

public function __construct(protected Script $script)
{
$this->language = strtolower($script->language ?? $script->scriptExecutor->language);
}

public function getAccessToken()
{
if (Cache::has('keycloak.access_token')) {
return Cache::get('keycloak.access_token');
}

$response = Http::asForm()->post(config('script-runner-microservice.keycloak.base_url'), [
'grant_type' => 'password',
'client_id' => config('script-runner-microservice.keycloak.client_id'),
'client_secret' => config('script-runner-microservice.keycloak.client_secret'),
'username' => config('script-runner-microservice.keycloak.username'),
'password' => config('script-runner-microservice.keycloak.password'),
]);

if ($response->successful()) {
Cache::put('keycloak.access_token', $response->json()['access_token'], $response->json()['expires_in'] - 60);
}

return Cache::get('keycloak.access_token');
}

public function getScriptRunner()
{
$response = Cache::remember('script-runner-microservice.script-languages', now()->addDay(), function () {
return Http::withToken($this->getAccessToken())
->get(config('script-runner-microservice.base_url') . '/scripts')->collect();
});

return $response->filter(function ($item) {
return $item['language'] == $this->language;
})->first();
}

public function run($code, array $data, array $config, $timeout, $user, $sync, $metadata)
{
Log::debug('Language: ' . $this->language);
Log::debug('Sync: ' . $sync);
Log::debug('Metadata: ' . print_r($metadata, true));

$scriptRunner = $this->getScriptRunner();

if (!$scriptRunner) {
throw new ConfigurationException('No exists script executor for this language: ' . $this->language);
}
$metadata = array_merge($this->getMetadata($user), $metadata);

$payload = [
'version' => config('script-runner-microservice.version') ?? $this->getProcessMakerVersion(),
'language' => $scriptRunner['language'],
'metadata'=> $metadata,
'data' => !empty($data) ? $this->sanitizeCss($data) : new stdClass(),
'config' => !empty($config) ? $config : new stdClass(),
'script' => base64_encode(str_replace("'", '&#39;', $code)),
'secrets' => $this->getEnvironmentVariables($user),
'callback' => config('script-runner-microservice.callback'),
'debug' => true,
'timeout' => $timeout,
'sync' => $sync,
];

Log::debug(print_r($payload, true));
Comment thread
gusys marked this conversation as resolved.
Resolved

$response = Http::withToken($this->getAccessToken())
->post(config('script-runner-microservice.base_url') . '/requests/create', $payload);

$response->throw();

return $response->json();
}

private function getEnvironmentVariables(User $user)
{
$variablesParameter = [];
EnvironmentVariable::chunk(50, function (Collection $variables) use (&$variablesParameter) {
foreach ($variables as $variable) {
// Fix variables that have spaces
$variablesParameter[str_replace(' ', '_', $variable->name)] = $variable->value;
}
});

// Add the url to the host
$variablesParameter['HOST_URL'] = config('app.docker_host_url');

// Create tokens for the SDK if a user is set
$token = null;
if ($user) {
$accessToken = Cache::remember('script-runner-' . $user->id, now()->addWeek(), function () use ($user) {
$user->removeOldRunScriptTokens();
$token = new GenerateAccessToken($user);

return $token->getToken();
});
$variablesParameter['API_TOKEN'] = $accessToken;
$variablesParameter['API_HOST'] = config('app.docker_host_url') . '/api/1.0';
$variablesParameter['APP_URL'] = config('app.docker_host_url');
$variablesParameter['API_SSL_VERIFY'] = (config('app.api_ssl_verify') ? '1' : '0');
}

return $variablesParameter;
}

public function setTokenId($tokenId)
{
$this->tokenId = $tokenId;
}

public function getProcessMakerVersion()
{
return Cache::remember('script-runner-microservice.processmaker-version', now()->addDay(), function () {
$composer_json_path = json_decode(file_get_contents(base_path() . '/composer.json'));

return $composer_json_path->version;
});
}

public function getMetadata($user)
{
return [
'script_id' => $this->script->id,
'instance' => config('app.url'),
'user_id' => $user->id,
'user_email' => $user->email,
];
}

public function sanitizeCss($data)
{
if ($this->language !== 'javascript-ssr') {
return $data;
}
if (array_key_exists('css', $data)) {
$data['css'] = false;
}

return $data;
}
}
Loading