From 7c5aca282bfaab485ab60f90707f88723a0be9e2 Mon Sep 17 00:00:00 2001 From: Jorge Aguilera Date: Tue, 27 Feb 2024 18:05:14 +0100 Subject: [PATCH 1/3] new volume configuration to mount in the task Signed-off-by: Jorge Aguilera --- .../main/nextflow/nomad/NomadConfig.groovy | 32 ++++++++++ .../nomad/executor/NomadService.groovy | 19 ++++++ .../nextflow/nomad/NomadConfigSpec.groovy | 40 +++++++++++++ .../nomad/executor/NomadServiceSpec.groovy | 58 +++++++++++++++++++ 4 files changed, 149 insertions(+) diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/NomadConfig.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/NomadConfig.groovy index 7f181f0..6390220 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/NomadConfig.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/NomadConfig.groovy @@ -30,6 +30,14 @@ import groovy.util.logging.Slf4j class NomadConfig { final static protected API_VERSION = "v1" + final static public String VOLUME_DOCKER_TYPE = "docker" + final static public String VOLUME_CSI_TYPE = "csi" + final static public String VOLUME_HOST_TYPE = "host" + + final static protected String[] VOLUME_TYPES = [ + VOLUME_CSI_TYPE, VOLUME_DOCKER_TYPE, VOLUME_HOST_TYPE + ] + final NomadClientOpts clientOpts final NomadJobOpts jobOpts @@ -57,6 +65,7 @@ class NomadConfig { final String region final String namespace final String dockerVolume + final VolumeSpec volumeSpec NomadJobOpts(Map nomadJobOpts){ deleteOnCompletion = nomadJobOpts.containsKey("deleteOnCompletion") ? @@ -71,6 +80,29 @@ class NomadConfig { region = nomadJobOpts.region ?: null namespace = nomadJobOpts.namespace ?: null dockerVolume = nomadJobOpts.dockerVolume ?: null + if( dockerVolume ){ + log.info "dockerVolume config will be deprecated, use volume type:'docker' name:'name' instead" + } + if( nomadJobOpts.volume && nomadJobOpts.volume instanceof Map){ + volumeSpec = new VolumeSpec(nomadJobOpts.volume as Map) + }else{ + volumeSpec = null + } + } + } + + class VolumeSpec{ + + final String type + final String name + + VolumeSpec(Map volumeConfig){ + if( !VOLUME_TYPES.contains(volumeConfig.type) ) + throw new IllegalArgumentException("Volume type $type is not supported") + if( !volumeConfig.name ) + throw new IllegalArgumentException("Volume name is required") + this.type = volumeConfig.type + this.name = volumeConfig.name } } } diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/executor/NomadService.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/executor/NomadService.groovy index e210645..7c1cdd5 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/executor/NomadService.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/executor/NomadService.groovy @@ -1,4 +1,5 @@ /* + * Copyright 2024, Evaluacion y Desarrollo de Negocios, Spain * Copyright 2023, Stellenbosch University, South Africa * Copyright 2022, Center for Medical Genetics, Ghent * @@ -28,6 +29,8 @@ import io.nomadproject.client.models.JobSummary import io.nomadproject.client.models.Task import io.nomadproject.client.models.TaskGroup import io.nomadproject.client.models.TaskGroupSummary +import io.nomadproject.client.models.VolumeMount +import io.nomadproject.client.models.VolumeRequest import nextflow.nomad.NomadConfig /** @@ -80,6 +83,15 @@ class NomadService implements Closeable{ name: "group", tasks: [ task ] ) + if( config.jobOpts.volumeSpec){ + taskGroup.volumes = [:] + taskGroup.volumes[config.jobOpts.volumeSpec.name]= new VolumeRequest( + type: config.jobOpts.volumeSpec.type, + source: config.jobOpts.volumeSpec.name, + attachmentMode: "file-system", + accessMode: "multi-node-multi-writer" + ) + } return taskGroup } @@ -105,6 +117,13 @@ class NomadService implements Closeable{ readonly : false ] } + if( config.jobOpts.volumeSpec){ + String destinationDir = workingDir.split(File.separator).dropRight(2).join(File.separator) + task.volumeMounts = [ new VolumeMount( + destination: destinationDir, + volume: config.jobOpts.volumeSpec.name + )] + } task } diff --git a/plugins/nf-nomad/src/test/nextflow/nomad/NomadConfigSpec.groovy b/plugins/nf-nomad/src/test/nextflow/nomad/NomadConfigSpec.groovy index e85f117..e76eeaa 100644 --- a/plugins/nf-nomad/src/test/nextflow/nomad/NomadConfigSpec.groovy +++ b/plugins/nf-nomad/src/test/nextflow/nomad/NomadConfigSpec.groovy @@ -119,4 +119,44 @@ class NomadConfigSpec extends Specification { expect: config.jobOpts.namespace == "namespace" } + + void "should instantiate a volume spec if specified"() { + when: + def config = new NomadConfig([ + jobs: [volume:[type:"docker", name:"test"]] + ]) + + then: + config.jobOpts.volumeSpec + config.jobOpts.volumeSpec.type == NomadConfig.VOLUME_DOCKER_TYPE + config.jobOpts.volumeSpec.name == "test" + + when: + def config2 = new NomadConfig([ + jobs: [volume:[type:"csi", name:"test"]] + ]) + + then: + config2.jobOpts.volumeSpec + config2.jobOpts.volumeSpec.type == NomadConfig.VOLUME_CSI_TYPE + config2.jobOpts.volumeSpec.name == "test" + + when: + def config3 = new NomadConfig([ + jobs: [volume:[type:"host", name:"test"]] + ]) + + then: + config3.jobOpts.volumeSpec + config3.jobOpts.volumeSpec.type == NomadConfig.VOLUME_HOST_TYPE + config3.jobOpts.volumeSpec.name == "test" + + when: + new NomadConfig([ + jobs: [volume:[type:"not-supported", name:"test"]] + ]) + + then: + thrown(IllegalArgumentException) + } } diff --git a/plugins/nf-nomad/src/test/nextflow/nomad/executor/NomadServiceSpec.groovy b/plugins/nf-nomad/src/test/nextflow/nomad/executor/NomadServiceSpec.groovy index 9fdbc73..0ed2185 100644 --- a/plugins/nf-nomad/src/test/nextflow/nomad/executor/NomadServiceSpec.groovy +++ b/plugins/nf-nomad/src/test/nextflow/nomad/executor/NomadServiceSpec.groovy @@ -187,4 +187,62 @@ class NomadServiceSpec extends Specification{ state == "Starting" } + + void "submit a task with a volume"(){ + given: + def config = new NomadConfig( + client:[ + address : "http://${mockWebServer.hostName}:${mockWebServer.port}" + ], + jobs:[ + volume: [ name:'test', type:'csi'] + ] + ) + def service = new NomadService(config) + + String id = "theId" + String name = "theName" + String image = "theImage" + List args = ["theCommand", "theArgs"] + String workingDir = "a/b/c" + Mapenv = [test:"test"] + + mockWebServer.enqueue(new MockResponse() + .setBody(JsonOutput.toJson(["EvalID":"test"]).toString()) + .addHeader("Content-Type", "application/json")); + when: + + def idJob = service.submitTask(id, name, image, args, workingDir,env) + def recordedRequest = mockWebServer.takeRequest(); + def body = new JsonSlurper().parseText(recordedRequest.body.readUtf8()) + + then: + idJob + + and: + recordedRequest.method == "POST" + recordedRequest.path == "/v1/jobs" + + and: + body.Job + body.Job.ID == id + body.Job.Name == name + body.Job.Datacenters == [] + body.Job.Type == "batch" + body.Job.TaskGroups.size() == 1 + body.Job.TaskGroups[0].Name == "group" + body.Job.TaskGroups[0].Tasks.size() == 1 + body.Job.TaskGroups[0].Tasks[0].Name == "nf-task" + body.Job.TaskGroups[0].Tasks[0].Driver == "docker" + body.Job.TaskGroups[0].Tasks[0].Config.image == image + body.Job.TaskGroups[0].Tasks[0].Config.work_dir == workingDir + body.Job.TaskGroups[0].Tasks[0].Config.command == args[0] + body.Job.TaskGroups[0].Tasks[0].Config.args == args.drop(1) + + body.Job.TaskGroups[0].Volumes.size() == 1 + body.Job.TaskGroups[0].Volumes['test'] == [AccessMode:"multi-node-multi-writer", AttachmentMode:"file-system", Source:"test", Type:"csi"] + body.Job.TaskGroups[0].Tasks[0].VolumeMounts.size() == 1 + body.Job.TaskGroups[0].Tasks[0].VolumeMounts[0] == [Destination:"a", Volume:"test"] + + } } From cf6b465d70b671fc9bc5d35bb4cb75e9b8e7764e Mon Sep 17 00:00:00 2001 From: Jorge Aguilera Date: Thu, 29 Feb 2024 20:40:56 +0100 Subject: [PATCH 2/3] fix gradle tasks issues Signed-off-by: Jorge Aguilera --- .../nextflow/gradle/plugins/SourcesMatcher.groovy | 2 +- plugins/build.gradle | 15 +++++++++++++++ plugins/nf-nomad/build.gradle | 12 ++++++++++++ .../nf-nomad/src/resources/META-INF/MANIFEST.MF | 7 ------- .../src/resources/META-INF/extensions.idx | 2 -- 5 files changed, 28 insertions(+), 10 deletions(-) delete mode 100644 plugins/nf-nomad/src/resources/META-INF/MANIFEST.MF delete mode 100644 plugins/nf-nomad/src/resources/META-INF/extensions.idx diff --git a/buildSrc/src/main/groovy/nextflow/gradle/plugins/SourcesMatcher.groovy b/buildSrc/src/main/groovy/nextflow/gradle/plugins/SourcesMatcher.groovy index 7e9260f..1865b93 100644 --- a/buildSrc/src/main/groovy/nextflow/gradle/plugins/SourcesMatcher.groovy +++ b/buildSrc/src/main/groovy/nextflow/gradle/plugins/SourcesMatcher.groovy @@ -41,7 +41,7 @@ class SourcesMatcher { } matcher.collect { file -> def source = file.toString() - "$root.absolutePath/src/main/" - return source.split('\\.').dropRight(1).join().split(File.separator).drop(1).join('.') + return source.split('\\.').dropRight(1).join().split(File.separator).join('.') } } diff --git a/plugins/build.gradle b/plugins/build.gradle index 57c5bcc..6b08277 100644 --- a/plugins/build.gradle +++ b/plugins/build.gradle @@ -32,6 +32,21 @@ subprojects { mavenCentral() } + java { + toolchain { + languageVersion = JavaLanguageVersion.of(19) + } + } + + compileJava { + options.release.set(11) + } + + tasks.withType(GroovyCompile) { + sourceCompatibility = '11' + targetCompatibility = '11' + } + tasks.withType(Jar) { duplicatesStrategy = DuplicatesStrategy.INCLUDE } diff --git a/plugins/nf-nomad/build.gradle b/plugins/nf-nomad/build.gradle index 4a34108..2e04bb1 100644 --- a/plugins/nf-nomad/build.gradle +++ b/plugins/nf-nomad/build.gradle @@ -98,3 +98,15 @@ test { jvmArgs '--add-opens=java.base/java.lang=ALL-UNNAMED' } +jar { + manifest { + attributes( + 'Manifest-Version':'1.0', + 'Plugin-Id': project.name, + 'Plugin-Version': archiveVersion, + 'Plugin-Class': "nextflow.nomad.NomadPlugin", + 'Plugin-Provider': 'nextflow', + 'Plugin-Requires': '>=23.10.0', + ) + } +} diff --git a/plugins/nf-nomad/src/resources/META-INF/MANIFEST.MF b/plugins/nf-nomad/src/resources/META-INF/MANIFEST.MF deleted file mode 100644 index ceec83c..0000000 --- a/plugins/nf-nomad/src/resources/META-INF/MANIFEST.MF +++ /dev/null @@ -1,7 +0,0 @@ -Manifest-Version: 1.0 -Plugin-Id: nf-nomad -Plugin-Version: 0.0.1 -Plugin-Class: nextflow.nomad.NomadPlugin -Plugin-Provider: nextflow -Plugin-Requires: >=23.10.0 - diff --git a/plugins/nf-nomad/src/resources/META-INF/extensions.idx b/plugins/nf-nomad/src/resources/META-INF/extensions.idx deleted file mode 100644 index 1d1a1f1..0000000 --- a/plugins/nf-nomad/src/resources/META-INF/extensions.idx +++ /dev/null @@ -1,2 +0,0 @@ -nextflow.nomad.NomadPlugin -nextflow.nomad.executor.NomadExecutor From 374996e4eec0ed6060610031a4c93c6f0d49ab59 Mon Sep 17 00:00:00 2001 From: Jorge Aguilera Date: Fri, 1 Mar 2024 16:40:12 +0100 Subject: [PATCH 3/3] configure volume using a closure instead a map Signed-off-by: Jorge Aguilera --- .../main/nextflow/nomad/NomadConfig.groovy | 41 +++++++++++++++---- .../nextflow/nomad/NomadConfigSpec.groovy | 8 ++-- .../nomad/executor/NomadServiceSpec.groovy | 2 +- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/NomadConfig.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/NomadConfig.groovy index 6390220..624efc2 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/NomadConfig.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/NomadConfig.groovy @@ -83,8 +83,13 @@ class NomadConfig { if( dockerVolume ){ log.info "dockerVolume config will be deprecated, use volume type:'docker' name:'name' instead" } - if( nomadJobOpts.volume && nomadJobOpts.volume instanceof Map){ - volumeSpec = new VolumeSpec(nomadJobOpts.volume as Map) + if( nomadJobOpts.volume && nomadJobOpts.volume instanceof Closure){ + this.volumeSpec = new VolumeSpec() + def closure = (nomadJobOpts.volume as Closure) + def clone = closure.rehydrate(this.volumeSpec, closure.owner, closure.thisObject) + clone.resolveStrategy = Closure.DELEGATE_FIRST + clone() + this.volumeSpec.validate() }else{ volumeSpec = null } @@ -93,16 +98,34 @@ class NomadConfig { class VolumeSpec{ - final String type - final String name + private String type + private String name - VolumeSpec(Map volumeConfig){ - if( !VOLUME_TYPES.contains(volumeConfig.type) ) + String getType() { + return type + } + + String getName() { + return name + } + + VolumeSpec type(String type){ + this.type = type + this + } + + VolumeSpec name(String name){ + this.name = name + this + } + + protected validate(){ + if( !VOLUME_TYPES.contains(type) ) { throw new IllegalArgumentException("Volume type $type is not supported") - if( !volumeConfig.name ) + } + if( !this.name ){ throw new IllegalArgumentException("Volume name is required") - this.type = volumeConfig.type - this.name = volumeConfig.name + } } } } diff --git a/plugins/nf-nomad/src/test/nextflow/nomad/NomadConfigSpec.groovy b/plugins/nf-nomad/src/test/nextflow/nomad/NomadConfigSpec.groovy index e76eeaa..51fa952 100644 --- a/plugins/nf-nomad/src/test/nextflow/nomad/NomadConfigSpec.groovy +++ b/plugins/nf-nomad/src/test/nextflow/nomad/NomadConfigSpec.groovy @@ -123,7 +123,7 @@ class NomadConfigSpec extends Specification { void "should instantiate a volume spec if specified"() { when: def config = new NomadConfig([ - jobs: [volume:[type:"docker", name:"test"]] + jobs: [volume : { type "docker" name "test" }] ]) then: @@ -133,7 +133,7 @@ class NomadConfigSpec extends Specification { when: def config2 = new NomadConfig([ - jobs: [volume:[type:"csi", name:"test"]] + jobs: [volume : { type "csi" name "test" }] ]) then: @@ -143,7 +143,7 @@ class NomadConfigSpec extends Specification { when: def config3 = new NomadConfig([ - jobs: [volume:[type:"host", name:"test"]] + jobs: [volume : { type "host" name "test" }] ]) then: @@ -153,7 +153,7 @@ class NomadConfigSpec extends Specification { when: new NomadConfig([ - jobs: [volume:[type:"not-supported", name:"test"]] + jobs: [volume : { type "not-supported" name "test" }] ]) then: diff --git a/plugins/nf-nomad/src/test/nextflow/nomad/executor/NomadServiceSpec.groovy b/plugins/nf-nomad/src/test/nextflow/nomad/executor/NomadServiceSpec.groovy index 0ed2185..3e7e69d 100644 --- a/plugins/nf-nomad/src/test/nextflow/nomad/executor/NomadServiceSpec.groovy +++ b/plugins/nf-nomad/src/test/nextflow/nomad/executor/NomadServiceSpec.groovy @@ -195,7 +195,7 @@ class NomadServiceSpec extends Specification{ address : "http://${mockWebServer.hostName}:${mockWebServer.port}" ], jobs:[ - volume: [ name:'test', type:'csi'] + volume: { type "csi" name "test" } ] ) def service = new NomadService(config)