Properly deal with missing tools in subworkflows

If a subworkflow is missing tools the entire parent workflow couldn't
be loaded into the editor, because we raised an exception. This is now
only logged as an error. On the user side we now prevent running and
saving such workflows by actually implementing the `get_errors` method
for the SubWorkflowModule class.

This should fix https://github.com/galaxyproject/galaxy/issues/7140.
This commit is contained in:
mvdbeek
2019-01-17 13:41:21 +01:00
parent 105a1d8a61
commit 54aecb3f31
4 changed files with 37 additions and 14 deletions
@@ -189,7 +189,7 @@ class Workflow {
id: node.id,
type: node.type,
content_id: node.content_id,
tool_version: node.config_form.version,
tool_version: node.config_form ? node.config_form.version : null,
tool_state: node.tool_state,
errors: node.errors,
input_connections: input_connections,
+5
View File
@@ -103,6 +103,11 @@ class ToolMissingException(MessageException):
status_code = 400
err_code = error_codes.USER_TOOL_MISSING_PROBLEM
@property
def tool_id(self):
if 'tool_id' in self.extra_error_info:
return self.extra_error_info['tool_id']
class RequestParameterInvalidException(MessageException):
status_code = 400
+4 -3
View File
@@ -415,9 +415,10 @@ class WorkflowContentsManager(UsesAnnotations):
for step in workflow.steps:
try:
module_injector.inject(step, steps=workflow.steps, exact_tools=False)
except exceptions.ToolMissingException:
if step.tool_id not in missing_tools:
missing_tools.append(step.tool_id)
except exceptions.ToolMissingException as e:
# FIXME: if a subworkflow lacks multiple tools we report only the first missing tool
if e.tool_id not in missing_tools:
missing_tools.append(e.tool_id)
continue
if step.upgrade_messages:
has_upgrade_messages = True
+27 -10
View File
@@ -140,7 +140,7 @@ class WorkflowModule(object):
else:
self.state.inputs = safe_loads(state) or {}
def get_errors(self):
def get_errors(self, **kwargs):
""" This returns a step related error message as string or None """
return None
@@ -327,6 +327,13 @@ class SubWorkflowModule(WorkflowModule):
inputs.append(input)
return inputs
def get_modules(self):
return [module_factory.from_workflow_step(self.trans, step) for step in self.subworkflow.steps]
def get_errors(self, **kwargs):
errors = (module.get_errors(include_tool_id=True) for module in self.get_modules())
return [e for e in errors if e]
def get_data_outputs(self):
outputs = []
if hasattr(self.subworkflow, 'workflow_outputs'):
@@ -338,20 +345,23 @@ class SubWorkflowModule(WorkflowModule):
tooltip=False)
for order_index in sorted(subworkflow_dict['steps']):
step = subworkflow_dict['steps'][order_index]
data_outputs = subworkflow_dict['steps'][order_index]['data_outputs']
data_outputs = step['data_outputs']
for workflow_output in step['workflow_outputs']:
label = workflow_output['label']
if not label:
label = "%s:%s" % (order_index, workflow_output['output_name'])
for data_output in data_outputs:
if data_output['name'] == workflow_output['output_name']:
if data_output['name'] == workflow_output['output_name'] or data_output['uuid'] == workflow_output['uuid']:
data_output['label'] = label
data_output['name'] = label
# That's the right data_output
break
else:
# This hopefully can't happen, but let's be clear
raise Exception("Workflow output '%s' defined, but not listed among data outputs" % workflow_output['output_name'])
# This can happen when importing workflows with missing tools.
# We can't raise an exception here, as that would prevent loading
# the workflow.
log.error("Workflow output '%s' defined, but not listed among data outputs" % workflow_output['output_name'])
continue
outputs.append(data_output)
return outputs
@@ -732,8 +742,12 @@ class ToolModule(WorkflowModule):
# ---- Configuration time -----------------------------------------------
def get_errors(self):
return None if self.tool else "Tool is not installed."
def get_errors(self, include_tool_id=False, **kwargs):
if not self.tool:
if include_tool_id:
return "%s is not installed" % self.tool_id
else:
return "Tool is not installed"
def get_inputs(self):
return self.tool.inputs if self.tool else {}
@@ -841,7 +855,8 @@ class ToolModule(WorkflowModule):
return RuntimeValue()
visit_input_values(self.tool.inputs, self.state.inputs, callback)
else:
raise ToolMissingException("Tool %s missing. Cannot add dummy datasets." % self.tool_id)
raise ToolMissingException("Tool %s missing. Cannot add dummy datasets." % self.tool_id,
tool_id=self.tool_id)
def get_post_job_actions(self, incoming):
return ActionBox.handle_incoming(incoming)
@@ -870,7 +885,8 @@ class ToolModule(WorkflowModule):
state.inputs[RUNTIME_STEP_META_STATE_KEY] = step_metadata_runtime_state
return state, step_errors
else:
raise ToolMissingException("Tool %s missing. Cannot compute runtime state." % self.tool_id)
raise ToolMissingException("Tool %s missing. Cannot compute runtime state." % self.tool_id,
tool_id=self.tool_id)
def decode_runtime_state(self, runtime_state):
""" Take runtime state from persisted invocation and convert it
@@ -882,7 +898,8 @@ class ToolModule(WorkflowModule):
self.__restore_step_meta_runtime_state(loads(runtime_state[RUNTIME_STEP_META_STATE_KEY]))
return state
else:
raise ToolMissingException("Tool %s missing. Cannot recover runtime state." % self.tool_id)
raise ToolMissingException("Tool %s missing. Cannot recover runtime state." % self.tool_id,
tool_id=self.tool_id)
def execute(self, trans, progress, invocation_step, use_cached_job=False):
invocation = invocation_step.workflow_invocation