From f6e99ad7cdfceea4361715f99448ee73a4164523 Mon Sep 17 00:00:00 2001 From: Benjamin Weder Date: Fri, 24 Nov 2023 23:38:48 +0100 Subject: [PATCH] Improve on-demand deployment (#126) * Change order of deployment modals * Retrieve on-demand state * Move instructions to the top * Only create service instances for not on-demand CSARs * Only perform binding for not on-demand services * Add on-demand check during binding * Fix issue not opening the on demand modal * Remove unused code * Add information about completion into on-demand modal * Remove unnecessary if * Forward required data to on-demand transformation * Forward required information to on-demand transformation * add input var preparation for vm * add input var preparation for vm * update ondemand subprocess * add scripts for ondemand depl * ondemand script * use slurper for parsing json * adding 2nd script task * Add first part of OT instance retrieval * Pass OT endpoint to instance check * enable dedicated policy for ondemand * Extend instance retrieval script * Fix upper case * Only perform on-demand transformation for tasks with corresponding policy * Remove log * Improve logging * Fix logging * Fix issue with dedicated policy * Eventually handle input parameters for on-demand deployment * Fix escaping in service creation script * add matching for vm parameters * Add all required input parameters and make selfServiceUrl task specific * Fix first part of waiting script * Fix waiting script * Adapt service invokation to scoped selfserviceinstance variables --------- Co-authored-by: mbeisel --- .../opentosca/deployment/DeploymentUtils.js | 25 +- .../replacement/OnDemandTransformator.js | 658 +++++++++++++++--- .../deployment/services/DeploymentPlugin.js | 492 ++++++++----- .../services/ServiceDeploymentBindingModal.js | 52 +- .../services/ServiceDeploymentInputModal.js | 146 ++-- .../ServiceDeploymentOverviewModal.js | 14 +- .../ServiceOnDemandDeploymentOverviewModal.js | 44 +- 7 files changed, 1058 insertions(+), 373 deletions(-) diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/DeploymentUtils.js b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/DeploymentUtils.js index 30222b6c..1a05a85b 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/DeploymentUtils.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/DeploymentUtils.js @@ -11,8 +11,12 @@ import { getBindingType } from "./BindingUtils"; import { getFlowElementsRecursively } from "../../../editor/util/ModellingUtilities"; -import { synchronousPostRequest } from "../utilities/Utilities"; +import { + synchronousGetRequest, + synchronousPostRequest, +} from "../utilities/Utilities"; import config from "../framework-config/config"; +import { getModeler } from "../../../editor/ModelerHandler"; /** * Get the ServiceTasks of the current workflow that have an attached deployment model to deploy the corresponding service starting from the given root element @@ -44,6 +48,17 @@ export function getServiceTasksToDeploy(startElement) { console.log("Adding to existing CSAR entry..."); csarEntry.serviceTaskIds.push(flowElement.id); } else { + // get businessObject for onDemand property retrieval + const taskData = getModeler() + .get("elementRegistry") + .get(flowElement.id); + let onDemand; + if (taskData.businessObject.onDemand) { + onDemand = taskData.businessObject.onDemand; + } else { + onDemand = false; + } + csarsToDeploy.push({ serviceTaskIds: [flowElement.id], url: flowElement.deploymentModelUrl, @@ -52,6 +67,7 @@ export function getServiceTasksToDeploy(startElement) { incomplete: !isCompleteDeploymentModel( flowElement.deploymentModelUrl ), + onDemand: onDemand, }); } } @@ -132,3 +148,10 @@ export function completeIncompleteDeploymentModel( return undefined; } } + +export function getTopology(deploymentModelUrl) { + let url = deploymentModelUrl.replace("/?csar", "/topologytemplate"); + console.log("Getting topology from URL:", url); + const topology = synchronousGetRequest(url, "application/json"); + return JSON.parse(topology); +} diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js b/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js index aaab10a3..0dc141ec 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js @@ -11,13 +11,16 @@ import { createTempModelerFromXml } from "../../../editor/ModelerHandler"; import { getXml } from "../../../editor/util/IoUtilities"; -import { isDeployableServiceTask } from "../deployment/DeploymentUtils"; import * as config from "../framework-config/config-manager"; import { makeId } from "../deployment/OpenTOSCAUtils"; import { getCamundaEndpoint } from "../../../editor/config/EditorConfigManager"; import { createElement } from "../../../editor/util/camunda-utils/ElementUtil"; -import { getRootProcess } from "../../../editor/util/ModellingUtilities"; -import * as consts from "../Constants"; +import { + getCamundaInputOutput, + getRootProcess, +} from "../../../editor/util/ModellingUtilities"; +import { layout } from "../../quantme/replacement/layouter/Layouter"; +import { deletePolicies } from "../utilities/Utilities"; const fetchMethod = ` function fetch(method, url, body) { @@ -56,80 +59,308 @@ function fetch(method, url, body) { } }`; -function createDeploymentScript(params) { +function createDeploymentScript( + opentoscaEndpoint, + camundaEndpoint, + camundaTopic, + subprocessId, + inputParams, + taskId, + reconstructedVMs +) { return ` -var params = ${JSON.stringify(params)}; -params.csarName = "ondemand_" + (Math.random().toString().substring(3)); +var inputParams = ${JSON.stringify(inputParams)}; +var csarName = "ondemand_" + (Math.random().toString().substring(3)); +var reconstructedVMs= ${JSON.stringify(reconstructedVMs)}; ${fetchMethod} -var createCsarResponse = fetch('POST', params.opentoscaEndpoint, JSON.stringify({ +var createCsarResponse = fetch('POST', "${opentoscaEndpoint}", JSON.stringify({ enrich: 'false', - name: params.csarName, - url: params.deploymentModelUrl + name: csarName, + url: execution.getVariable("completeModelUrl_" + "${taskId}") + "?csar" })) -var serviceTemplates = JSON.parse(fetch('GET', params.opentoscaEndpoint + "/" + params.csarName + ".csar/servicetemplates")) +var deployedTopology = JSON.parse(fetch('GET', execution.getVariable("completeModelUrl_" + "${taskId}") + "topologytemplate")); + +for (const [key, value] of Object.entries(deployedTopology.nodeTemplates)) { + for (const [constructKey, constructValue] of Object.entries(reconstructedVMs)) { + if ( + constructValue.name.includes(value.name) && + !value.name.includes("VM") + ) { + for (const [propertyName, propertyValue] of Object.entries(constructValue.requiredAttributes)) { + inputParams[propertyName] = propertyValue; + } + } + } +} +java.lang.System.out.println("Input parameters after update: " + JSON.stringify(inputParams)); + +var serviceTemplates = JSON.parse(fetch('GET', "${opentoscaEndpoint}" + "/" + csarName + ".csar/servicetemplates")) var buildPlansUrl = serviceTemplates.service_templates[0]._links.self.href + '/buildplans' var buildPlans = JSON.parse(fetch('GET', buildPlansUrl)) var buildPlanUrl = buildPlans.plans[0]._links.self.href var inputParameters = JSON.parse(fetch('GET', buildPlanUrl)).input_parameters for(var i = 0; i < inputParameters.length; i++) { if(inputParameters[i].name === "camundaEndpoint") { - inputParameters[i].value = params.opentoscaEndpoint + inputParameters[i].value = "${camundaEndpoint}" } else if(inputParameters[i].name === "camundaTopic") { - inputParameters[i].value = params.camundaTopic + inputParameters[i].value = "${camundaTopic}" } else { - inputParameters[i].value = "null" + inputParameters[i].value = inputParams[inputParameters[i].name]; } } + var createInstanceResponse = fetch('POST', buildPlanUrl + "/instances", JSON.stringify(inputParameters)) -execution.setVariable(params.subprocessId + "_deploymentBuildPlanInstanceUrl", buildPlanUrl + "/instances/" + createInstanceResponse);`; +execution.setVariable("${subprocessId}" + "_deploymentBuildPlanInstanceUrl", buildPlanUrl + "/instances/" + createInstanceResponse);`; } -function createWaitScript(params) { +function createWaitScript(subprocessId, taskId) { return ` -var params = ${JSON.stringify(params)}; ${fetchMethod} -var buildPlanInstanceUrl = execution.getVariable(params.subprocessId + "_deploymentBuildPlanInstanceUrl"); +var buildPlanInstanceUrl = execution.getVariable("${subprocessId}" + "_deploymentBuildPlanInstanceUrl"); var instanceUrl; -for(var i = 0; i < 30; i++) { +for(var i = 0; i < 20; i++) { try { instanceUrl = JSON.parse(fetch('GET', buildPlanInstanceUrl))._links.service_template_instance.href; if (instanceUrl) break; } catch (e) { } - java.lang.Thread.sleep(2000); + java.lang.Thread.sleep(10000); } -java.lang.System.out.println("InstanceUrl: " + instanceUrl); +console.log("InstanceUrl: " + instanceUrl); -for(var i = 0; i < 30 * 3; i++) { +var buildPlanUrl = ""; +for(var i = 0; i < 50; i++) { try { + java.lang.System.out.println("Iteration: " + i); var createInstanceResponse = fetch('GET', instanceUrl); - var instance = JSON.parse(createInstanceResponse).service_template_instances; + var instance = JSON.parse(createInstanceResponse); + console.log("Instance state: " + instance.state); + buildPlanUrl = instance._links.build_plan_instance.href; if (instance && instance.state === "CREATED") { break; } } catch (e) { + java.lang.System.out.println("Error while checking instance state: " + e); } java.lang.Thread.sleep(30000); } -var properties = JSON.parse(fetch('GET', instanceUrl + "/properties")); +console.log("Retrieving selfServiceApplicationUrl from build plan output from URL: ", buildPlanUrl); +var buildPlanResult = JSON.parse(fetch('GET', buildPlanUrl)); +console.log("Build plan result: ", buildPlanResult); +var buildPlanOutputs = buildPlanResult.outputs; +console.log("Outputs: ", buildPlanOutputs.toString()); +var selfserviceApplicationUrl = buildPlanOutputs.filter((output) => output.name === "selfserviceApplicationUrl"); +console.log("SelfServiceApplicationUrl: " + selfserviceApplicationUrl[0].value); -execution.setVariable("selfserviceApplicationUrl", properties.selfserviceApplicationUrl); +execution.setVariable("${taskId}" + "_selfserviceApplicationUrl", selfserviceApplicationUrl[0].value); java.lang.Thread.sleep(12000); `; } +function createCompleteModelScript(url, blacklist, policies, taskId) { + return ` +import groovy.json.* +def url = "${url}" +def blacklist = ${JSON.stringify(blacklist)}; +def slurper = new JsonSlurper(); +def policies = slurper.parseText(${JSON.stringify(policies)}); + +def message = JsonOutput.toJson("policies": policies, "blacklist": blacklist); + +try { + def post = new URL(url).openConnection(); + post.setRequestMethod("POST"); + post.setDoOutput(true); + post.setRequestProperty("Content-Type", "application/json"); + post.setRequestProperty("accept", "application/json"); + + OutputStreamWriter wr = new OutputStreamWriter(post.getOutputStream()); + println message; + wr.write(message.toString()); + wr.flush(); + + def status = post.getResponseCode(); + println status; + if(status.toString().startsWith("2")){ + println post; + println post.getInputStream(); + def location = post.getHeaderFields()['Location'][0]; + def saveVarName = "completeModelUrl_" + "${taskId}"; + execution.setVariable(saveVarName, location); + }else{ + throw new org.camunda.bpm.engine.delegate.BpmnError("Received status code " + status + " while completing Deployment Model!"); + } +} catch(org.camunda.bpm.engine.delegate.BpmnError e) { + println e.errorCode; + throw new org.camunda.bpm.engine.delegate.BpmnError(e.errorCode); +} catch(Exception e) { + println e; + throw new org.camunda.bpm.engine.delegate.BpmnError("Unable to connect to given endpoint: " + "${url}"); +}; +`; +} + +function createCheckForEquivalencyScript(taskId) { + return ` +import groovy.json.* +def url = execution.getVariable("completeModelUrl_" + "${taskId}"); +url = url + "topologytemplate/checkforequivalentcsars" + +try { + def post = new URL(url).openConnection(); + post.setRequestMethod("POST"); + post.setDoOutput(true); + post.setRequestProperty("Content-Type", "application/json"); + post.setRequestProperty("accept", "application/json"); + + post.getOutputStream().write(); + + def status = post.getResponseCode(); + println status; + if(status.toString().startsWith("2")){ + println post.getInputStream(); + def resultText = post.getInputStream().getText(); + def slurper = new JsonSlurper(); + def json = slurper.parseText(resultText); + def saveVarName = "equivalentCSARs_" + "${taskId}"; + execution.setVariable(saveVarName, json); + }else{ + throw new org.camunda.bpm.engine.delegate.BpmnError("Received status code " + status + " while completing Deployment Model!"); + } +} catch(org.camunda.bpm.engine.delegate.BpmnError e) { + println e.errorCode; + throw new org.camunda.bpm.engine.delegate.BpmnError(e.errorCode); +} catch(Exception e) { + println e; + throw new org.camunda.bpm.engine.delegate.BpmnError("Unable to connect to given endpoint: " + url); +}; +`; +} + +function createCheckForAvailableInstancesScript(containerUrl, taskId) { + return ` +import groovy.json.* +def containerUrl = "${containerUrl}"; +def equivalentCSARs = execution.getVariable("equivalentCSARs_" + "${taskId}"); + +try { + for (String equivalentCSAR : equivalentCSARs ){ + println "Checking availability for CSAR with URL: " + equivalentCSAR; + def values = equivalentCSAR.split('/'); + def csarName = values[values.length - 1]; + println "Checking availability for CSAR with name: " + csarName; + + def csarUrl = containerUrl + "/" + csarName + ".csar"; + println "Checking for ServiceTemaplates using URL: " + csarUrl; + + def get = new URL(csarUrl).openConnection(); + get.setRequestMethod("GET"); + get.setDoOutput(true); + get.setRequestProperty("accept", "application/json"); + def status = get.getResponseCode(); + println "Status code for ServiceTemplate retrieval: " + status; + if(status != 200){ + println "CSAR not found. Skipping..."; + continue; + } + def resultText = get.getInputStream().getText(); + def json = new JsonSlurper().parseText(resultText); + def serviceTemplateLink = json.get("_links").get("servicetemplate").get("href") + "/instances"; + println "Retrieved link to ServiceTemplate: " + serviceTemplateLink; + + get = new URL(serviceTemplateLink).openConnection(); + get.setRequestMethod("GET"); + get.setDoOutput(true); + get.setRequestProperty("accept", "application/json"); + status = get.getResponseCode(); + println "Status code for instance retrieval: " + status; + if(status != 200){ + println "Unable to retrieve instances. Skipping..."; + continue; + } + resultText = get.getInputStream().getText(); + json = new JsonSlurper().parseText(resultText); + def serviceTemplateInstances = json.get("service_template_instances"); + println serviceTemplateInstances; + + for (Object serviceTemplateInstance: serviceTemplateInstances){ + println "Checking instance with ID: " + serviceTemplateInstance.get("id"); + if(serviceTemplateInstance.get("state") != "CREATED"){ + println "Instance has invalid state. Skipping: " + serviceTemplateInstance.get("state"); + continue; + } + + println "Found instance with state CREATED. Extracting selfServiceUrl..."; + def instancesLink = serviceTemplateInstance.get("_links").get("self").get("href"); + println "Retrieving instance information from URL: " + instancesLink; + + get = new URL(instancesLink).openConnection(); + get.setRequestMethod("GET"); + get.setDoOutput(true); + get.setRequestProperty("accept", "application/json"); + status = get.getResponseCode(); + if(status != 200){ + println "Unable to retrieve instance information. Skipping..."; + continue; + } + + resultText = get.getInputStream().getText(); + json = new JsonSlurper().parseText(resultText); + def buildPlanLink = json .get("_links").get("build_plan_instance").get("href"); + println "Retrieved build plan URL: " + buildPlanLink; + + get = new URL(buildPlanLink).openConnection(); + get.setRequestMethod("GET"); + get.setDoOutput(true); + get.setRequestProperty("accept", "application/json"); + status = get.getResponseCode(); + if(status != 200){ + println "Unable to retrieve build plan information. Skipping..."; + continue; + } + + resultText = get.getInputStream().getText(); + json = new JsonSlurper().parseText(resultText); + def outputs = json.get("outputs"); + println outputs; + + def selfserviceApplicationUrlEntry = outputs.findAll { it.name.equalsIgnoreCase("selfserviceApplicationUrl") }; + if(selfserviceApplicationUrlEntry .size() < 1) { + println "Unable to retrieve selfserviceApplicationUrl. Skipping..."; + continue; + } + def selfserviceApplicationUrl = selfserviceApplicationUrlEntry[0].value; + println "Retrieved selfserviceApplicationUrl: " + selfserviceApplicationUrl; + execution.setVariable("instanceAvailable", "true"); + execution.setVariable("selfserviceApplicationUrl", selfserviceApplicationUrl); + return; + } + } + + println "Unable to retrieve suitable instances!"; + execution.setVariable("instanceAvailable", "false"); +} catch(Exception e) { + println "Exception while searching for available instances: " + e; + execution.setVariable("instanceAvailable", "false"); +}; +`; +} + /** * Initiate the replacement process for the ServiceTasks requiring on-demand deployment in the current process model * * @param xml the BPMN diagram in XML format + * @param csars the CSARs to use for the on-demand deployment */ -export async function startOnDemandReplacementProcess(xml) { +export async function startOnDemandReplacementProcess(xml, csars) { + console.log("Starting on-demand replacement with CSARs: ", csars); + const modeler = await createTempModelerFromXml(xml); const modeling = modeler.get("modeling"); const elementRegistry = modeler.get("elementRegistry"); @@ -140,18 +371,29 @@ export async function startOnDemandReplacementProcess(xml) { const definitions = modeler.getDefinitions(); const rootElement = getRootProcess(definitions); - const serviceTasks = elementRegistry.filter(({ businessObject }) => - isDeployableServiceTask(businessObject) + let serviceTaskIds = []; + csars + .filter((csar) => csar.onDemand) + .forEach( + (csar) => + (serviceTaskIds = serviceTaskIds.concat( + csar.serviceTaskIds.filter((id) => !serviceTaskIds.includes(id)) + )) + ); + console.log( + "Performing on-demand transformation for the following ServiceTask IDs: ", + serviceTaskIds ); - let onDemandPolicies = []; - for (const flowElement of rootElement.flowElements) { - if (flowElement.$type === consts.ON_DEMAND_POLICY) { - onDemandPolicies.push(elementRegistry.get(flowElement.id)); - } - } - modeling.removeElements(onDemandPolicies); - for (const serviceTask of serviceTasks) { + for (const serviceTaskId of serviceTaskIds) { + let serviceTask = elementRegistry.get(serviceTaskId); + + // delete policies as they are incorporated into the completion functionality + deletePolicies(modeler, serviceTaskId); + + let CSARForServiceTask = csars.filter((csar) => + csar.serviceTaskIds.filter((id) => id === serviceTaskId) + )[0]; let onDemand = serviceTask.businessObject.get("onDemand"); if (onDemand) { let deploymentModelUrl = serviceTask.businessObject.get( @@ -164,8 +406,6 @@ export async function startOnDemandReplacementProcess(xml) { ); } - const extensionElements = serviceTask.businessObject.extensionElements; - let subProcess = bpmnReplace.replaceElement(serviceTask, { type: "bpmn:SubProcess", }); @@ -184,60 +424,297 @@ export async function startOnDemandReplacementProcess(xml) { subProcess ); - let topicName = makeId(12); - const serviceTask1 = modeling.appendShape( + const serviceTaskCompleteDeploymentModel = modeling.appendShape( startEvent, { type: "bpmn:ScriptTask", }, { x: 400, y: 200 } ); - serviceTask1.businessObject.set("scriptFormat", "javascript"); - serviceTask1.businessObject.set( + serviceTaskCompleteDeploymentModel.businessObject.set( + "name", + "Adapt Model" + ); + serviceTaskCompleteDeploymentModel.businessObject.set( + "scriptFormat", + "groovy" + ); + serviceTaskCompleteDeploymentModel.businessObject.asyncBefore = true; + serviceTaskCompleteDeploymentModel.businessObject.asyncAfter = true; + serviceTaskCompleteDeploymentModel.businessObject.set( + "script", + createCompleteModelScript( + deploymentModelUrl.replace("?csar", "topologytemplate/completemodel"), + CSARForServiceTask.blacklistedNodetypes, + JSON.stringify(CSARForServiceTask.policies), + serviceTask.id + ) + ); + + // add gateway to check for dedicated policy + let dedicatedGateway = modeling.createShape( + { type: "bpmn:ExclusiveGateway" }, + { x: 50, y: 50 }, + subProcess, + {} + ); + let dedicatedGatewayBo = elementRegistry.get( + dedicatedGateway.id + ).businessObject; + dedicatedGatewayBo.name = "Dedidcated Policy?"; + modeling.connect(serviceTaskCompleteDeploymentModel, dedicatedGateway, { + type: "bpmn:SequenceFlow", + }); + + // add task to check for running container instance + let serviceTaskCheckForEquivalentDeploymentModel = modeling.createShape( + { type: "bpmn:ScriptTask" }, + { x: 50, y: 50 }, + subProcess, + {} + ); + serviceTaskCheckForEquivalentDeploymentModel.businessObject.set( + "name", + "Check For Equivalent Deployment Model" + ); + serviceTaskCheckForEquivalentDeploymentModel.businessObject.set( + "scriptFormat", + "groovy" + ); + serviceTaskCheckForEquivalentDeploymentModel.businessObject.asyncBefore = true; + serviceTaskCheckForEquivalentDeploymentModel.businessObject.asyncAfter = true; + serviceTaskCheckForEquivalentDeploymentModel.businessObject.set( "script", - createDeploymentScript({ - opentoscaEndpoint: config.getOpenTOSCAEndpoint(), - deploymentModelUrl: deploymentModelUrl, - subprocessId: subProcess.id, - camundaTopic: topicName, - camundaEndpoint: getCamundaEndpoint(), - }) - ); - serviceTask1.businessObject.set("name", "Create deployment"); - - const serviceTask2 = modeling.appendShape( - serviceTask1, + createCheckForEquivalencyScript(serviceTask.id) + ); + + let dedicatedFlow = modeling.connect( + dedicatedGateway, + serviceTaskCheckForEquivalentDeploymentModel, + { type: "bpmn:SequenceFlow" } + ); + let dedicatedFlowBo = elementRegistry.get( + dedicatedFlow.id + ).businessObject; + dedicatedFlowBo.name = "no"; + let dedicatedFlowCondition = bpmnFactory.create("bpmn:FormalExpression"); + dedicatedFlowCondition.body = + '${execution.hasVariable("dedicatedHosting") == false || dedicatedHosting == false}'; + dedicatedFlowBo.conditionExpression = dedicatedFlowCondition; + + // add task to check for available instance + let serviceTaskCheckForAvailableInstance = modeling.createShape( + { type: "bpmn:ScriptTask" }, + { x: 50, y: 50 }, + subProcess, + {} + ); + serviceTaskCheckForAvailableInstance.businessObject.set( + "name", + "Check Container For Available Instance" + ); + serviceTaskCheckForAvailableInstance.businessObject.set( + "scriptFormat", + "groovy" + ); + serviceTaskCheckForAvailableInstance.businessObject.asyncBefore = true; + serviceTaskCheckForAvailableInstance.businessObject.asyncAfter = true; + serviceTaskCheckForAvailableInstance.businessObject.set( + "script", + createCheckForAvailableInstancesScript( + config.getOpenTOSCAEndpoint(), + serviceTask.id + ) + ); + + modeling.connect( + serviceTaskCheckForEquivalentDeploymentModel, + serviceTaskCheckForAvailableInstance, { - type: "bpmn:ScriptTask", - }, - { x: 600, y: 200 } + type: "bpmn:SequenceFlow", + } + ); + + // add gateway to check if instance is available + let instanceAvailablityGateway = modeling.createShape( + { type: "bpmn:ExclusiveGateway" }, + { x: 50, y: 50 }, + subProcess, + {} + ); + let instanceAvailablityGatewayBo = elementRegistry.get( + instanceAvailablityGateway.id + ).businessObject; + instanceAvailablityGatewayBo.name = "Instance Available?"; + + modeling.connect( + serviceTaskCheckForAvailableInstance, + instanceAvailablityGateway, + { + type: "bpmn:SequenceFlow", + } + ); + + let joiningDedicatedGateway = modeling.createShape( + { type: "bpmn:ExclusiveGateway" }, + { x: 50, y: 50 }, + subProcess, + {} + ); + // add connection from InstanceAvailableGateway to joiningDedicatedGateway and add condition + let notInstanceAvailableFlow = modeling.connect( + instanceAvailablityGateway, + joiningDedicatedGateway, + { + type: "bpmn:SequenceFlow", + } + ); + let notInstanceAvailableFlowBo = elementRegistry.get( + notInstanceAvailableFlow.id + ).businessObject; + notInstanceAvailableFlowBo.name = "no"; + let notInstanceAvailableFlowCondition = bpmnFactory.create( + "bpmn:FormalExpression" + ); + notInstanceAvailableFlowCondition.body = + '${execution.hasVariable("instanceAvailable") == false || instanceAvailable == false}'; + notInstanceAvailableFlowBo.conditionExpression = + notInstanceAvailableFlowCondition; + + // add connection from dedicatedGateway to joining joiningDedicatedGateway and add condition + let notDedicatedFlow = modeling.connect( + dedicatedGateway, + joiningDedicatedGateway, + { + type: "bpmn:SequenceFlow", + } + ); + let notDedicatedFlowBo = elementRegistry.get( + notDedicatedFlow.id + ).businessObject; + notDedicatedFlowBo.name = "yes"; + let notDedicatedFlowCondition = bpmnFactory.create( + "bpmn:FormalExpression" + ); + notDedicatedFlowCondition.body = + '${execution.hasVariable("dedicatedHosting") == true && dedicatedHosting == true}'; + notDedicatedFlowBo.conditionExpression = notDedicatedFlowCondition; + + let topicName = makeId(12); + const scriptTaskUploadToContainer = modeling.createShape( + { type: "bpmn:ScriptTask" }, + { x: 50, y: 50 }, + subProcess, + {} + ); + scriptTaskUploadToContainer.businessObject.set( + "scriptFormat", + "javascript" + ); + scriptTaskUploadToContainer.businessObject.asyncBefore = true; + scriptTaskUploadToContainer.businessObject.asyncAfter = true; + scriptTaskUploadToContainer.businessObject.set( + "script", + createDeploymentScript( + config.getOpenTOSCAEndpoint(), + getCamundaEndpoint(), + topicName, + subProcess.id, + CSARForServiceTask.inputParams, + serviceTask.id, + CSARForServiceTask.reconstructedVMs + ) + ); + scriptTaskUploadToContainer.businessObject.set( + "name", + "Upload to Container" ); - serviceTask2.businessObject.set("scriptFormat", "javascript"); - serviceTask2.businessObject.set( + + modeling.connect(joiningDedicatedGateway, scriptTaskUploadToContainer, { + type: "bpmn:SequenceFlow", + }); + + const scriptTaskWaitForDeployment = modeling.createShape( + { type: "bpmn:ScriptTask" }, + { x: 50, y: 50 }, + subProcess, + {} + ); + scriptTaskWaitForDeployment.businessObject.set( + "scriptFormat", + "javascript" + ); + scriptTaskWaitForDeployment.businessObject.asyncBefore = true; + scriptTaskWaitForDeployment.businessObject.asyncAfter = true; + scriptTaskWaitForDeployment.businessObject.set( "script", - createWaitScript({ subprocessId: subProcess.id }) + createWaitScript(subProcess.id, serviceTask.id) + ); + scriptTaskWaitForDeployment.businessObject.set("name", "Deploy Service"); + modeling.connect( + scriptTaskUploadToContainer, + scriptTaskWaitForDeployment, + { + type: "bpmn:SequenceFlow", + } ); - serviceTask2.businessObject.set("name", "Wait for deployment"); - const serviceTask3 = modeling.appendShape( - serviceTask2, + let joiningInstanceAvailablityGatewayGateway = modeling.createShape( + { type: "bpmn:ExclusiveGateway" }, + { x: 50, y: 50 }, + subProcess, + {} + ); + modeling.connect( + scriptTaskWaitForDeployment, + joiningInstanceAvailablityGatewayGateway, { - type: "bpmn:ServiceTask", - }, - { x: 800, y: 200 } + type: "bpmn:SequenceFlow", + } + ); + + // add connection from instanceAvailableGateway to joiningInstanceAvailableGateway and add condition + let instanceAvailableFlow = modeling.connect( + instanceAvailablityGateway, + joiningInstanceAvailablityGatewayGateway, + { + type: "bpmn:SequenceFlow", + } + ); + let InstanceAvailableFlowBo = elementRegistry.get( + instanceAvailableFlow.id + ).businessObject; + InstanceAvailableFlowBo.name = "yes"; + let InstanceAvailableFlowCondition = bpmnFactory.create( + "bpmn:FormalExpression" ); + InstanceAvailableFlowCondition.body = + '${execution.hasVariable("instanceAvailable") == true && instanceAvailable == true}'; + InstanceAvailableFlowBo.conditionExpression = + InstanceAvailableFlowCondition; - serviceTask3.businessObject.set("name", "Call service"); + const serviceTaskInvokeService = modeling.createShape( + { type: "bpmn:ServiceTask" }, + { x: 50, y: 50 }, + subProcess, + {} + ); + const extensionElements = serviceTask.businessObject.extensionElements; + serviceTaskInvokeService.businessObject.set("name", "Invoke Service"); if (!extensionElements) { - serviceTask3.businessObject.set("camunda:type", "external"); - serviceTask3.businessObject.set("camunda:topic", topicName); + serviceTaskInvokeService.businessObject.set("camunda:type", "external"); + serviceTaskInvokeService.businessObject.asyncBefore = true; + serviceTaskInvokeService.businessObject.asyncAfter = true; + serviceTaskInvokeService.businessObject.set("camunda:topic", topicName); } else { const values = extensionElements.values; for (let value of values) { if (value.inputOutput === undefined) continue; for (let param of value.inputOutput.inputParameters) { if (param.name === "url") { - param.value = `\${selfserviceApplicationUrl.concat(${JSON.stringify( + param.value = `\${${ + serviceTask.id + }_selfserviceApplicationUrl.concat(${JSON.stringify( param.value || "" )})}`; break; @@ -245,26 +722,51 @@ export async function startOnDemandReplacementProcess(xml) { } } + modeling.connect( + joiningInstanceAvailablityGatewayGateway, + serviceTaskInvokeService, + { + type: "bpmn:SequenceFlow", + } + ); + const newExtensionElements = createElement( "bpmn:ExtensionElements", { values }, - serviceTask2.businessObject, + scriptTaskWaitForDeployment.businessObject, bpmnFactory ); + + // remove attributes from original service task that was replaced by subprocess subProcess.businessObject.set("extensionElements", undefined); - serviceTask3.businessObject.set( + + let subprocessInputOutput = getCamundaInputOutput( + subProcess.businessObject, + bpmnFactory + ); + subprocessInputOutput.inputParameters.push( + bpmnFactory.create("camunda:InputParameter", { + name: "dedicatedHosting", + value: String(CSARForServiceTask.dedicatedHosting) ?? "false", + }) + ); + + serviceTaskInvokeService.businessObject.set( "extensionElements", newExtensionElements ); } - modeling.appendShape( - serviceTask3, - { - type: "bpmn:EndEvent", - }, - { x: 1000, y: 200 }, - subProcess + let endEvent = modeling.createShape( + { type: "bpmn:EndEvent" }, + { x: 50, y: 50 }, + subProcess, + {} ); + modeling.connect(serviceTaskInvokeService, endEvent, { + type: "bpmn:SequenceFlow", + }); + + layout(modeling, elementRegistry, rootElement); } } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/DeploymentPlugin.js b/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/DeploymentPlugin.js index 32200847..f07b50dd 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/DeploymentPlugin.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/DeploymentPlugin.js @@ -25,6 +25,7 @@ import { bindUsingPull, bindUsingPush } from "../../../deployment/BindingUtils"; import { completeIncompleteDeploymentModel, getServiceTasksToDeploy, + getTopology, } from "../../../deployment/DeploymentUtils"; import { getModeler } from "../../../../../editor/ModelerHandler"; import NotificationHandler from "../../../../../editor/ui/notifications/NotificationHandler"; @@ -35,6 +36,7 @@ import { startOnDemandReplacementProcess } from "../../../replacement/OnDemandTr import { deletePolicies, getPolicies } from "../../../utilities/Utilities"; import { CLOUD_DEPLOYMENT_MODEL_POLICY, + DEDICATED_HOSTING_POLICY, LOCATION_POLICY, } from "../../../Constants"; @@ -91,26 +93,19 @@ export default class DeploymentPlugin extends PureComponent { * @param result the result from the dialog */ async handleOnDemandDeploymentClosed(result) { - if (result && result.hasOwnProperty("onDemand")) { + if (result && result.hasOwnProperty("next") && result.next === true) { + console.log("Starting on-demand transformation: ", result); let xml = (await this.modeler.saveXML({ format: true })).xml; - if (result.onDemand) { - xml = await startOnDemandReplacementProcess(xml); - loadDiagram(xml, this.modeler); - this.setState({ - windowOpenOnDemandDeploymentOverview: false, - windowOpenDeploymentOverview: true, - windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: false, - }); - } - } else { - this.setState({ - windowOpenOnDemandDeploymentOverview: false, - windowOpenDeploymentOverview: false, - windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: false, - }); + xml = await startOnDemandReplacementProcess(xml, result.csarList); + loadDiagram(xml, this.modeler); } + + this.setState({ + windowOpenDeploymentOverview: false, + windowOpenDeploymentInput: false, + windowOpenDeploymentBinding: false, + windowOpenOnDemandDeploymentOverview: false, + }); } /** @@ -166,6 +161,7 @@ export default class DeploymentPlugin extends PureComponent { windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, windowOpenDeploymentBinding: false, + windowOpenOnDemandDeploymentOverview: false, }); return; } @@ -185,6 +181,7 @@ export default class DeploymentPlugin extends PureComponent { windowOpenDeploymentOverview: false, windowOpenDeploymentInput: true, windowOpenDeploymentBinding: false, + windowOpenOnDemandDeploymentOverview: false, csarList: csarList, }); return; @@ -195,6 +192,7 @@ export default class DeploymentPlugin extends PureComponent { windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, windowOpenDeploymentBinding: false, + windowOpenOnDemandDeploymentOverview: false, }); } @@ -211,10 +209,24 @@ export default class DeploymentPlugin extends PureComponent { result.nodeTypeRequirements ); + let reconstructedVMs = {}; + result.requiredVMAttributesMappedToOtherNodetype.forEach((attr) => { + reconstructedVMs[attr.nodeTypeName] ??= { + name: attr.nodeTypeName, + qName: attr.qName, + }; + reconstructedVMs[attr.nodeTypeName]["requiredAttributes"] ??= {}; + reconstructedVMs[attr.nodeTypeName].requiredAttributes[ + attr.requiredAttribute + ] = + result.nodeTypeRequirements[attr.nodeTypeName].requiredAttributes[ + attr.requiredAttribute + ]; + }); + // Blacklist Nodetypes which don't have their requirements fulfilled for Incomplete Deployment Models - const nodeTypeRequirements = result.nodeTypeRequirements; let blacklistedNodetypes = []; - Object.entries(nodeTypeRequirements).forEach(([key, value]) => { + Object.entries(reconstructedVMs).forEach(([key, value]) => { console.log(value); Object.values(value.requiredAttributes).forEach((innerValue) => { if ( @@ -225,6 +237,29 @@ export default class DeploymentPlugin extends PureComponent { } }); }); + + const nodeTypeRequirements = result.nodeTypeRequirements; + Object.entries(nodeTypeRequirements).forEach(([key, value]) => { + console.log(value); + Object.entries(value.requiredAttributes).forEach( + ([innerKey, innerValue]) => { + if ( + innerValue === "" && + !blacklistedNodetypes.includes(value.qName) && + !innerKey?.startsWith("VM") + ) { + blacklistedNodetypes.push(value.qName); + } + } + ); + // remove VM attributes from other Nodetypes + value.requiredAttributes = Object.fromEntries( + Object.entries(value.requiredAttributes).filter( + ([innerKey, innerValue]) => !innerKey?.startsWith("VM") + ) + ); + console.log("value" + value.requiredAttributes.length); + }); console.log("Blacklisted NodeTypes: ", blacklistedNodetypes); // collect input parameters of all NodeTypes that might be used during completion @@ -250,10 +285,9 @@ export default class DeploymentPlugin extends PureComponent { let csarList = result.csarList; console.log("List of CSARs before completion: ", csarList); - for (let csar of csarList) { + for (var i in csarList) { + let csar = csarList[i]; if (csar.incomplete) { - console.log("Found incomplete CSAR: ", csar.csarName); - // retrieve policies for the ServiceTask the CSAR belongs to let policyShapes = getPolicies(this.modeler, csar.serviceTaskIds[0]); let policies = {}; @@ -274,81 +308,185 @@ export default class DeploymentPlugin extends PureComponent { ); policies[policy.type] = policy.businessObject.location; break; + case DEDICATED_HOSTING_POLICY: + csar.dedicatedHosting = true; + break; default: console.error( - "Policy of type %s not supported for completion!" + "Policy of type %s not supported for completion!", + policy.type ); } }); console.log("Invoking completion with policies: ", policies); - // complete CSAR and refresh meta data - const locationOfCompletedCSAR = completeIncompleteDeploymentModel( - csar.url, - blacklistedNodetypes, - policies - ); - if (!locationOfCompletedCSAR) { - // notify user about failed completion - NotificationHandler.getInstance().displayNotification({ - type: "error", - title: "Unable to complete ServiceTemplate", - content: - "ServiceTemplate with Id '" + - csar.csarName + - "' could not be completed!", - duration: 20000, - }); + if (csar.onDemand) { + // add variables in case the CSAR is on-demand to enable a later transformation + console.log( + "CSAR %s is incomplete and on-demand. Adding inputs and blacklisted NodeTypes", + csar.csarName + ); + csar.blacklistedNodetypes = blacklistedNodetypes; + csar.policies = policies; + csar.inputParams = inputParams; + csar.reconstructedVMs = reconstructedVMs; + } else { + console.log( + "Found incomplete CSAR which is not deployed on-demand: ", + csar.csarName + ); - // abort process - this.setState({ - windowOpenDeploymentOverview: false, - windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: false, - }); - return; + // complete CSAR and refresh meta data + const locationOfCompletedCSAR = completeIncompleteDeploymentModel( + csar.url, + blacklistedNodetypes, + policies + ); + if (!locationOfCompletedCSAR) { + // notify user about failed completion + NotificationHandler.getInstance().displayNotification({ + type: "error", + title: "Unable to complete ServiceTemplate", + content: + "ServiceTemplate with Id '" + + csar.csarName + + "' could not be completed!", + duration: 20000, + }); + + // abort process + this.setState({ + windowOpenDeploymentOverview: false, + windowOpenDeploymentInput: false, + windowOpenDeploymentBinding: false, + windowOpenOnDemandDeploymentOverview: false, + }); + return; + } + const nameOfCompletedCSAR = locationOfCompletedCSAR + .split("/") + .filter((x) => x.length > 1) + .pop(); + csar.url = locationOfCompletedCSAR + "?csar"; + csar.csarName = nameOfCompletedCSAR + ".csar"; + csar.incomplete = false; + console.log("Completed CSAR. New name: ", csar.csarName); + console.log("New location: ", csar.url); + + // update the deployment model connected to the ServiceTask + let serviceTask = this.modeler + .get("elementRegistry") + .get(csar.serviceTaskIds[0]); + serviceTask.businessObject.deploymentModelUrl = + "{{ wineryEndpoint }}/servicetemplates/" + + csar.url.split("/servicetemplates/")[1]; + + // delete the policies as they are now incorporated into the new deployment model + deletePolicies(this.modeler, csar.serviceTaskIds[0]); + + // upload completed CSAR to the OpenTOSCA Container + console.log( + "Uploading CSAR to the OpenTOSCA Container at: ", + this.modeler.config.opentoscaEndpoint + ); + let uploadResult = await uploadCSARToContainer( + this.modeler.config.opentoscaEndpoint, + csar.csarName, + csar.url, + this.modeler.config.wineryEndpoint + ); + if (uploadResult.success === false) { + // notify user about failed CSAR upload + NotificationHandler.getInstance().displayNotification({ + type: "error", + title: "Unable to upload CSAR to the OpenTOSCA Container", + content: + "CSAR defined for ServiceTasks with Id '" + + csar.serviceTaskIds + + "' could not be uploaded to the connected OpenTOSCA Container!", + duration: 20000, + }); + + // abort process + this.setState({ + windowOpenDeploymentOverview: false, + windowOpenDeploymentInput: false, + windowOpenDeploymentBinding: false, + windowOpenOnDemandDeploymentOverview: false, + }); + return; + } + + // set URL of the CSAR in the OpenTOSCA Container which is required to create instances + console.log("Upload successfully!"); + csar.buildPlanUrl = uploadResult.url; + csar.inputParameters = uploadResult.inputParameters; + csar.wasIncomplete = true; + console.log("Build plan URL: ", csar.buildPlanUrl); + console.log("Input Parameters: ", csar.inputParameters); + + // update element in list + csarList[i] = csar; } - const nameOfCompletedCSAR = locationOfCompletedCSAR - .split("/") - .filter((x) => x.length > 1) - .pop(); - csar.url = locationOfCompletedCSAR + "?csar"; - csar.csarName = nameOfCompletedCSAR + ".csar"; - csar.incomplete = false; - console.log("Completed CSAR. New name: ", csar.csarName); - console.log("New location: ", csar.url); - - // update the deployment model connected to the ServiceTask - let serviceTask = this.modeler - .get("elementRegistry") - .get(csar.serviceTaskIds[0]); - serviceTask.businessObject.deploymentModelUrl = - "{{ wineryEndpoint }}/servicetemplates/" + - csar.url.split("/servicetemplates/")[1]; - - // delete the policies as they are now incorporated into the new deployment model - deletePolicies(this.modeler, csar.serviceTaskIds[0]); - - // upload completed CSAR to the OpenTOSCA Container - console.log( - "Uploading CSAR to the OpenTOSCA Container at: ", - this.modeler.config.opentoscaEndpoint - ); - let uploadResult = await uploadCSARToContainer( - this.modeler.config.opentoscaEndpoint, - csar.csarName, - csar.url, - this.modeler.config.wineryEndpoint + } + } + console.log("Retrieved CSAR list after completion: ", csarList); + + // calculate progress step size for the number of CSARs to create a service instance for + let progressStep = Math.round( + 90 / csarList.filter((csar) => !csar.onDemand).length + ); + + // create service instances for all CSARs, which are not on-demand + for (let i = 0; i < csarList.length; i++) { + let csar = csarList[i]; + if (csar.onDemand) { + console.log("Skipping CSAR as it is deployed on-demand: ", csar); + } else { + console.log("Creating service instance for CSAR: ", csar); + + if (csar?.wasIncomplete === true) { + // Add suitable VM properties for completion + const deployedTopology = getTopology(csar.url); + for (const [key, value] of Object.entries( + deployedTopology.nodeTemplates + )) { + for (const [constructKey, constructValue] of Object.entries( + reconstructedVMs + )) { + if ( + constructValue.name.includes(value.name) && + !value.name.includes("VM") + ) { + inputParams = Object.assign( + {}, + inputParams, + constructValue.requiredAttributes + ); + } + } + } + } + console.log("Updated input params" + inputParams); + + let instanceCreationResponse = await createServiceInstance( + csar, + this.modeler.config.camundaEndpoint, + this.modeler.config.qprovEndpoint, + inputParams ); - if (uploadResult.success === false) { - // notify user about failed CSAR upload + console.log("Creating service instance for CSAR: ", csar); + csar.properties = instanceCreationResponse.properties; + csar.buildPlanUrl = instanceCreationResponse.buildPlanUrl; + if (instanceCreationResponse.success === false) { + // notify user about failed instance creation NotificationHandler.getInstance().displayNotification({ type: "error", - title: "Unable to upload CSAR to the OpenTOSCA Container", + title: "Unable to create service instace", content: - "CSAR defined for ServiceTasks with Id '" + - csar.serviceTaskIds + - "' could not be uploaded to the connected OpenTOSCA Container!", + "Unable to create service instance for CSAR '" + + csar.csarName + + "'. Aborting process!", duration: 20000, }); @@ -357,65 +495,19 @@ export default class DeploymentPlugin extends PureComponent { windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, windowOpenDeploymentBinding: false, + windowOpenOnDemandDeploymentOverview: false, }); return; } - // set URL of the CSAR in the OpenTOSCA Container which is required to create instances - console.log("Upload successfully!"); - csar.buildPlanUrl = uploadResult.url; - csar.inputParameters = uploadResult.inputParameters; - console.log("Build plan URL: ", csar.buildPlanUrl); - console.log("Input Parameters: ", csar.inputParameters); - } - } - console.log("Retrieved CSAR list after completion: ", csarList); - - // calculate progress step size for the number of CSARs to create a service instance for - let progressStep = Math.round(90 / csarList.length); - - // create service instances for all CSARs - for (let i = 0; i < csarList.length; i++) { - let csar = csarList[i]; - console.log("Creating service instance for CSAR: ", csar); - - let instanceCreationResponse = await createServiceInstance( - csar, - this.modeler.config.camundaEndpoint, - this.modeler.config.qprovEndpoint, - inputParams - ); - console.log("Creating service instance for CSAR: ", csar); - csar.properties = instanceCreationResponse.properties; - csar.buildPlanUrl = instanceCreationResponse.buildPlanUrl; - if (instanceCreationResponse.success === false) { - // notify user about failed instance creation - NotificationHandler.getInstance().displayNotification({ - type: "error", - title: "Unable to create service instace", - content: - "Unable to create service instance for CSAR '" + - csar.csarName + - "'. Aborting process!", - duration: 20000, - }); - - // abort process - this.setState({ - windowOpenDeploymentOverview: false, - windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: false, - }); - return; - } + // store topic name for pulling services + if (instanceCreationResponse.topicName !== undefined) { + csar.topicName = instanceCreationResponse.topicName; + } - // store topic name for pulling services - if (instanceCreationResponse.topicName !== undefined) { - csar.topicName = instanceCreationResponse.topicName; + // increase progress in the UI + this.handleProgress(progressBar, progressStep); } - - // increase progress in the UI - this.handleProgress(progressBar, progressStep); } // update CSAR list for the binding @@ -425,6 +517,7 @@ export default class DeploymentPlugin extends PureComponent { windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, windowOpenDeploymentBinding: true, + windowOpenOnDemandDeploymentOverview: false, }); return; } @@ -434,6 +527,7 @@ export default class DeploymentPlugin extends PureComponent { windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, windowOpenDeploymentBinding: false, + windowOpenOnDemandDeploymentOverview: false, }); } @@ -449,69 +543,83 @@ export default class DeploymentPlugin extends PureComponent { let csarList = result.csarList; for (let i = 0; i < csarList.length; i++) { let csar = csarList[i]; + if (!csar.onDemand) { + let serviceTaskIds = csar.serviceTaskIds; + for (let j = 0; j < serviceTaskIds.length; j++) { + // bind the service instance using the specified binding pattern + let bindingResponse = undefined; + if (csar.type === "pull") { + bindingResponse = bindUsingPull( + csar, + serviceTaskIds[j], + this.modeler.get("elementRegistry"), + this.modeler.get("modeling") + ); + } else if (csar.type === "push") { + bindingResponse = bindUsingPush( + csar, + serviceTaskIds[j], + this.modeler.get("elementRegistry") + ); + } - let serviceTaskIds = csar.serviceTaskIds; - for (let j = 0; j < serviceTaskIds.length; j++) { - // bind the service instance using the specified binding pattern - let bindingResponse = undefined; - if (csar.type === "pull") { - bindingResponse = bindUsingPull( - csar, - serviceTaskIds[j], - this.modeler.get("elementRegistry"), - this.modeler.get("modeling") - ); - } else if (csar.type === "push") { - bindingResponse = bindUsingPush( - csar, - serviceTaskIds[j], - this.modeler.get("elementRegistry") - ); - } - - // abort if binding pattern is invalid or binding fails - if ( - bindingResponse === undefined || - bindingResponse.success === false - ) { - // notify user about failed binding - NotificationHandler.getInstance().displayNotification({ - type: "error", - title: "Unable to perform binding", - content: - "Unable to bind ServiceTask with Id '" + - serviceTaskIds[j] + - "' using binding pattern '" + - csar.type + - "'. Aborting process!", - duration: 20000, - }); - - // abort process - this.setState({ - windowOpenDeploymentOverview: false, - windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: false, - }); - return; + // abort if binding pattern is invalid or binding fails + if ( + bindingResponse === undefined || + bindingResponse.success === false + ) { + // notify user about failed binding + NotificationHandler.getInstance().displayNotification({ + type: "error", + title: "Unable to perform binding", + content: + "Unable to bind ServiceTask with Id '" + + serviceTaskIds[j] + + "' using binding pattern '" + + csar.type + + "'. Aborting process!", + duration: 20000, + }); + + // abort process + this.setState({ + windowOpenDeploymentOverview: false, + windowOpenDeploymentInput: false, + windowOpenDeploymentBinding: false, + windowOpenOnDemandDeploymentOverview: false, + }); + return; + } } + } else { + console.log( + "CSAR is on-demand and will be bound during runtime: ", + csar + ); } } + if (csarList.filter((csar) => csar.onDemand).length > 0) { + console.log( + "On-demand CSARs available. Opening transformation modal..." + ); + this.setState({ + windowOpenDeploymentOverview: false, + windowOpenDeploymentInput: false, + windowOpenDeploymentBinding: false, + windowOpenOnDemandDeploymentOverview: true, + }); + return; + } - // notify user about successful binding - NotificationHandler.getInstance().displayNotification({ - type: "info", - title: "Binding completed", - content: - "Binding of the deployed service instances completed. The resulting workflow can now be deployed to the Camunda engine!", - duration: 20000, - }); + this.csarList = csarList; } + // cancel button was pressed or no on-demand CSARs this.setState({ windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, windowOpenDeploymentBinding: false, + windowOpenOnDemandDeploymentOverview: false, }); } @@ -582,7 +690,7 @@ export default class DeploymentPlugin extends PureComponent { className="qwm-toolbar-btn" title="Open service deployment menu" onClick={() => - this.setState({ windowOpenOnDemandDeploymentOverview: true }) + this.setState({ windowOpenDeploymentOverview: true }) } > @@ -594,7 +702,7 @@ export default class DeploymentPlugin extends PureComponent { {this.state.windowOpenOnDemandDeploymentOverview && ( )} diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentBindingModal.js b/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentBindingModal.js index e3619c53..d71d1573 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentBindingModal.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentBindingModal.js @@ -27,28 +27,32 @@ export default function ServiceDeploymentBindingModal({ onClose, initValues }) { let csar = initValues[i]; let serviceTaskIds = csar.serviceTaskIds; for (let j = 0; j < serviceTaskIds.length; j++) { - if (csar.type === "pull") { - bindByPullTasks.push( -
  • - {serviceTaskIds[j]} -
  • + if (!csar.onDemand) { + if (csar.type === "pull") { + bindByPullTasks.push( +
  • + {serviceTaskIds[j]} +
  • + ); + continue; + } + + if (csar.type === "push") { + bindByPushTasks.push( +
  • + {serviceTaskIds[j]} +
  • + ); + continue; + } + + console.error( + "Found task that does not use the push or pull pattern: %s", + serviceTaskIds[j] ); - continue; + } else { + console.log("CSAR is on-demand and will be bound later: ", csar); } - - if (csar.type === "push") { - bindByPushTasks.push( -
  • - {serviceTaskIds[j]} -
  • - ); - continue; - } - - console.error( - "Found task that does not use the push or pull pattern: %s", - serviceTaskIds[j] - ); } } @@ -57,9 +61,15 @@ export default function ServiceDeploymentBindingModal({ onClose, initValues }) { const onFinished = () => onClose({ next: true, csarList: initValues }); + // skip dialog if there is nothing to bind + if (!bindByPull && !bindByPush) { + console.log("Nothing to bind. Skipping dialog..."); + onFinished(); + } + return ( - Service Deployment (4/4) + Service Deployment (3/4)

    diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentInputModal.js b/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentInputModal.js index 7d4fbacb..a133bbc3 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentInputModal.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentInputModal.js @@ -10,7 +10,7 @@ */ /* eslint-disable no-unused-vars */ -import React from "react"; +import React, { useEffect } from "react"; // polyfill upcoming structural components import Modal from "../../../../../editor/ui/modal/Modal"; @@ -46,12 +46,46 @@ export default function ServiceDeploymentInputModal({ onClose, initValues }) { initValues.filter((csar) => csar.incomplete).length > 0; let completionHTML = []; let nodetypesToChange = {}; + let requiredVMAttributesMappedToOtherNodetype = []; + let attributeListHTMLMap = {}; if (containsIncompleteModels) { try { const url = config.wineryEndpoint + "/nodetypes"; const nodetypes = JSON.parse(synchronousGetRequest(url)); console.log("Found NodeTypes: ", nodetypes); + // save requiredAttributes of NodeTypes of type VM to Map them to corresponding Host NodeType + nodetypes.forEach((nodetype) => { + const nodetypeUri = encodeURIComponent( + encodeURIComponent(nodetype.qName.substring(1, nodetype.qName.length)) + ).replace("%257D", "/"); + const tags = JSON.parse( + synchronousGetRequest(url + "/" + nodetypeUri + "/tags") + ); + const type = tags.filter((x) => x.name === "type")[0]; + if (type?.value === "vm") { + const requiredAttributes = tags + .filter((x) => x.name === "requiredAttributes")?.[0] + ?.value?.split(","); + if (requiredAttributes !== undefined) { + const attributeListHTML = []; + requiredAttributes.sort(); + nodetype.requiredAttributes = {}; + nodetypesToChange[nodetype.name] = nodetype; + requiredAttributes.forEach((attribute) => { + // Some VM requiredAttributes are host dependent and thus need to be mapped to host + if (attribute.split(".").length > 1) { + requiredVMAttributesMappedToOtherNodetype.push({ + nodeTypeName: attribute.split(".")[0], + requiredAttribute: attribute.split(".")[1], + vmQName: nodetype.qName, + }); + } + }); + } + } + }); + nodetypes.forEach((nodetype) => { const nodetypeUri = encodeURIComponent( encodeURIComponent(nodetype.qName.substring(1, nodetype.qName.length)) @@ -64,55 +98,69 @@ export default function ServiceDeploymentInputModal({ onClose, initValues }) { ?.value?.split(","); if (requiredAttributes !== undefined) { const attributeListHTML = []; + + // insert requiredVMAttributesMappedToOtherNodetype if suitable + requiredVMAttributesMappedToOtherNodetype.forEach((vmRequirement) => { + if (nodetype.name === vmRequirement.nodeTypeName) { + vmRequirement["qName"] = nodetype.qName; + requiredAttributes.push(vmRequirement.requiredAttribute); + } + }); + requiredAttributes.sort(); nodetype.requiredAttributes = {}; nodetypesToChange[nodetype.name] = nodetype; requiredAttributes.forEach((attribute) => { - nodetype.requiredAttributes[attribute] = ""; - attributeListHTML.push( - - {attribute} - -