Skip to content

Commit c785af1

Browse files
authored
AN-736 Add support for container attribute in WDL 1.1 (#7809)
1 parent 2beb60b commit c785af1

File tree

43 files changed

+871
-376
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+871
-376
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,14 @@ This will allow for better grouping of jobs in the Batch UI and ensure determini
3030
* Added support for specifying an IAM role for AWS Batch job containers via the `aws_batch_job_role_arn` workflow option. This allows containers to access AWS resources based on the permissions granted to the specified role.
3131
* ECR [pull-through caches](https://docs.aws.amazon.com/AmazonECR/latest/userguide/pull-through-cache.html) can now be used to access Docker images. See [ReadTheDocs](https://cromwell.readthedocs.io/en/develop/backends/AWSBatch/) for details.
3232

33+
### Progress toward WDL 1.1 Support
34+
* WDL 1.1 support is in progress. Users that would like to try out the current partial support can do so by using WDL version `development-1.1`. In Cromwell 91, `development-1.1` has been enhanced to include:
35+
* Runtime attribute `container`, which may be a single string or an array of strings, is preferred over `docker` for specifying the image a task should run on. If given a list of multiple images, Cromwell will choose the first.
36+
* `docker://` is permitted as a prefix for image names, ex. `container: docker://ubuntu:latest`.
37+
3338
### Other changes
3439
* Removed unused code related to Azure cloud services.
40+
* Changed log level from WARN to INFO for messages about unsupported runtime attributes.
3541

3642
## 90 Release Notes
3743

backend/src/main/scala/cromwell/backend/standard/StandardCachingActorHelper.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import akka.actor.{Actor, ActorRef}
44
import cromwell.backend._
55
import cromwell.backend.io.{JobPaths, WorkflowPaths}
66
import cromwell.backend.standard.callcaching.JobCachingActorHelper
7-
import cromwell.backend.validation.{DockerValidation, RuntimeAttributesValidation, ValidatedRuntimeAttributes}
7+
import cromwell.backend.validation.{Containers, RuntimeAttributesValidation, ValidatedRuntimeAttributes}
88
import cromwell.core.logging.JobLogging
99
import cromwell.core.path.Path
1010
import cromwell.services.metadata.CallMetadataKeys
@@ -57,7 +57,7 @@ trait StandardCachingActorHelper extends JobCachingActorHelper {
5757
}
5858

5959
lazy val isDockerRun: Boolean =
60-
RuntimeAttributesValidation.extractOption(DockerValidation.instance, validatedRuntimeAttributes).isDefined
60+
Containers.extractContainerOption(validatedRuntimeAttributes).isDefined
6161

6262
/**
6363
* Returns the paths to the job.

backend/src/main/scala/cromwell/backend/standard/StandardInitializationActor.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ class StandardInitializationActor(val standardParams: StandardInitializationActo
9595

9696
if (notSupportedAttributes.nonEmpty) {
9797
val notSupportedAttrString = notSupportedAttributes mkString ", "
98-
workflowLogger.warn(
98+
workflowLogger.info(
9999
s"Key/s [$notSupportedAttrString] is/are not supported by backend. " +
100100
s"Unsupported attributes will not be part of job executions."
101101
)

backend/src/main/scala/cromwell/backend/standard/pollmonitoring/PollResultMonitorActor.scala

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
package cromwell.backend.standard.pollmonitoring
22

33
import akka.actor.{Actor, ActorRef}
4+
import cromwell.backend.validation._
45
import cromwell.backend.{BackendJobDescriptor, BackendWorkflowDescriptor, Platform}
5-
import cromwell.backend.validation.{
6-
CpuValidation,
7-
DockerValidation,
8-
MemoryValidation,
9-
RuntimeAttributesValidation,
10-
ValidatedRuntimeAttributes
11-
}
126
import cromwell.core.logging.JobLogger
137
import cromwell.services.cost.InstantiatedVmInfo
148
import cromwell.services.metadata.CallMetadataKeys
@@ -70,8 +64,7 @@ trait PollResultMonitorActor[PollResultType] extends Actor {
7064
val workflowDescriptor = params.workflowDescriptor
7165
val jobDescriptor = params.jobDescriptor
7266
val platform = params.platform.map(_.runtimeKey)
73-
val dockerImage =
74-
RuntimeAttributesValidation.extractOption(DockerValidation.instance, validatedRuntimeAttributes)
67+
val dockerImage = Containers.extractContainerOption(validatedRuntimeAttributes)
7568
val cpus = RuntimeAttributesValidation.extract(CpuValidation.instance, validatedRuntimeAttributes).value
7669
val memory = RuntimeAttributesValidation
7770
.extract(MemoryValidation.instance(), validatedRuntimeAttributes)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package cromwell.backend.validation
2+
3+
import cats.implicits.catsSyntaxValidatedId
4+
import common.validation.ErrorOr.ErrorOr
5+
import wdl.draft2.model.LocallyQualifiedName
6+
import wom.RuntimeAttributesKeys
7+
import wom.types.{WomStringType, WomType}
8+
import wom.values.{WomArray, WomString, WomValue}
9+
10+
// Wrapper type for the list of containers that can be provided as 'docker' or 'container' runtime attributes.
11+
// In WDL, this value can be either a single string or an array of strings, but in the backend we always
12+
// want to deal with it as a list of strings.
13+
//
14+
// Previous to WDL 1.1, only 'docker' was supported. From WDL 1.1 onwards, they are aliases of each other, with
15+
// `container` being preferred and `docker` deprecated. Only one of they two may be provided in runtime attrs.
16+
// Note that we strip `container` out of pre-1.1 WDL files during parsing, so at this stage we only see `docker`
17+
// in those cases.
18+
case class Containers(values: List[String]) {
19+
override def toString: String = values.mkString(", ")
20+
}
21+
22+
object Containers {
23+
val validWdlTypes: Set[wom.types.WomType] =
24+
Set(wom.types.WomStringType, wom.types.WomArrayType(wom.types.WomStringType))
25+
26+
val runtimeAttrKeys = List(RuntimeAttributesKeys.ContainerKey, RuntimeAttributesKeys.DockerKey)
27+
28+
def apply(value: String): Containers = Containers(List(value))
29+
30+
def extractContainer(validatedRuntimeAttributes: ValidatedRuntimeAttributes): String =
31+
extractContainerOption(validatedRuntimeAttributes).getOrElse {
32+
throw new RuntimeException("No container image found in either 'container' or 'docker' runtime attributes.")
33+
}
34+
35+
def extractContainerOption(validatedRuntimeAttributes: ValidatedRuntimeAttributes): Option[String] = {
36+
val dockerContainer = RuntimeAttributesValidation
37+
.extractOption[Containers](RuntimeAttributesKeys.DockerKey, validatedRuntimeAttributes)
38+
.flatMap(_.values.headOption)
39+
val containerContainer = RuntimeAttributesValidation
40+
.extractOption[Containers](RuntimeAttributesKeys.ContainerKey, validatedRuntimeAttributes)
41+
.flatMap(_.values.headOption)
42+
43+
containerContainer.orElse(dockerContainer)
44+
}
45+
46+
def extractContainerFromPreValidationAttrs(attributes: Map[LocallyQualifiedName, WomValue]): Option[String] = {
47+
val containerContainer = attributes.get(RuntimeAttributesKeys.ContainerKey) match {
48+
case Some(WomArray(_, values)) =>
49+
values.headOption.map(_.valueString)
50+
case Some(WomString(value)) => Some(value)
51+
case _ => None
52+
}
53+
54+
val dockerContainer = attributes.get(RuntimeAttributesKeys.DockerKey) match {
55+
case Some(WomArray(_, values)) =>
56+
values.headOption.map(_.valueString)
57+
case Some(WomString(value)) => Some(value)
58+
case _ => None
59+
}
60+
61+
// TODO enhance to select the best container from the list if multiple are provided.
62+
// Currently we always choose the first, should prefer one that matches our platform.
63+
// https://broadworkbench.atlassian.net/browse/AN-734
64+
65+
containerContainer.orElse(dockerContainer)
66+
}
67+
}
68+
69+
/**
70+
* Trait to handle validation of both 'docker' and 'container' runtime attributes, which are mutually exclusive
71+
* ways of specifying the container image to use for a task.
72+
*/
73+
trait ContainersValidation extends OptionalRuntimeAttributesValidation[Containers] {
74+
override def coercion: Set[WomType] = Containers.validWdlTypes
75+
76+
override def usedInCallCaching: Boolean = true
77+
78+
override protected def missingValueMessage: String = s"Can't find an attribute value for key ${key}"
79+
80+
override protected def invalidValueMessage(value: WomValue): String = super.missingValueMessage
81+
82+
override protected def validateOption: PartialFunction[WomValue, ErrorOr[Containers]] = {
83+
case WomString(value) => value.validNel.map(v => Containers(v))
84+
case WomArray(womType, values) if womType.memberType == WomStringType =>
85+
Containers(values.map(_.valueString).toList).validNel
86+
}
87+
}
Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,23 @@
11
package cromwell.backend.validation
22

3-
import cats.syntax.validated._
4-
import common.validation.ErrorOr.ErrorOr
53
import wom.RuntimeAttributesKeys
6-
import wom.values._
74

85
/**
9-
* Validates the "docker" runtime attribute as a String, returning it as `String`.
10-
*
11-
* `instance` returns an validation that errors when no attribute is specified.
12-
*
13-
* There is no default, however `optional` can be used return the validated value as an `Option`, wrapped in a `Some`,
14-
* if present, or `None` if not found.
6+
* Different WDL versions support different names for the runtime attribute that specifies the container image to use.
7+
* WDL 1.0 supports only `docker`, WDL 1.1 and later support `container` (preferred) and `docker` (deprecated).
158
*/
169
object DockerValidation {
17-
lazy val instance: RuntimeAttributesValidation[String] = new DockerValidation
18-
lazy val optional: OptionalRuntimeAttributesValidation[String] = instance.optional
10+
lazy val instance: OptionalRuntimeAttributesValidation[Containers] = new DockerValidation
1911
}
2012

21-
class DockerValidation extends StringRuntimeAttributesValidation(RuntimeAttributesKeys.DockerKey) {
22-
override def usedInCallCaching: Boolean = true
23-
24-
override protected def missingValueMessage: String = "Can't find an attribute value for key docker"
13+
class DockerValidation extends ContainersValidation {
14+
override val key: String = RuntimeAttributesKeys.DockerKey
15+
}
2516

26-
override protected def invalidValueMessage(value: WomValue): String = super.missingValueMessage
17+
object ContainerValidation {
18+
lazy val instance: OptionalRuntimeAttributesValidation[Containers] = new ContainerValidation
19+
}
2720

28-
// NOTE: Docker's current test specs don't like WdlInteger, etc. auto converted to WdlString.
29-
override protected def validateValue: PartialFunction[WomValue, ErrorOr[String]] = { case WomString(value) =>
30-
value.validNel
31-
}
21+
class ContainerValidation extends ContainersValidation {
22+
override val key: String = RuntimeAttributesKeys.ContainerKey
3223
}

backend/src/main/scala/cromwell/backend/validation/RuntimeAttributesValidation.scala

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,15 @@ object RuntimeAttributesValidation {
2626
if (unrecognized.nonEmpty) logger.warn(s"Unrecognized runtime attribute keys: $unrecognized")
2727
}
2828

29-
def validateDocker(docker: Option[WomValue], onMissingKey: => ErrorOr[Option[String]]): ErrorOr[Option[String]] =
30-
validateWithValidation(docker, DockerValidation.instance.optional, onMissingKey)
29+
def validateDocker(docker: Option[WomValue],
30+
onMissingKey: => ErrorOr[Option[Containers]]
31+
): ErrorOr[Option[Containers]] =
32+
validateWithValidation(docker, DockerValidation.instance, onMissingKey)
33+
34+
def validateContainer(container: Option[WomValue],
35+
onMissingKey: => ErrorOr[Option[Containers]]
36+
): ErrorOr[Option[Containers]] =
37+
validateWithValidation(container, ContainerValidation.instance, onMissingKey)
3138

3239
def validateFailOnStderr(value: Option[WomValue], onMissingKey: => ErrorOr[Boolean]): ErrorOr[Boolean] =
3340
validateWithValidation(value, FailOnStderrValidation.instance, onMissingKey)

backend/src/test/scala/cromwell/backend/standard/StandardValidatedRuntimeAttributesBuilderSpec.scala

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,16 @@ class StandardValidatedRuntimeAttributesBuilderSpec
5353

5454
"validate a valid Docker entry" in {
5555
val runtimeAttributes = Map("docker" -> WomString("ubuntu:latest"))
56-
val expectedRuntimeAttributes = defaultRuntimeAttributes + (DockerKey -> Option("ubuntu:latest"))
56+
val expectedRuntimeAttributes = defaultRuntimeAttributes + (DockerKey -> Option(Containers("ubuntu:latest")))
5757
assertRuntimeAttributesSuccessfulCreation(runtimeAttributes, expectedRuntimeAttributes)
5858
}
5959

6060
"fail to validate an invalid Docker entry" in {
6161
val runtimeAttributes = Map("docker" -> WomInteger(1))
62-
assertRuntimeAttributesFailedCreation(runtimeAttributes, "Expecting docker runtime attribute to be a String")
62+
assertRuntimeAttributesFailedCreation(
63+
runtimeAttributes,
64+
"Expecting docker runtime attribute to be a type in Set(WomStringType, WomMaybeEmptyArrayType(WomStringType))"
65+
)
6366
}
6467

6568
"validate a valid failOnStderr entry" in {
@@ -170,7 +173,7 @@ class StandardValidatedRuntimeAttributesBuilderSpec
170173
val builder = if (includeDockerSupport) {
171174
StandardValidatedRuntimeAttributesBuilder
172175
.default(mockBackendRuntimeConfig)
173-
.withValidation(DockerValidation.optional)
176+
.withValidation(DockerValidation.instance)
174177
} else {
175178
StandardValidatedRuntimeAttributesBuilder.default(mockBackendRuntimeConfig)
176179
}
@@ -185,7 +188,7 @@ class StandardValidatedRuntimeAttributesBuilderSpec
185188
val continueOnReturnCode =
186189
RuntimeAttributesValidation.extract(ContinueOnReturnCodeValidation.instance, validatedRuntimeAttributes)
187190

188-
docker should be(expectedRuntimeAttributes(DockerKey).asInstanceOf[Option[String]])
191+
docker should be(expectedRuntimeAttributes(DockerKey).asInstanceOf[Option[Containers]])
189192
failOnStderr should be(expectedRuntimeAttributes(FailOnStderrKey).asInstanceOf[Boolean])
190193
continueOnReturnCode should be(
191194
expectedRuntimeAttributes(ContinueOnReturnCodeKey)
@@ -203,7 +206,7 @@ class StandardValidatedRuntimeAttributesBuilderSpec
203206
val builder = if (supportsDocker) {
204207
StandardValidatedRuntimeAttributesBuilder
205208
.default(mockBackendRuntimeConfig)
206-
.withValidation(DockerValidation.optional)
209+
.withValidation(DockerValidation.instance)
207210
} else {
208211
StandardValidatedRuntimeAttributesBuilder.default(mockBackendRuntimeConfig)
209212
}

backend/src/test/scala/cromwell/backend/validation/RuntimeAttributesValidationSpec.scala

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class RuntimeAttributesValidationSpec
2828
"Failed to get Docker mandatory key from runtime attributes".invalidNel
2929
)
3030
result match {
31-
case Valid(x) => assert(x.get == "someImage")
31+
case Valid(x) => assert(x.get.values.head == "someImage")
3232
case Invalid(e) => fail(e.toList.mkString(" "))
3333
}
3434
}
@@ -62,7 +62,35 @@ class RuntimeAttributesValidationSpec
6262
)
6363
result match {
6464
case Valid(_) => fail("A failure was expected.")
65-
case Invalid(e) => assert(e.head == "Expecting docker runtime attribute to be a String")
65+
case Invalid(e) =>
66+
assert(
67+
e.head == "Expecting docker runtime attribute to be a type in Set(WomStringType, WomMaybeEmptyArrayType(WomStringType))"
68+
)
69+
}
70+
}
71+
72+
"return success when tries to validate a valid container entry" in {
73+
val imageValue = Some(WomString("someImage"))
74+
val result = RuntimeAttributesValidation.validateContainer(
75+
imageValue,
76+
"Failed to get container key from runtime attributes".invalidNel
77+
)
78+
result match {
79+
case Valid(x) => assert(x.get.values == List("someImage"))
80+
case Invalid(e) => fail(e.toList.mkString(" "))
81+
}
82+
}
83+
84+
"return success when tries to validate a valid container entry containing a list" in {
85+
val imageValue =
86+
Some(WomArray(WomArrayType(WomStringType), Seq(WomString("someImage"), WomString("someOtherImage"))))
87+
val result = RuntimeAttributesValidation.validateContainer(
88+
imageValue,
89+
"Failed to get container key from runtime attributes".invalidNel
90+
)
91+
result match {
92+
case Valid(x) => assert(x.get.values == Seq("someImage", "someOtherImage"))
93+
case Invalid(e) => fail(e.toList.mkString(" "))
6694
}
6795
}
6896

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name: container_attr_wdl10
2+
testFormat: workflowsuccess
3+
4+
files {
5+
workflow: wdl/container_attr_wdl10.wdl
6+
}
7+
8+
metadata {
9+
"calls.container_attr_wdl10.dockerOnly.dockerImageUsed": "ubuntu@sha256:d0afa9fbcf16134b776fbba4a04c31d476eece2d080c66c887fdd2608e4219a9",
10+
"calls.container_attr_wdl10.dockerAndContainer.dockerImageUsed": "ubuntu@sha256:d0afa9fbcf16134b776fbba4a04c31d476eece2d080c66c887fdd2608e4219a9"
11+
}

0 commit comments

Comments
 (0)