Skip to content

Commit a8c64c3

Browse files
committed
Allow describing wdl with zipped imports.
1 parent 430fc40 commit a8c64c3

File tree

18 files changed

+202
-24
lines changed

18 files changed

+202
-24
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
Cromwell now allows opting into configured soft links on shared file systems such as HPC environments. More details can
88
be found [here](https://cromwell.readthedocs.io/en/stable/backends/HPC/#optional-docker-soft-links).
99

10+
### `/describe` endpoint support for `workflowDependencies`
11+
12+
Previously it was not possible to describe a `workflowSource` that required `workflowDependencies` as the `/describe`
13+
endpoint did not allow specifying the additional zip file.
14+
15+
Now the endpoint will allow the user to upload the `workflowDependencies` zip file, will validate the top level
16+
workflow plus the dependencies, then return the appropriate response including the defined workflow inputs and outputs.
17+
1018
## 87 Release Notes
1119

1220
### `upgrade` command removed from Womtool

CromIAM/src/main/resources/swagger/cromiam.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,11 @@ paths:
838838
required: false
839839
type: file
840840
in: formData
841+
- name: workflowDependencies
842+
description: ZIP file containing workflow source files that are used to resolve local imports. This zip bundle will be unpacked in a sandbox accessible to this workflow.
843+
required: false
844+
type: file
845+
in: formData
841846
- $ref: '#/parameters/workflowTypeParam'
842847
- $ref: '#/parameters/workflowTypeVersionParam'
843848
responses:

centaur/src/main/scala/centaur/test/Test.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,7 @@ object Operations extends StrictLogging {
244244
}
245245

246246
override def run: IO[Unit] =
247-
// We can't describe workflows based on zipped imports, so don't try:
248-
if (workflow.skipDescribeEndpointValidation || workflow.data.zippedImports.nonEmpty) {
247+
if (workflow.skipDescribeEndpointValidation) {
249248
IO.pure(())
250249
} else {
251250
checkDescriptionInner(0)

centaur/src/main/scala/centaur/test/workflow/Workflow.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ final case class Workflow private (testName: String,
4545
workflowUrl = data.workflowUrl,
4646
workflowType = data.workflowType,
4747
workflowTypeVersion = data.workflowTypeVersion,
48-
inputsJson = data.inputs.map(_.unsafeRunSync())
48+
inputsJson = data.inputs.map(_.unsafeRunSync()),
49+
zippedImports = data.zippedImports
4950
)
5051

5152
def secondRun: Workflow =

cromwellApiClient/src/main/scala/cromwell/api/CromwellClient.scala

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,13 @@ object CromwellClient {
266266
Multipart.FormData.BodyPart(name, HttpEntity(MediaTypes.`application/json`, ByteString(source)))
267267
}
268268

269-
val multipartFormData = Multipart.FormData(sourceBodyParts.toSeq: _*)
269+
val zipBodyParts = Map(
270+
"workflowDependencies" -> describeRequest.zippedImports
271+
) collect { case (name, Some(file)) =>
272+
Multipart.FormData.BodyPart.fromPath(name, MediaTypes.`application/zip`, file.path)
273+
}
274+
275+
val multipartFormData = Multipart.FormData((sourceBodyParts ++ zipBodyParts).toSeq: _*)
270276
multipartFormData.toEntity()
271277
}
272278

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package cromwell.api.model
22

3+
import better.files.File
4+
35
final case class WorkflowDescribeRequest(workflowSource: Option[String],
46
workflowUrl: Option[String],
57
workflowType: Option[String],
68
workflowTypeVersion: Option[String],
7-
inputsJson: Option[String]
9+
inputsJson: Option[String],
10+
zippedImports: Option[File]
811
)

docs/api/RESTAPI.md

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

engine/src/main/resources/swagger/cromwell.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,11 @@ paths:
662662
required: false
663663
type: file
664664
in: formData
665+
- name: workflowDependencies
666+
description: ZIP file containing workflow source files that are used to resolve local imports. This zip bundle will be unpacked in a sandbox accessible to this workflow.
667+
required: false
668+
type: file
669+
in: formData
665670
- $ref: '#/parameters/workflowTypeParam'
666671
- $ref: '#/parameters/workflowTypeVersionParam'
667672
responses:

engine/src/main/scala/cromwell/webservice/routes/WomtoolRouteSupport.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ trait WomtoolRouteSupport extends WebServiceUtils with GithubAuthVendingSupport
6060
val workflowInputs = data.get("workflowInputs").map(_.utf8String)
6161
val workflowType = data.get("workflowType").map(_.utf8String)
6262
val workflowVersion = data.get("workflowTypeVersion").map(_.utf8String)
63+
val workflowDependencies = data.get("workflowDependencies").map(_.toArray)
6364

6465
val wsfc = WorkflowSourceFilesCollection(
6566
workflowSource,
@@ -70,7 +71,7 @@ trait WomtoolRouteSupport extends WebServiceUtils with GithubAuthVendingSupport
7071
workflowInputs.getOrElse(""),
7172
workflowOptions = WorkflowOptions.empty,
7273
labelsJson = "",
73-
importsFile = None,
74+
importsFile = workflowDependencies,
7475
workflowOnHold = false,
7576
warnings = Seq.empty,
7677
requestedWorkflowId = None

engine/src/test/scala/cromwell/webservice/routes/CromwellApiServiceSpec.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -761,7 +761,9 @@ object CromwellApiServiceSpec {
761761
s"[reading back DescribeRequest contents] workflow url: ${sourceFiles.workflowUrl}",
762762
s"[reading back DescribeRequest contents] inputs: ${sourceFiles.inputsJson}",
763763
s"[reading back DescribeRequest contents] type: ${sourceFiles.workflowType}",
764-
s"[reading back DescribeRequest contents] version: ${sourceFiles.workflowTypeVersion}"
764+
s"[reading back DescribeRequest contents] version: ${sourceFiles.workflowTypeVersion}",
765+
s"[reading back DescribeRequest contents] dependencies: ${sourceFiles.importsZipFileOption
766+
.map(bytes => bytes.map(b => "0x%02X".format(b)).mkString("[", ", ", "]"))}"
765767
)
766768

767769
sender() ! DescribeSuccess(description =

engine/src/test/scala/cromwell/webservice/routes/WomtoolRouteSupportSpec.scala

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ class WomtoolRouteSupportSpec extends AsyncFlatSpec with ScalatestRouteTest with
4646
val workflowType = Multipart.FormData.BodyPart("workflowType", HttpEntity(ContentTypes.`text/plain(UTF-8)`, "WDL"))
4747
val workflowVersion =
4848
Multipart.FormData.BodyPart("workflowTypeVersion", HttpEntity(ContentTypes.`text/plain(UTF-8)`, "1.0"))
49+
val workflowDependencies =
50+
Multipart.FormData.BodyPart(
51+
"workflowDependencies",
52+
HttpEntity(MediaTypes.`application/zip`, Array[Byte](0x0a, 0x0b, 0x0c))
53+
)
4954

5055
val workflowSourceTriggerDescribeFailure =
5156
Multipart.FormData.BodyPart("workflowSource", HttpEntity(ContentTypes.`text/plain(UTF-8)`, "fail to describe"))
@@ -95,7 +100,8 @@ class WomtoolRouteSupportSpec extends AsyncFlatSpec with ScalatestRouteTest with
95100
"[reading back DescribeRequest contents] workflow url: None",
96101
"[reading back DescribeRequest contents] inputs: ",
97102
"[reading back DescribeRequest contents] type: None",
98-
"[reading back DescribeRequest contents] version: None"
103+
"[reading back DescribeRequest contents] version: None",
104+
"[reading back DescribeRequest contents] dependencies: None"
99105
),
100106
validWorkflow = true
101107
)
@@ -118,7 +124,8 @@ class WomtoolRouteSupportSpec extends AsyncFlatSpec with ScalatestRouteTest with
118124
"[reading back DescribeRequest contents] workflow url: Some(https://raw.githubusercontent.com/broadinstitute/cromwell/develop/womtool/src/test/resources/validate/wdl_draft3/valid/callable_imports/my_workflow.wdl)",
119125
"[reading back DescribeRequest contents] inputs: ",
120126
"[reading back DescribeRequest contents] type: None",
121-
"[reading back DescribeRequest contents] version: None"
127+
"[reading back DescribeRequest contents] version: None",
128+
"[reading back DescribeRequest contents] dependencies: None"
122129
),
123130
validWorkflow = true
124131
)
@@ -146,7 +153,43 @@ class WomtoolRouteSupportSpec extends AsyncFlatSpec with ScalatestRouteTest with
146153
"[reading back DescribeRequest contents] workflow url: None",
147154
"[reading back DescribeRequest contents] inputs: {\"a\":\"is for apple\"}",
148155
"[reading back DescribeRequest contents] type: Some(WDL)",
149-
"[reading back DescribeRequest contents] version: Some(1.0)"
156+
"[reading back DescribeRequest contents] version: Some(1.0)",
157+
"[reading back DescribeRequest contents] dependencies: None"
158+
),
159+
validWorkflow = true
160+
)
161+
}(responseAs[WorkflowDescription])
162+
}
163+
}
164+
165+
it should "include inputs, workflow type, workflow version, and workflow dependencies in the WorkflowSourceFilesCollection" in {
166+
Post(
167+
s"/womtool/$version/describe",
168+
Multipart
169+
.FormData(
170+
BodyParts.workflowSource,
171+
BodyParts.workflowInputs,
172+
BodyParts.workflowType,
173+
BodyParts.workflowVersion,
174+
BodyParts.workflowDependencies
175+
)
176+
.toEntity()
177+
) ~>
178+
akkaHttpService.womtoolRoutes ~>
179+
check {
180+
status should be(StatusCodes.OK)
181+
182+
assertResult {
183+
WorkflowDescription(
184+
valid = true,
185+
errors = List(
186+
"this is fake data from the mock SR actor",
187+
"[reading back DescribeRequest contents] workflow hashcode: Some(580529622)",
188+
"[reading back DescribeRequest contents] workflow url: None",
189+
"[reading back DescribeRequest contents] inputs: {\"a\":\"is for apple\"}",
190+
"[reading back DescribeRequest contents] type: Some(WDL)",
191+
"[reading back DescribeRequest contents] version: Some(1.0)",
192+
"[reading back DescribeRequest contents] dependencies: Some([0x0A, 0x0B, 0x0C])"
150193
),
151194
validWorkflow = true
152195
)

services/src/main/scala/cromwell/services/womtool/Describer.scala

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package cromwell.services.womtool
22

33
import cats.data.Validated.{Invalid, Valid}
4-
import cromwell.core.WorkflowSourceFilesCollection
5-
import cromwell.languages.util.ImportResolver.{HttpResolver, ImportAuthProvider, ImportResolver}
4+
import cats.syntax.traverse._
5+
import common.validation.ErrorOr.ErrorOr
6+
import cromwell.core.{WorkflowId, WorkflowSourceFilesCollection}
7+
import cromwell.languages.util.ImportResolver.{HttpResolver, ImportAuthProvider}
68
import cromwell.languages.util.{ImportResolver, LanguageFactoryUtil}
79
import cromwell.languages.{LanguageFactory, ValidatedWomNamespace}
810
import cromwell.services.womtool.WomtoolServiceMessages.{DescribeFailure, DescribeResult, DescribeSuccess}
@@ -14,9 +16,23 @@ import wom.expression.NoIoFunctionSet
1416
object Describer {
1517

1618
def describeWorkflow(wsfc: WorkflowSourceFilesCollection, authProviders: List[ImportAuthProvider]): DescribeResult = {
19+
val zipResolverErrorOr: ErrorOr[Option[ImportResolver.ImportResolver]] =
20+
wsfc.importsZipFileOption.map(ImportResolver.zippedImportResolver(_, WorkflowId.randomId())).sequence
1721

18-
val initialResolvers: List[ImportResolver] = List(HttpResolver(None, Map.empty, authProviders))
22+
val initialResolversErrorOr: ErrorOr[List[ImportResolver.ImportResolver]] =
23+
zipResolverErrorOr map { zipResolverOption =>
24+
zipResolverOption.toList ++ List(HttpResolver(None, Map.empty, authProviders))
25+
}
1926

27+
initialResolversErrorOr match {
28+
case Valid(initialResolvers) => describeWorkflowWithResolvers(wsfc, initialResolvers)
29+
case Invalid(errors) => DescribeFailure(errors.toList.mkString(", "))
30+
}
31+
}
32+
33+
private def describeWorkflowWithResolvers(wsfc: WorkflowSourceFilesCollection,
34+
initialResolvers: List[ImportResolver.ImportResolver]
35+
): DescribeResult =
2036
// The HTTP resolver is used to pull down workflows submitted by URL
2137
LanguageFactoryUtil.findWorkflowSource(wsfc.workflowSource, wsfc.workflowUrl, initialResolvers) match {
2238
case Right((workflowSource: WorkflowSource, importResolvers: List[ImportResolver.ImportResolver])) =>
@@ -40,7 +56,6 @@ object Describer {
4056
reason = errors.toList.mkString(", ")
4157
)
4258
}
43-
}
4459

4560
// By this point there are no "out of band" errors that can occur (i.e. those that would indicate a BadRequest, versus just showing up in the `errors` list)
4661
private def describeWorkflowInner(factory: LanguageFactory,
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"valid" : true,
3+
"errors" : [
4+
],
5+
"validWorkflow" : true,
6+
"name" : "relative_imports",
7+
"inputs" : [
8+
],
9+
"outputs" : [
10+
{
11+
"name" : "result",
12+
"valueType" : {
13+
"typeName" : "Int"
14+
},
15+
"typeDisplayName" : "Int"
16+
}
17+
],
18+
"images" : [
19+
],
20+
"submittedDescriptorType" : {
21+
"descriptorType" : "WDL",
22+
"descriptorTypeVersion" : "Cascades"
23+
},
24+
"importedDescriptorTypes" : [
25+
],
26+
"meta" : {
27+
28+
},
29+
"parameterMeta" : {
30+
31+
},
32+
"isRunnableWorkflow" : true
33+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
version development
2+
3+
import "sub_wfs/foo.wdl"
4+
5+
workflow relative_imports {
6+
call foo.foo_wf
7+
8+
output {
9+
Int result = foo_wf.unpacked
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
version development
2+
3+
struct MyStruct {
4+
Int a
5+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
version development
2+
3+
import "../structs/my_struct.wdl"
4+
import "tasks/add5.wdl" as a5
5+
6+
workflow foo_wf {
7+
call a5.add5 { input: x = object { a: 100 } }
8+
output {
9+
Int unpacked = add5.five_added.a
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
version development
2+
3+
import "../../structs/my_struct.wdl"
4+
5+
task add5 {
6+
input {
7+
MyStruct x
8+
}
9+
command <<<
10+
echo $((5 + ~{x.a}))
11+
>>>
12+
output {
13+
MyStruct five_added = object { a: read_int(stdout()) }
14+
}
15+
runtime {
16+
docker: "ubuntu:latest"
17+
}
18+
}

services/src/test/scala/cromwell/services/womtool/DescriberSpec.scala

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package cromwell.services.womtool
22

33
import common.assertion.CromwellTimeoutSpec
44
import cromwell.core.path._
5-
import cromwell.core.{WorkflowOptions, WorkflowSourceFilesCollection, WorkflowSourceFilesWithoutImports}
5+
import cromwell.core.{WorkflowOptions, WorkflowSourceFilesCollection}
66
import cromwell.languages.config.{CromwellLanguages, LanguageConfiguration}
77
import cromwell.services.womtool.DescriberSpec._
88
import cromwell.services.womtool.WomtoolServiceMessages.DescribeSuccess
@@ -37,26 +37,37 @@ class DescriberSpec extends AnyFlatSpec with CromwellTimeoutSpec with Matchers {
3737
val workflowType = Try(caseDirectory.resolve("workflowType").contentAsString.stripLineEnd).toOption
3838
val workflowTypeVersion =
3939
Try(caseDirectory.resolve("workflowTypeVersion").contentAsString.stripLineEnd).toOption
40+
val importsFile =
41+
Try(caseDirectory.resolve("workflowDependencies")).filter(_.exists).map(_.zip()).toOption
4042

41-
val interimWsfc = WorkflowSourceFilesWithoutImports(
42-
workflowSource = None,
43-
workflowUrl = None,
43+
val workflowSource = testCase match {
44+
case FileAndDescription(file, _) => Option(file)
45+
case _ => None
46+
}
47+
48+
val workflowUrl = testCase match {
49+
case UrlAndDescription(url, _) => Option(url)
50+
case _ => None
51+
}
52+
53+
val wsfc = WorkflowSourceFilesCollection(
54+
workflowSource = workflowSource,
55+
workflowUrl = workflowUrl,
4456
workflowRoot = None,
4557
workflowType = workflowType,
4658
workflowTypeVersion = workflowTypeVersion,
4759
inputsJson = "",
4860
workflowOptions = WorkflowOptions.empty,
61+
importsFile = importsFile.map(_.byteArray),
62+
workflowOnHold = false,
4963
labelsJson = "",
5064
warnings = Seq.empty,
5165
requestedWorkflowId = None
5266
)
5367

54-
val wsfc = testCase match {
55-
case FileAndDescription(file, _) => interimWsfc.copy(workflowSource = Option(file))
56-
case UrlAndDescription(url, _) => interimWsfc.copy(workflowUrl = Option(url))
57-
}
58-
5968
check(wsfc, parse(testCase.expectedDescription).toOption.get)
69+
70+
importsFile.map(_.delete(swallowIOExceptions = true))
6071
}
6172
}
6273
}

0 commit comments

Comments
 (0)