diff --git a/ProcessMaker/Http/Controllers/Admin/ScriptExecutorController.php b/ProcessMaker/Http/Controllers/Admin/ScriptExecutorController.php index 230458ad89..5dfe1df1fd 100644 --- a/ProcessMaker/Http/Controllers/Admin/ScriptExecutorController.php +++ b/ProcessMaker/Http/Controllers/Admin/ScriptExecutorController.php @@ -8,15 +8,20 @@ class ScriptExecutorController extends Controller { - public function index(Request $request) + public function index(Request $request, ScriptMicroserviceService $service) { if (!config('app.custom_executors')) { abort(404); } + $scriptMicroserviceEnabled = config('script-runner-microservice.enabled'); + return view('admin.script-executors.index', [ - 'script_microservice_enabled' => config('script-runner-microservice.enabled'), + 'script_microservice_enabled' => $scriptMicroserviceEnabled, + 'script_microservice_tenant_id' => $scriptMicroserviceEnabled + ? $service->getInstanceUuid() + : null, ]); } } diff --git a/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php b/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php index 7627c9d7a4..36425976ce 100644 --- a/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php +++ b/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php @@ -4,6 +4,7 @@ use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Http\Client\RequestException; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use ProcessMaker\Enums\ScriptExecutorType; @@ -124,7 +125,15 @@ public function store(Request $request, ScriptMicroserviceService $service) ScriptExecutorCreated::dispatch($scriptExecutor->getAttributes()); BuildScriptExecutor::dispatch($scriptExecutor->id, $request->user()->id); } else { - $service->createCustomExecutor($scriptExecutor); + try { + $service->createCustomExecutor($scriptExecutor); + } catch (RequestException $e) { + // The remote executor was rejected, so keeping the local record would leave + // an executor that can never be built. + $scriptExecutor->delete(); + + $this->throwMicroserviceError($e); + } } return ['status' => 'started', 'uuid' => $scriptExecutor->uuid, 'id' => $scriptExecutor->id]; @@ -189,7 +198,11 @@ public function update(Request $request, ScriptExecutor $scriptExecutor, ScriptM ); if (config('script-runner-microservice.enabled') && $scriptExecutor->type == ScriptExecutorType::Custom) { - $service->updateCustomExecutor($scriptExecutor); + try { + $service->updateCustomExecutor($scriptExecutor); + } catch (RequestException $e) { + $this->throwMicroserviceError($e); + } } else { if (!empty($scriptExecutor->getChanges())) { ScriptExecutorUpdated::dispatch($scriptExecutor->id, $original, $scriptExecutor->getChanges()); @@ -274,6 +287,28 @@ public function delete(Request $request, ScriptExecutor $scriptExecutor, ScriptM return ['status' => 'done']; } + /** + * Report a script microservice rejection on the form, since the build itself + * is only reported over the websocket channel. + * + * @param RequestException $e Failed script microservice response + * + * @return never + * + * @throws ValidationException|RequestException + */ + private function throwMicroserviceError(RequestException $e): never + { + if (!$e->response->clientError()) { + throw $e; + } + + $detail = $e->response->json('detail'); + $message = is_string($detail) && $detail !== '' ? $detail : $e->getMessage(); + + throw ValidationException::withMessages(['language' => [$message]]); + } + private function checkAuth($request) { if (!config('app.custom_executors')) { diff --git a/resources/js/admin/script-executors/ScriptExecutors.vue b/resources/js/admin/script-executors/ScriptExecutors.vue index 63970dc86c..76653b711e 100644 --- a/resources/js/admin/script-executors/ScriptExecutors.vue +++ b/resources/js/admin/script-executors/ScriptExecutors.vue @@ -252,6 +252,7 @@ export default { "filter", "permission", "script_microservice_enabled", + "script_microservice_tenant_id", ], data() { return { @@ -271,7 +272,8 @@ export default { exitCode: 0, showDockerfile: false, loading: true, - script_microservice_broadcast_uui: null, + activeBuildUuid: null, + pendingBuildEvents: [], localLoadOnStart: true, orderBy: "language", @@ -356,16 +358,15 @@ export default { } ); } + + if (this.script_microservice_enabled && this.script_microservice_tenant_id) { + this.subscribeToTenantBuildChannel(); + } }, watch: { commandOutput() { this.scrollToBottom(); }, - script_microservice_broadcast_uui(newVal) { - if (newVal) { - this.subscribeToScriptMicroserviceChannel(newVal); - } - }, }, computed: { modalTitle() { @@ -434,7 +435,13 @@ export default { }, setErrors(errors) { this.status = "error"; - this.errors = errors.response.data.errors; + this.errors = _.get(errors, "response.data.errors", {}); + const messages = Object.values(this.errors) + .flat() + .filter((message) => typeof message === "string" && message !== ""); + if (messages.length) { + this.output(messages.join("\n") + "\n"); + } }, doNotHideIfRunning(e) { if (this.isRunning) { @@ -472,13 +479,15 @@ export default { save() { this.resetProcessInfo(); this.status = "saving"; + this.activeBuildUuid = this.formData.uuid || null; + this.pendingBuildEvents = []; if (this.formData.id) { const path = "/script-executors/" + this.formData.id; ProcessMaker.apiClient .put(path, this.formData) .then((result) => { this.status = _.get(result, "data.status", "error"); - this.script_microservice_broadcast_uui = result.data.uuid; + this.setActiveBuildUuid(_.get(result, "data.uuid", this.formData.uuid)); }) .catch((e) => { this.setErrors(e); @@ -489,9 +498,10 @@ export default { .post(path, this.formData) .then((result) => { this.status = _.get(result, "data.status", "error"); - this.script_microservice_broadcast_uui = result.data.uuid; + this.setActiveBuildUuid(_.get(result, "data.uuid")); if (this.status === "started") { this.formData.id = result.data.id; + this.formData.uuid = result.data.uuid; this.fetch(); // refresh the table (beneath the modal) } }) @@ -505,13 +515,17 @@ export default { }, edit(row) { this.formData = _.cloneDeep(row); + this.activeBuildUuid = row.uuid || null; this.$refs.edit.show(); }, reset() { this.formData = _.cloneDeep(this.emptyFormData); this.errors = {}; this.showDockerfile = false; - (this.status = "idle"), this.resetProcessInfo(); + this.status = "idle"; + this.activeBuildUuid = null; + this.pendingBuildEvents = []; + this.resetProcessInfo(); }, resetProcessInfo() { this.commandOutput = ""; @@ -550,31 +564,72 @@ export default { onAddToBundle(data) { this.$root.$emit('add-to-bundle', data); }, - subscribeToScriptMicroserviceChannel(name) { - const channel = `build-image-${name}`; - if (this.script_microservice_enabled) { - // Subscribe to new channel - window.ScriptMicroserviceEcho - .channel(channel) - .listenToAll((eventName, data) => { - this.status = this.status === "idle" ? "starting" : this.status; - switch (eventName) { - case ".build-image": - this.output(`${data}\n`); - break; - case ".build-finished": - this.pidFile = null; - this.exitCode = 0; - this.status = "done"; - break; - case ".build-error": - this.output(data); - this.pidFile = null; - this.exitCode = 1; - this.status = "done"; - break; - } - }); + subscribeToTenantBuildChannel() { + if (!window.ScriptMicroserviceEcho) { + return; + } + const channel = `tenant-${this.script_microservice_tenant_id}-builds`; + window.ScriptMicroserviceEcho.channel(channel).listen( + ".executor-build", + (data) => { + this.handleExecutorBuildEvent(data); + } + ); + }, + setActiveBuildUuid(uuid) { + if (!uuid) { + return; + } + this.activeBuildUuid = uuid; + const pending = this.pendingBuildEvents.filter( + (event) => event.executor_id === uuid + ); + this.pendingBuildEvents = []; + pending.forEach((event) => this.applyExecutorBuildEvent(event)); + }, + handleExecutorBuildEvent(data) { + if (!data || typeof data !== "object") { + return; + } + if (!this.isRunning) { + return; + } + if (!this.activeBuildUuid) { + this.pendingBuildEvents.push(data); + return; + } + if (data.executor_id !== this.activeBuildUuid) { + return; + } + this.applyExecutorBuildEvent(data); + }, + applyExecutorBuildEvent(data) { + if (this.status === "saving") { + this.status = "starting"; + } else if (this.status === "idle") { + this.status = "starting"; + } + + if (data.type === "status") { + this.output(`[${data.phase}] ${data.message || ""}\n`); + } else if (data.message) { + const message = data.message.endsWith("\n") + ? data.message + : `${data.message}\n`; + this.output(message); + } + + if (data.phase === "completed" && data.type === "status") { + this.pidFile = null; + this.exitCode = 0; + this.status = "done"; + return; + } + + if (data.phase === "error" || data.type === "error") { + this.pidFile = null; + this.exitCode = 1; + this.status = "done"; } }, }, diff --git a/resources/views/admin/script-executors/index.blade.php b/resources/views/admin/script-executors/index.blade.php index cb3146fabe..bc70a1ddfb 100644 --- a/resources/views/admin/script-executors/index.blade.php +++ b/resources/views/admin/script-executors/index.blade.php @@ -19,6 +19,7 @@