Repository: DataBiosphere/toil

Issue:
Toil WES support change workflow_params to be optional
When launching a workflow using a Toil WES server, the `workflow_params` (input JSON file) is required, even if the workflow itself takes no inputs. Not including the `workflow_params` body part in the HTTP request causes the WEs server to return an error:
```
io.openapi.wes.client.ApiException: {
  "msg": "Missing 'workflow_params' in submission",
  "status_code": 400
}
```

Ideally, this parameter should be optional, to allow easy launching of input parameter-free workflows. This is pretty easy to work around though, as passing in a dummy test parameter file suppresses the exception.

┆Issue is synchronized with this [Jira Story](https://ucsc-cgl.atlassian.net/browse/TOIL-1143)
┆friendlyId: TOIL-1143



Discussion:

Resolving patch:
diff --git a/src/toil/server/wes/abstract_backend.py b/src/toil/server/wes/abstract_backend.py
--- a/src/toil/server/wes/abstract_backend.py
+++ b/src/toil/server/wes/abstract_backend.py
@@ -256,7 +256,8 @@ def collect_attachments(self, run_id: Optional[str], temp_dir: Optional[str]) ->
         else:
             raise MalformedRequestException("Missing 'workflow_url' in submission")
 
-        if "workflow_params" not in body:
-            raise MalformedRequestException("Missing 'workflow_params' in submission")
+        if "workflow_params" in body and not isinstance(body["workflow_params"], dict):
+            # They sent us something silly like "workflow_params": "5"
+            raise MalformedRequestException("Got a 'workflow_params' which does not decode to a JSON object")
 
         return temp_dir, body
diff --git a/src/toil/server/wes/tasks.py b/src/toil/server/wes/tasks.py
--- a/src/toil/server/wes/tasks.py
+++ b/src/toil/server/wes/tasks.py
@@ -191,8 +191,9 @@ def initialize_run(self) -> List[str]:
         to be executed. Return that list of shell commands that should be
         executed in order to complete this workflow run.
         """
-        # Obtain CWL-style workflow parameters from the request.
-        workflow_params = self.request["workflow_params"]
+        # Obtain CWL-style workflow parameters from the request. Default to an
+        # empty dict if not found, because we want to tolerate omitting this.
+        workflow_params = self.request.get("workflow_params", {})
 
         # And any workflow engine parameters the user specified.
         workflow_engine_parameters = self.request.get("workflow_engine_parameters", {})


Test patch:
diff --git a/src/toil/test/server/serverTest.py b/src/toil/test/server/serverTest.py
--- a/src/toil/test/server/serverTest.py
+++ b/src/toil/test/server/serverTest.py
@@ -464,18 +464,26 @@ class ToilWESServerWorkflowTest(AbstractToilWESServerTest):
     Tests of the WES server running workflows.
     """
 
-    def run_zip_workflow(self, zip_path: str, include_message: bool = True) -> None:
+    def run_zip_workflow(self, zip_path: str, include_message: bool = True, include_params: bool = True) -> None:
         """
         We have several zip file tests; this submits a zip file and makes sure it ran OK.
+        
+        If include_message is set to False, don't send a "message" argument in  workflow_params.
+        If include_params is also set to False, don't send workflow_params at all.
         """
         self.assertTrue(os.path.exists(zip_path))
+        
+        # Set up what we will POST to start the workflow.
+        post_data = {
+            "workflow_url": "file://" + zip_path,
+            "workflow_type": "CWL",
+            "workflow_type_version": "v1.0"
+        }
+        if include_params or include_message:
+            # We need workflow_params too
+            post_data["workflow_params"] = json.dumps({"message": "Hello, world!"} if include_message else {})
         with self.app.test_client() as client:
-            rv = client.post("/ga4gh/wes/v1/runs", data={
-                "workflow_url": "file://" + zip_path,
-                "workflow_type": "CWL",
-                "workflow_type_version": "v1.0",
-                "workflow_params": json.dumps({"message": "Hello, world!"} if include_message else {})
-            })
+            rv = client.post("/ga4gh/wes/v1/runs", data=post_data)
             # workflow is submitted successfully
             self.assertEqual(rv.status_code, 200)
             self.assertTrue(rv.is_json)
@@ -586,6 +594,16 @@ def test_run_workflow_manifest_and_inputs_zip(self) -> None:
             zip_file.writestr('data.json', json.dumps({"message": "Hello, world!"}))
             zip_file.writestr('MANIFEST.json', json.dumps({"mainWorkflowURL": "actual.cwl", "inputFileURLs": ["data.json"]}))
         self.run_zip_workflow(zip_path, include_message=False)
+        
+    def test_run_workflow_no_params_zip(self) -> None:
+        """Test run example CWL workflow from ZIP without workflow_params."""
+        workdir = self._createTempDir()
+        zip_path = os.path.abspath(os.path.join(workdir, 'workflow.zip'))
+        with zipfile.ZipFile(zip_path, 'w') as zip_file:
+            zip_file.writestr('main.cwl', self.example_cwl)
+            zip_file.writestr('inputs.json', json.dumps({"message": "Hello, world!"}))
+        # Don't even bother sending workflow_params
+        self.run_zip_workflow(zip_path, include_message=False, include_params=False)
 
     def test_run_and_cancel_workflows(self) -> None:
         """