diff --git a/client/src/components/Workflow/Run/WorkflowRunForm.vue b/client/src/components/Workflow/Run/WorkflowRunForm.vue index 8b63094e327..a65441c99dd 100644 --- a/client/src/components/Workflow/Run/WorkflowRunForm.vue +++ b/client/src/components/Workflow/Run/WorkflowRunForm.vue @@ -206,6 +206,9 @@ export default { // Tool form always wants a list of invocations back // so that inputs can be batched. batch: true, + // the user is already warned if tool versions are wrong, + // they can still choose to invoke the workflow anyway. + require_exact_tool_versions: false, }; console.debug("WorkflowRunForm::onExecute()", "Ready for submission.", jobDef); diff --git a/client/src/components/Workflow/Run/WorkflowRunFormSimple.vue b/client/src/components/Workflow/Run/WorkflowRunFormSimple.vue index e95f828c33a..6d8543f0e66 100644 --- a/client/src/components/Workflow/Run/WorkflowRunFormSimple.vue +++ b/client/src/components/Workflow/Run/WorkflowRunFormSimple.vue @@ -114,6 +114,7 @@ export default { inputs_by: "step_index", batch: true, use_cached_job: this.useJobCache, + require_exact_tool_versions: false, }; if (this.targetHistory == "current") { data.history_id = this.model.historyId; diff --git a/lib/galaxy/managers/workflows.py b/lib/galaxy/managers/workflows.py index 1d5769e86bb..e1539a3bebd 100644 --- a/lib/galaxy/managers/workflows.py +++ b/lib/galaxy/managers/workflows.py @@ -1506,15 +1506,16 @@ class WorkflowContentsManager(UsesAnnotations): dry_run=refactor_request.dry_run, ) - def get_all_tool_ids(self, workflow): - tool_ids = set() + def get_all_tools(self, workflow): + tools = [] for step in workflow.steps: if step.type == 'tool': if step.tool_id: - tool_ids.add(step.tool_id) - elif step.type == 'subworkflow': - tool_ids.update(self.get_all_tool_ids(step.subworkflow)) - return tool_ids + if {"tool_id": step.tool_id, "tool_version": step.tool_version} not in tools: + tools.append({"tool_id": step.tool_id, "tool_version": step.tool_version}) + elif step.type == "subworkflow": + tools.extend(self.get_all_tools(step.subworkflow)) + return tools class RefactorRequest(RefactorActions): diff --git a/lib/galaxy/webapps/galaxy/api/workflows.py b/lib/galaxy/webapps/galaxy/api/workflows.py index 5e06952caea..793de027e55 100644 --- a/lib/galaxy/webapps/galaxy/api/workflows.py +++ b/lib/galaxy/webapps/galaxy/api/workflows.py @@ -191,8 +191,12 @@ class WorkflowsAPIController(BaseGalaxyAPIController, UsesStoredWorkflowMixin, U workflows = [] workflows_by_toolshed = dict() for value in rval: - tool_ids = self.workflow_contents_manager.get_all_tool_ids(self.__get_stored_workflow(trans, value['id']).latest_workflow) - missing_tool_ids = [tool_id for tool_id in tool_ids if self.app.toolbox.is_missing_shed_tool(tool_id)] + tools = self.workflow_contents_manager.get_all_tools( + self.__get_stored_workflow(trans, value["id"]).latest_workflow + ) + missing_tool_ids = [ + tool["tool_id"] for tool in tools if self.app.toolbox.is_missing_shed_tool(tool["tool_id"]) + ] if len(missing_tool_ids) > 0: value['missing_tools'] = missing_tool_ids workflows_missing_tools.append(value) @@ -752,10 +756,24 @@ class WorkflowsAPIController(BaseGalaxyAPIController, UsesStoredWorkflowMixin, U if not is_batch and len(run_configs) != 1: raise exceptions.RequestParameterInvalidException("Must specify 'batch' to use batch parameters.") - tool_ids = self.workflow_contents_manager.get_all_tool_ids(workflow) - missing_tool_ids = [tool_id for tool_id in tool_ids if not self.app.toolbox.has_tool(tool_id)] - if missing_tool_ids: - raise exceptions.MessageException(f"Workflow was not invoked; the following required tools are not installed: {', '.join(missing_tool_ids)}") + require_exact_tool_versions = util.string_as_bool(payload.get("require_exact_tool_versions", "true")) + tools = self.workflow_contents_manager.get_all_tools(workflow) + missing_tools = [ + tool + for tool in tools + if not self.app.toolbox.has_tool( + tool["tool_id"], tool_version=tool["tool_version"], exact=require_exact_tool_versions + ) + ] + if missing_tools: + missing_tools_message = "Workflow was not invoked; the following required tools are not installed: " + if require_exact_tool_versions: + missing_tools_message += ", ".join( + [f"{tool['tool_id']} (version {tool['tool_version']})" for tool in missing_tools] + ) + else: + missing_tools_message += ", ".join([tool["tool_id"] for tool in missing_tools]) + raise exceptions.MessageException(missing_tools_message) invocations = [] for run_config in run_configs: diff --git a/lib/galaxy_test/api/test_workflows.py b/lib/galaxy_test/api/test_workflows.py index 416f7dc31b3..8b355ebe6ff 100644 --- a/lib/galaxy_test/api/test_workflows.py +++ b/lib/galaxy_test/api/test_workflows.py @@ -20,6 +20,7 @@ from galaxy_test.base.populators import ( wait_on, WorkflowPopulator ) +from galaxy_test.base.uses_shed import UsesShed from galaxy_test.base.workflow_fixtures import ( WORKFLOW_NESTED_REPLACEMENT_PARAMETER, WORKFLOW_NESTED_RUNTIME_PARAMETER, @@ -208,8 +209,7 @@ input1: # - Allow post to workflows//run in addition to posting to # /workflows with id in payload. # - Much more testing obviously, always more testing. -class WorkflowsApiTestCase(BaseWorkflowsApiTestCase, ChangeDatatypeTestCase): - +class WorkflowsApiTestCase(BaseWorkflowsApiTestCase, ChangeDatatypeTestCase, UsesShed): def test_show_valid(self): workflow_id = self.workflow_populator.simple_workflow("dummy") workflow_id = self.workflow_populator.simple_workflow("test_regular") @@ -876,15 +876,34 @@ steps: assert invocation["state"] == "scheduled", invocation def test_run_workflow_with_missing_tool(self): + self.install_repository("iuc", "compose_text_param", "feb3acba1e0a") # 0.1.0 with self.dataset_populator.test_history() as history_id: workflow_id = self._upload_yaml_workflow(""" class: GalaxyWorkflow steps: - step1: + nonexistent: tool_id: nonexistent_tool tool_version: "0.1" -""") - invocation_response = self.__invoke_workflow(workflow_id, history_id=history_id, assert_ok=False) + label: nonexistent + compose_text_param: + tool_id: compose_text_param + tool_version: 0.0.1 + label: compose_text_param +""" + ) + # should fail and return both tool ids since version 0.0.1 of compose_text_param does not exist + invocation_response = self.__invoke_workflow( + workflow_id, history_id=history_id, assert_ok=False, request={"require_exact_tool_versions": True} + ) + self._assert_status_code_is(invocation_response, 400) + self.assertEqual( + invocation_response.json().get("err_msg"), + "Workflow was not invoked; the following required tools are not installed: nonexistent_tool (version 0.1), compose_text_param (version 0.0.1)", + ) + # should fail but return only the tool_id of non_existent tool as another version of compose_text_param is installed + invocation_response = self.__invoke_workflow( + workflow_id, history_id=history_id, assert_ok=False, request={"require_exact_tool_versions": False} + ) self._assert_status_code_is(invocation_response, 400) self.assertEqual(invocation_response.json().get('err_msg'), "Workflow was not invoked; the following required tools are not installed: nonexistent_tool")