From 0dfc8169c743a3376fca0d6df58cc863af513f91 Mon Sep 17 00:00:00 2001 From: Jorge Aguilera Date: Tue, 9 Jul 2024 09:37:00 +0200 Subject: [PATCH 1/8] implement ConstraintNodeSpec Signed-off-by: Jorge Aguilera --- .../nomad/config/ConstraintNodeSpec.groovy | 81 +++++++++++++ .../nomad/config/ConstraintsSpec.groovy | 28 +++++ .../nextflow/nomad/config/NomadJobOpts.groovy | 19 +++ .../nomad/executor/NomadService.groovy | 85 ++++++++++++- .../nomad/executor/TaskDirectives.groovy | 5 +- .../nomad/{ => config}/NomadConfigSpec.groovy | 2 +- .../nomad/config/NomadConstraintsSpec.groovy | 114 ++++++++++++++++++ .../nomad/executor/NomadServiceSpec.groovy | 66 ++++++++++ validation/constraints/main.nf | 18 +++ validation/constraints/node-nextflow.config | 41 +++++++ validation/run-all.sh | 4 +- validation/run-pipeline.sh | 2 + 12 files changed, 459 insertions(+), 6 deletions(-) create mode 100644 plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintNodeSpec.groovy create mode 100644 plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintsSpec.groovy rename plugins/nf-nomad/src/test/nextflow/nomad/{ => config}/NomadConfigSpec.groovy (99%) create mode 100644 plugins/nf-nomad/src/test/nextflow/nomad/config/NomadConstraintsSpec.groovy create mode 100644 validation/constraints/main.nf create mode 100644 validation/constraints/node-nextflow.config diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintNodeSpec.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintNodeSpec.groovy new file mode 100644 index 0000000..6260175 --- /dev/null +++ b/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintNodeSpec.groovy @@ -0,0 +1,81 @@ +package nextflow.nomad.config + +class ConstraintNodeSpec { + + private String id = null + private String name = null + private String clientClass = null + private String pool = null + private String dataCenter = null + private String region = null + + String getId() { + return id + } + + String getName() { + return name + } + + String getClientClass() { + return clientClass + } + + String getPool() { + return pool + } + + String getDataCenter() { + return dataCenter + } + + String getRegion() { + return region + } + + ConstraintNodeSpec setUnique(Map map){ + unique(map) + } + + ConstraintNodeSpec unique(Map map){ + this.id = map.containsKey("id") ? map["id"].toString() : null + this.name = map.containsKey("name") ? map["name"].toString() : null + this + } + + ConstraintNodeSpec setClientClass(Object map){ + clientClass(map) + } + + ConstraintNodeSpec clientClass(Object clientClass){ + this.clientClass = clientClass.toString() + this + } + + ConstraintNodeSpec setPool(Object map){ + pool(map) + } + + ConstraintNodeSpec pool(Object pool){ + this.pool = pool.toString() + this + } + + ConstraintNodeSpec setDataCenter(Object map){ + dataCenter(map) + } + + ConstraintNodeSpec dataCenter(Object dataCenter){ + this.dataCenter = dataCenter.toString() + this + } + + ConstraintNodeSpec setRegion(Object map){ + region(map) + } + + ConstraintNodeSpec region(Object region){ + this.region = region.toString() + this + } +} diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintsSpec.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintsSpec.groovy new file mode 100644 index 0000000..1d5aedd --- /dev/null +++ b/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintsSpec.groovy @@ -0,0 +1,28 @@ +package nextflow.nomad.config + +class ConstraintsSpec { + + List nodeSpecs = [] + + ConstraintsSpec node( @DelegatesTo(ConstraintNodeSpec)Closure closure){ + ConstraintNodeSpec constraintNodeSpec = new ConstraintNodeSpec() + def clone = closure.rehydrate(constraintNodeSpec, closure.owner, closure.thisObject) + clone.resolveStrategy = Closure.DELEGATE_FIRST + clone() + nodeSpecs << constraintNodeSpec + this + } + + void validate(){ + + } + + static ConstraintsSpec parse(@DelegatesTo(ConstraintsSpec)Closure closure){ + ConstraintsSpec constraintsSpec = new ConstraintsSpec() + def clone = closure.rehydrate(constraintsSpec, closure.owner, closure.thisObject) + clone.resolveStrategy = Closure.DELEGATE_FIRST + clone() + constraintsSpec.validate() + constraintsSpec + } +} diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/config/NomadJobOpts.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/config/NomadJobOpts.groovy index 2d61344..ea0b001 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/config/NomadJobOpts.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/config/NomadJobOpts.groovy @@ -41,6 +41,8 @@ class NomadJobOpts{ AffinitySpec affinitySpec ConstraintSpec constraintSpec + ConstraintsSpec constraintsSpec + NomadJobOpts(Map nomadJobOpts, Map env=null){ assert nomadJobOpts!=null @@ -69,6 +71,7 @@ class NomadJobOpts{ this.volumeSpec = parseVolumes(nomadJobOpts) this.affinitySpec = parseAffinity(nomadJobOpts) this.constraintSpec = parseConstraint(nomadJobOpts) + this.constraintsSpec = parseConstraints(nomadJobOpts) } VolumeSpec[] parseVolumes(Map nomadJobOpts){ @@ -110,6 +113,7 @@ class NomadJobOpts{ AffinitySpec parseAffinity(Map nomadJobOpts) { if (nomadJobOpts.affinity && nomadJobOpts.affinity instanceof Closure) { + log.info "affinity config will be deprecated, use affinities closure instead" def affinitySpec = new AffinitySpec() def closure = (nomadJobOpts.affinity as Closure) def clone = closure.rehydrate(affinitySpec, closure.owner, closure.thisObject) @@ -124,6 +128,7 @@ class NomadJobOpts{ ConstraintSpec parseConstraint(Map nomadJobOpts){ if (nomadJobOpts.constraint && nomadJobOpts.constraint instanceof Closure) { + log.info "constraint config will be deprecated, use constraints closure instead" def constraintSpec = new ConstraintSpec() def closure = (nomadJobOpts.constraint as Closure) def clone = closure.rehydrate(constraintSpec, closure.owner, closure.thisObject) @@ -135,4 +140,18 @@ class NomadJobOpts{ null } } + + ConstraintsSpec parseConstraints(Map nomadJobOpts){ + if (nomadJobOpts.constraints && nomadJobOpts.constraints instanceof Closure) { + def constraintsSpec = new ConstraintsSpec() + def closure = (nomadJobOpts.constraints as Closure) + def clone = closure.rehydrate(constraintsSpec, closure.owner, closure.thisObject) + clone.resolveStrategy = Closure.DELEGATE_FIRST + clone() + constraintsSpec.validate() + constraintsSpec + }else{ + null + } + } } \ No newline at end of file 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 bc6cbf6..8c7d942 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/executor/NomadService.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/executor/NomadService.groovy @@ -22,6 +22,8 @@ import groovy.util.logging.Slf4j import io.nomadproject.client.ApiClient import io.nomadproject.client.api.JobsApi import io.nomadproject.client.model.* +import nextflow.nomad.config.ConstraintNodeSpec +import nextflow.nomad.config.ConstraintsSpec import nextflow.nomad.config.NomadConfig import nextflow.nomad.config.VolumeSpec import nextflow.processor.TaskRun @@ -182,7 +184,8 @@ class NomadService implements Closeable{ volumes(task, taskDef, workingDir) affinity(task, taskDef) - constrains(task, taskDef) + constrain(task, taskDef) + constraints(task, taskDef) return taskDef } @@ -233,7 +236,7 @@ class NomadService implements Closeable{ taskDef } - protected Task constrains(TaskRun task, Task taskDef){ + protected Task constrain(TaskRun task, Task taskDef){ if( config.jobOpts().constraintSpec ){ def constraint = new Constraint() if(config.jobOpts().constraintSpec.attribute){ @@ -251,8 +254,84 @@ class NomadService implements Closeable{ taskDef } + protected Task constraints(TaskRun task, Task taskDef){ + def constraints = [] as List + + if( config.jobOpts().constraintsSpec ){ + def list = constraintsSpecToList(config.jobOpts().constraintsSpec) + constraints.addAll(list) + } + + if( task.processor?.config?.get(TaskDirectives.CONSTRAINTS) && + task.processor?.config?.get(TaskDirectives.CONSTRAINTS) instanceof Closure) { + Closure closure = task.processor?.config?.get(TaskDirectives.CONSTRAINTS) as Closure + ConstraintsSpec constraintsSpec = ConstraintsSpec.parse(closure) + def list = constraintsSpecToList(constraintsSpec) + constraints.addAll(list) + } + + if( constraints.size()) { + taskDef.constraints(constraints) + } + taskDef + } + + protected List constraintsSpecToList(ConstraintsSpec spec){ + def constraints = [] as List + if( spec?.nodeSpecs ){ + def nodes = config.jobOpts() + .constraintsSpec + ?.nodeSpecs + ?.collect({ nodeConstraints(it)}) + ?.flatten() as List + constraints.addAll(nodes) + } + return constraints + } + + protected List nodeConstraints(ConstraintNodeSpec nodeSpec){ + def ret = [] as List + if( nodeSpec.id ){ + ret.add new Constraint() + .ltarget('${node.unique.id}') + .operand("=") + .rtarget(nodeSpec.id) + } + if( nodeSpec.name ){ + ret.add new Constraint() + .ltarget('${node.unique.name}') + .operand("=") + .rtarget(nodeSpec.name) + } + if( nodeSpec.clientClass ){ + ret.add new Constraint() + .ltarget('${node.class}') + .operand("=") + .rtarget(nodeSpec.clientClass) + } + if( nodeSpec.dataCenter ){ + ret.add new Constraint() + .ltarget('${node.datacenter}') + .operand("=") + .rtarget(nodeSpec.dataCenter) + } + if( nodeSpec.region ){ + ret.add new Constraint() + .ltarget('${node.region}') + .operand("=") + .rtarget(nodeSpec.region) + } + if( nodeSpec.pool ){ + ret.add new Constraint() + .ltarget('${node.pool}') + .operand("=") + .rtarget(nodeSpec.pool) + } + ret + } + protected Job assignDatacenters(TaskRun task, Job job){ - def datacenters = task.processor?.config?.get("datacenters") + def datacenters = task.processor?.config?.get(TaskDirectives.DATACENTERS) if( datacenters ){ if( datacenters instanceof List) { job.datacenters( datacenters as List) diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/executor/TaskDirectives.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/executor/TaskDirectives.groovy index 669b617..edc1722 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/executor/TaskDirectives.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/executor/TaskDirectives.groovy @@ -4,7 +4,10 @@ class TaskDirectives { public static final String DATACENTERS = "datacenters" + public static final String CONSTRAINTS = "constraints" + public static final List ALL = [ - DATACENTERS + DATACENTERS, + CONSTRAINTS ] } diff --git a/plugins/nf-nomad/src/test/nextflow/nomad/NomadConfigSpec.groovy b/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadConfigSpec.groovy similarity index 99% rename from plugins/nf-nomad/src/test/nextflow/nomad/NomadConfigSpec.groovy rename to plugins/nf-nomad/src/test/nextflow/nomad/config/NomadConfigSpec.groovy index 028514f..749b87f 100644 --- a/plugins/nf-nomad/src/test/nextflow/nomad/NomadConfigSpec.groovy +++ b/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadConfigSpec.groovy @@ -15,7 +15,7 @@ * limitations under the License. */ -package nextflow.nomad +package nextflow.nomad.config import nextflow.nomad.config.NomadConfig import nextflow.nomad.config.VolumeSpec diff --git a/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadConstraintsSpec.groovy b/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadConstraintsSpec.groovy new file mode 100644 index 0000000..ac2a253 --- /dev/null +++ b/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadConstraintsSpec.groovy @@ -0,0 +1,114 @@ +/* + * Copyright 2023-, Stellenbosch University, South Africa + * Copyright 2024, Evaluacion y Desarrollo de Negocios, Spain + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nextflow.nomad.config + +import spock.lang.Specification + +/** + * Unit test for Nomad Config + * + * @author : Jorge Aguilera + * @author : Abhinav Sharma + */ +class NomadConstraintsSpec extends Specification { + + + void "should instantiate a constraints spec if specified"() { + when: + def config = new NomadConfig([ + jobs: [ + constraints: { + node { + unique = [id :"node-id", name: "node-name"] + clientClass = "linux-64bit" + pool = "custom-pool" + dataCenter = 'dc1' + region = 'us' + } + } + ] + ]) + + then: + config.jobOpts.constraintsSpec + config.jobOpts.constraintsSpec.nodeSpecs.size() + config.jobOpts.constraintsSpec.nodeSpecs[0].id == "node-id" + config.jobOpts.constraintsSpec.nodeSpecs[0].name == "node-name" + config.jobOpts.constraintsSpec.nodeSpecs[0].clientClass == "linux-64bit" + config.jobOpts.constraintsSpec.nodeSpecs[0].pool == "custom-pool" + config.jobOpts.constraintsSpec.nodeSpecs[0].dataCenter == "dc1" + config.jobOpts.constraintsSpec.nodeSpecs[0].region == "us" + } + + void "should instantiate a no completed constraints spec"() { + when: + def config = new NomadConfig([ + jobs: [ + constraints: { + node { + unique = [id :"node-id", name: "node-name"] + clientClass = "linux-64bit" + } + } + ] + ]) + + then: + config.jobOpts.constraintsSpec + config.jobOpts.constraintsSpec.nodeSpecs.size() + config.jobOpts.constraintsSpec.nodeSpecs[0].id == "node-id" + config.jobOpts.constraintsSpec.nodeSpecs[0].name == "node-name" + config.jobOpts.constraintsSpec.nodeSpecs[0].clientClass == "linux-64bit" + !config.jobOpts.constraintsSpec.nodeSpecs[0].pool + !config.jobOpts.constraintsSpec.nodeSpecs[0].dataCenter + } + + void "should instantiate a list of constraints spec if specified"() { + when: + def config = new NomadConfig([ + jobs: [ + constraints: { + node { + unique = [id :"node-id", name: "node-name"] + clientClass = "linux-64bit" + pool = "custom-pool" + dataCenter = 'dc1' + region = 'us' + } + node { + unique = [id :"node-id", name: "node-name"] + clientClass = "linux-64bit" + pool = "custom-pool" + dataCenter = 'dc1' + region = 'us' + } + } + ] + ]) + + then: + config.jobOpts.constraintsSpec + config.jobOpts.constraintsSpec.nodeSpecs.size() + config.jobOpts.constraintsSpec.nodeSpecs[0].id == "node-id" + config.jobOpts.constraintsSpec.nodeSpecs[0].name == "node-name" + config.jobOpts.constraintsSpec.nodeSpecs[0].clientClass == "linux-64bit" + config.jobOpts.constraintsSpec.nodeSpecs[0].pool == "custom-pool" + config.jobOpts.constraintsSpec.nodeSpecs[0].dataCenter == "dc1" + config.jobOpts.constraintsSpec.nodeSpecs[0].region == "us" + } +} 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 b29089d..2904264 100644 --- a/plugins/nf-nomad/src/test/nextflow/nomad/executor/NomadServiceSpec.groovy +++ b/plugins/nf-nomad/src/test/nextflow/nomad/executor/NomadServiceSpec.groovy @@ -622,4 +622,70 @@ class NomadServiceSpec extends Specification{ 1 | ['1'] ({ 'a'*10 }) | ['aaaaaaaaaa'] } + + void "submit a task with a node constraint"(){ + given: + def config = new NomadConfig( + client:[ + address : "http://${mockWebServer.hostName}:${mockWebServer.port}" + ], + jobs:[ + constraints: { + node { + unique = [name:'test'] + } + } + ] + ) + 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"] + + def mockTask = Mock(TaskRun){ + getName() >> name + getContainer() >> image + getConfig() >> Mock(TaskConfig) + getWorkDirStr() >> workingDir + getContainer() >> "ubuntu" + getProcessor() >> Mock(TaskProcessor){ + getExecutor() >> Mock(Executor){ + isFusionEnabled() >> false + } + } + getWorkDir() >> Path.of(workingDir) + toTaskBean() >> Mock(TaskBean){ + getWorkDir() >> Path.of(workingDir) + getScript() >> "theScript" + getShell() >> ["bash"] + getInputFiles() >> [:] + } + } + + mockWebServer.enqueue(new MockResponse() + .setBody(JsonOutput.toJson(["EvalID":"test"]).toString()) + .addHeader("Content-Type", "application/json")); + when: + + def idJob = service.submitTask(id, mockTask, args, 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.TaskGroups[0].Tasks[0].Constraints[0].LTarget == '${node.unique.name}' + body.Job.TaskGroups[0].Tasks[0].Constraints[0].RTarget == 'test' + body.Job.TaskGroups[0].Tasks[0].Constraints[0].Operand == '=' + } + } diff --git a/validation/constraints/main.nf b/validation/constraints/main.nf new file mode 100644 index 0000000..87f26f8 --- /dev/null +++ b/validation/constraints/main.nf @@ -0,0 +1,18 @@ +#!/usr/bin/env nextflow + +process sayHello { + container 'ubuntu:20.04' + + input: + val x + output: + stdout + script: + """ + echo '$x world!' + """ +} + +workflow { + Channel.of('Bonjour', 'Ciao', 'Hello', 'Hola') | sayHello | view +} \ No newline at end of file diff --git a/validation/constraints/node-nextflow.config b/validation/constraints/node-nextflow.config new file mode 100644 index 0000000..99077e5 --- /dev/null +++ b/validation/constraints/node-nextflow.config @@ -0,0 +1,41 @@ +plugins { + id 'nf-nomad@latest' +} + +process { + executor = "nomad" +} + +nomad { + + client { + address = "http://localhost:4646" + } + + jobs { + deleteOnCompletion = false + volume = { type "host" name "scratchdir" } + + constraints = { + node { + unique = [ name: params.RUN_IN_NODE ] + } + } + } + +} + +profiles{ + localnomad{ + process { + withName: sayHello { + datacenters = ['test-datacenter', 'demo-datacenter'] + constraints = { + node { + unique = [ name: params.RUN_IN_NODE ] + } + } + } + } + } +} \ No newline at end of file diff --git a/validation/run-all.sh b/validation/run-all.sh index 81867e0..7fade60 100755 --- a/validation/run-all.sh +++ b/validation/run-all.sh @@ -29,12 +29,14 @@ if [ "$SKIPLOCAL" == 0 ]; then ./run-pipeline.sh -c basic/nextflow.config basic/main.nf - ./run-pipeline.sh -c directives/nextflow.config directives/main.nf + ./run-pipeline.sh -c directives/nextflow.config directives/main.nf -profile localnomad ./run-pipeline.sh -c multiple-volumes/2-volumes.config multiple-volumes/main.nf ./run-pipeline.sh -c multiple-volumes/3-volumes.config multiple-volumes/main.nf + ./run-pipeline.sh -c constraints/node-nextflow.config constraints/main.nf -profile localnomad --RUN_IN_NODE $HOSTNAME + ./run-pipeline.sh -c basic/nextflow.config nf-core/demo \ -r dev -profile test,docker \ --outdir $(pwd)/nomad_temp/scratchdir/out diff --git a/validation/run-pipeline.sh b/validation/run-pipeline.sh index 6986600..bec6922 100755 --- a/validation/run-pipeline.sh +++ b/validation/run-pipeline.sh @@ -2,6 +2,8 @@ ./wait-nomad.sh +./nomad system gc + NXF_ASSETS=$(pwd)/nomad_temp/scratchdir/assets \ NXF_CACHE_DIR=$(pwd)/nomad_temp/scratchdir/cache \ nextflow run -w $(pwd)/nomad_temp/scratchdir/ "$@" From 0872a02153bf1dddb4034d9e7b41896a6fc51968 Mon Sep 17 00:00:00 2001 From: Jorge Aguilera Date: Sun, 14 Jul 2024 23:28:47 +0200 Subject: [PATCH 2/8] implement ConstraintAttrSpec Signed-off-by: Jorge Aguilera --- .../nomad/config/ConstraintAttrSpec.groovy | 38 +++ .../nomad/config/ConstraintsSpec.groovy | 16 +- .../nomad/executor/ConstraintsBuilder.groovy | 97 +++++++ .../nomad/executor/NomadService.groovy | 57 +--- .../nomad/config/NomadConstraintsSpec.groovy | 8 +- .../NomadServiceConstraintsSpec.groovy | 251 ++++++++++++++++++ .../nomad/executor/NomadServiceSpec.groovy | 65 ----- 7 files changed, 408 insertions(+), 124 deletions(-) create mode 100644 plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintAttrSpec.groovy create mode 100644 plugins/nf-nomad/src/main/nextflow/nomad/executor/ConstraintsBuilder.groovy create mode 100644 plugins/nf-nomad/src/test/nextflow/nomad/executor/NomadServiceConstraintsSpec.groovy diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintAttrSpec.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintAttrSpec.groovy new file mode 100644 index 0000000..0e3cda8 --- /dev/null +++ b/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintAttrSpec.groovy @@ -0,0 +1,38 @@ +package nextflow.nomad.config + +class ConstraintAttrSpec { + + private String arch = null + private Integer numcores= null + private Integer reservablecores= null + private Double totalcompute= null + + String getArch() { + return arch + } + + Integer getNumcores() { + return numcores + } + + Integer getReservablecores() { + return reservablecores + } + + Double getTotalcompute() { + return totalcompute + } + + ConstraintAttrSpec setCpu(Map map){ + cpu(map) + } + + ConstraintAttrSpec cpu(Map map){ + this.arch = map.containsKey("arch") ? map["arch"].toString() : null + this.numcores = map.containsKey("numcores") ? map["numcores"] as int : null + this.reservablecores = map.containsKey("reservablecores") ? map["reservablecores"] as int : null + this.totalcompute = map.containsKey("totalcompute") ? map["totalcompute"] as double : null + this + } + +} diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintsSpec.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintsSpec.groovy index 1d5aedd..0de58da 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintsSpec.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintsSpec.groovy @@ -3,13 +3,23 @@ package nextflow.nomad.config class ConstraintsSpec { List nodeSpecs = [] + List attrSpecs = [] ConstraintsSpec node( @DelegatesTo(ConstraintNodeSpec)Closure closure){ - ConstraintNodeSpec constraintNodeSpec = new ConstraintNodeSpec() - def clone = closure.rehydrate(constraintNodeSpec, closure.owner, closure.thisObject) + ConstraintNodeSpec constraintSpec = new ConstraintNodeSpec() + def clone = closure.rehydrate(constraintSpec, closure.owner, closure.thisObject) clone.resolveStrategy = Closure.DELEGATE_FIRST clone() - nodeSpecs << constraintNodeSpec + nodeSpecs << constraintSpec + this + } + + ConstraintsSpec attr( @DelegatesTo(ConstraintAttrSpec)Closure closure){ + ConstraintAttrSpec constraintSpec = new ConstraintAttrSpec() + def clone = closure.rehydrate(constraintSpec, closure.owner, closure.thisObject) + clone.resolveStrategy = Closure.DELEGATE_FIRST + clone() + attrSpecs << constraintSpec this } diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/executor/ConstraintsBuilder.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/executor/ConstraintsBuilder.groovy new file mode 100644 index 0000000..5c5cc05 --- /dev/null +++ b/plugins/nf-nomad/src/main/nextflow/nomad/executor/ConstraintsBuilder.groovy @@ -0,0 +1,97 @@ +package nextflow.nomad.executor + +import io.nomadproject.client.model.Constraint +import nextflow.nomad.config.ConstraintAttrSpec +import nextflow.nomad.config.ConstraintNodeSpec +import nextflow.nomad.config.ConstraintsSpec + +class ConstraintsBuilder { + + protected static List constraintsSpecToList(ConstraintsSpec spec){ + def constraints = [] as List + if( spec?.nodeSpecs ){ + def nodes = spec.nodeSpecs + ?.collect({ nodeConstraints(it)}) + ?.flatten() as List + constraints.addAll(nodes) + } + if( spec?.attrSpecs ){ + def nodes = spec.attrSpecs + ?.collect({ attrConstraints(it)}) + ?.flatten() as List + constraints.addAll(nodes) + } + return constraints + } + + protected static List nodeConstraints(ConstraintNodeSpec nodeSpec){ + def ret = [] as List + if( nodeSpec.id ){ + ret.add new Constraint() + .ltarget('${node.unique.id}') + .operand("=") + .rtarget(nodeSpec.id) + } + if( nodeSpec.name ){ + ret.add new Constraint() + .ltarget('${node.unique.name}') + .operand("=") + .rtarget(nodeSpec.name) + } + if( nodeSpec.clientClass ){ + ret.add new Constraint() + .ltarget('${node.class}') + .operand("=") + .rtarget(nodeSpec.clientClass) + } + if( nodeSpec.dataCenter ){ + ret.add new Constraint() + .ltarget('${node.datacenter}') + .operand("=") + .rtarget(nodeSpec.dataCenter) + } + if( nodeSpec.region ){ + ret.add new Constraint() + .ltarget('${node.region}') + .operand("=") + .rtarget(nodeSpec.region) + } + if( nodeSpec.pool ){ + ret.add new Constraint() + .ltarget('${node.pool}') + .operand("=") + .rtarget(nodeSpec.pool) + } + ret + } + + protected static List attrConstraints(ConstraintAttrSpec nodeSpec) { + def ret = [] as List + if (nodeSpec.arch) { + ret.add new Constraint() + .ltarget('${attr.cpu.arch}') + .operand("=") + .rtarget(nodeSpec.arch) + } + if (nodeSpec.numcores) { + ret.add new Constraint() + .ltarget('${attr.cpu.numcores}') + .operand("=") + .rtarget("$nodeSpec.numcores") + } + if (nodeSpec.reservablecores) { + ret.add new Constraint() + .ltarget('${attr.cpu.reservablecores}') + .operand("=") + .rtarget("$nodeSpec.reservablecores") + } + if (nodeSpec.totalcompute) { + ret.add new Constraint() + .ltarget('${attr.cpu.totalcompute}') + .operand("=") + .rtarget("$nodeSpec.totalcompute") + } + ret + } + +} 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 8c7d942..cd800c9 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/executor/NomadService.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/executor/NomadService.groovy @@ -22,7 +22,6 @@ import groovy.util.logging.Slf4j import io.nomadproject.client.ApiClient import io.nomadproject.client.api.JobsApi import io.nomadproject.client.model.* -import nextflow.nomad.config.ConstraintNodeSpec import nextflow.nomad.config.ConstraintsSpec import nextflow.nomad.config.NomadConfig import nextflow.nomad.config.VolumeSpec @@ -258,7 +257,7 @@ class NomadService implements Closeable{ def constraints = [] as List if( config.jobOpts().constraintsSpec ){ - def list = constraintsSpecToList(config.jobOpts().constraintsSpec) + def list = ConstraintsBuilder.constraintsSpecToList(config.jobOpts().constraintsSpec) constraints.addAll(list) } @@ -266,7 +265,7 @@ class NomadService implements Closeable{ task.processor?.config?.get(TaskDirectives.CONSTRAINTS) instanceof Closure) { Closure closure = task.processor?.config?.get(TaskDirectives.CONSTRAINTS) as Closure ConstraintsSpec constraintsSpec = ConstraintsSpec.parse(closure) - def list = constraintsSpecToList(constraintsSpec) + def list = ConstraintsBuilder.constraintsSpecToList(constraintsSpec) constraints.addAll(list) } @@ -276,59 +275,7 @@ class NomadService implements Closeable{ taskDef } - protected List constraintsSpecToList(ConstraintsSpec spec){ - def constraints = [] as List - if( spec?.nodeSpecs ){ - def nodes = config.jobOpts() - .constraintsSpec - ?.nodeSpecs - ?.collect({ nodeConstraints(it)}) - ?.flatten() as List - constraints.addAll(nodes) - } - return constraints - } - protected List nodeConstraints(ConstraintNodeSpec nodeSpec){ - def ret = [] as List - if( nodeSpec.id ){ - ret.add new Constraint() - .ltarget('${node.unique.id}') - .operand("=") - .rtarget(nodeSpec.id) - } - if( nodeSpec.name ){ - ret.add new Constraint() - .ltarget('${node.unique.name}') - .operand("=") - .rtarget(nodeSpec.name) - } - if( nodeSpec.clientClass ){ - ret.add new Constraint() - .ltarget('${node.class}') - .operand("=") - .rtarget(nodeSpec.clientClass) - } - if( nodeSpec.dataCenter ){ - ret.add new Constraint() - .ltarget('${node.datacenter}') - .operand("=") - .rtarget(nodeSpec.dataCenter) - } - if( nodeSpec.region ){ - ret.add new Constraint() - .ltarget('${node.region}') - .operand("=") - .rtarget(nodeSpec.region) - } - if( nodeSpec.pool ){ - ret.add new Constraint() - .ltarget('${node.pool}') - .operand("=") - .rtarget(nodeSpec.pool) - } - ret - } protected Job assignDatacenters(TaskRun task, Job job){ def datacenters = task.processor?.config?.get(TaskDirectives.DATACENTERS) diff --git a/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadConstraintsSpec.groovy b/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadConstraintsSpec.groovy index ac2a253..43bb352 100644 --- a/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadConstraintsSpec.groovy +++ b/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadConstraintsSpec.groovy @@ -40,19 +40,25 @@ class NomadConstraintsSpec extends Specification { dataCenter = 'dc1' region = 'us' } + attr{ + cpu = [arch:'286'] + } } ] ]) then: config.jobOpts.constraintsSpec - config.jobOpts.constraintsSpec.nodeSpecs.size() + config.jobOpts.constraintsSpec.nodeSpecs.size() == 1 config.jobOpts.constraintsSpec.nodeSpecs[0].id == "node-id" config.jobOpts.constraintsSpec.nodeSpecs[0].name == "node-name" config.jobOpts.constraintsSpec.nodeSpecs[0].clientClass == "linux-64bit" config.jobOpts.constraintsSpec.nodeSpecs[0].pool == "custom-pool" config.jobOpts.constraintsSpec.nodeSpecs[0].dataCenter == "dc1" config.jobOpts.constraintsSpec.nodeSpecs[0].region == "us" + + config.jobOpts.constraintsSpec.attrSpecs.size() == 1 + config.jobOpts.constraintsSpec.attrSpecs[0].arch == '286' } void "should instantiate a no completed constraints spec"() { diff --git a/plugins/nf-nomad/src/test/nextflow/nomad/executor/NomadServiceConstraintsSpec.groovy b/plugins/nf-nomad/src/test/nextflow/nomad/executor/NomadServiceConstraintsSpec.groovy new file mode 100644 index 0000000..11a2d77 --- /dev/null +++ b/plugins/nf-nomad/src/test/nextflow/nomad/executor/NomadServiceConstraintsSpec.groovy @@ -0,0 +1,251 @@ +/* + * Copyright 2023-, Stellenbosch University, South Africa + * Copyright 2024, Evaluacion y Desarrollo de Negocios, Spain + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package nextflow.nomad.executor + +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import nextflow.executor.Executor +import nextflow.nomad.config.NomadConfig +import nextflow.processor.TaskBean +import nextflow.processor.TaskConfig +import nextflow.processor.TaskProcessor +import nextflow.processor.TaskRun +import nextflow.script.ProcessConfig +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import spock.lang.Specification + +import java.nio.file.Files +import java.nio.file.Path + +/** + * Unit test for Nomad Service + * + * Validate requests using a Mock WebServer + * + * @author : Jorge Aguilera + */ +class NomadServiceConstraintsSpec extends Specification{ + + MockWebServer mockWebServer + + def setup() { + mockWebServer = new MockWebServer() + mockWebServer.start() + } + + def cleanup() { + mockWebServer.shutdown() + } + + void "submit a task with a node constraint"(){ + given: + def config = new NomadConfig( + client:[ + address : "http://${mockWebServer.hostName}:${mockWebServer.port}" + ], + jobs:[ + constraints: { + node { + unique = [name:'test'] + } + } + ] + ) + 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"] + + def mockTask = Mock(TaskRun){ + getName() >> name + getContainer() >> image + getConfig() >> Mock(TaskConfig) + getWorkDirStr() >> workingDir + getContainer() >> "ubuntu" + getProcessor() >> Mock(TaskProcessor){ + getExecutor() >> Mock(Executor){ + isFusionEnabled() >> false + } + } + getWorkDir() >> Path.of(workingDir) + toTaskBean() >> Mock(TaskBean){ + getWorkDir() >> Path.of(workingDir) + getScript() >> "theScript" + getShell() >> ["bash"] + getInputFiles() >> [:] + } + } + + mockWebServer.enqueue(new MockResponse() + .setBody(JsonOutput.toJson(["EvalID":"test"]).toString()) + .addHeader("Content-Type", "application/json")); + when: + + def idJob = service.submitTask(id, mockTask, args, 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.TaskGroups[0].Tasks[0].Constraints[0].LTarget == '${node.unique.name}' + body.Job.TaskGroups[0].Tasks[0].Constraints[0].RTarget == 'test' + body.Job.TaskGroups[0].Tasks[0].Constraints[0].Operand == '=' + } + + void "submit a task with a config attr constraint"(){ + given: + def config = new NomadConfig( + client:[ + address : "http://${mockWebServer.hostName}:${mockWebServer.port}" + ], + jobs:[ + constraints: { + attr { + cpu = [arch:'286'] + } + } + ] + ) + 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"] + + def mockTask = Mock(TaskRun){ + getName() >> name + getContainer() >> image + getConfig() >> Mock(TaskConfig) + getWorkDirStr() >> workingDir + getContainer() >> "ubuntu" + getProcessor() >> Mock(TaskProcessor){ + getExecutor() >> Mock(Executor){ + isFusionEnabled() >> false + } + } + getWorkDir() >> Path.of(workingDir) + toTaskBean() >> Mock(TaskBean){ + getWorkDir() >> Path.of(workingDir) + getScript() >> "theScript" + getShell() >> ["bash"] + getInputFiles() >> [:] + } + } + + mockWebServer.enqueue(new MockResponse() + .setBody(JsonOutput.toJson(["EvalID":"test"]).toString()) + .addHeader("Content-Type", "application/json")); + when: + + def idJob = service.submitTask(id, mockTask, args, 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.TaskGroups[0].Tasks[0].Constraints[0].LTarget == '${attr.cpu.arch}' + body.Job.TaskGroups[0].Tasks[0].Constraints[0].RTarget == '286' + body.Job.TaskGroups[0].Tasks[0].Constraints[0].Operand == '=' + } + + void "submit a task with an attr constraint"(){ + given: + def config = new NomadConfig( + client:[ + address : "http://${mockWebServer.hostName}:${mockWebServer.port}" + ], + ) + 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"] + + def contraints = { + attr { + cpu = [arch:'286'] + } + } + + def mockTask = Mock(TaskRun){ + getName() >> name + getContainer() >> image + getConfig() >> Mock(TaskConfig) + getWorkDirStr() >> workingDir + getContainer() >> "ubuntu" + getProcessor() >> Mock(TaskProcessor){ + getExecutor() >> Mock(Executor){ + isFusionEnabled() >> false + } + getConfig() >> Mock(ProcessConfig){ + get("constraints") >> contraints + } + } + getWorkDir() >> Path.of(workingDir) + toTaskBean() >> Mock(TaskBean){ + getWorkDir() >> Path.of(workingDir) + getScript() >> "theScript" + getShell() >> ["bash"] + getInputFiles() >> [:] + } + } + + mockWebServer.enqueue(new MockResponse() + .setBody(JsonOutput.toJson(["EvalID":"test"]).toString()) + .addHeader("Content-Type", "application/json")); + when: + + def idJob = service.submitTask(id, mockTask, args, 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.TaskGroups[0].Tasks[0].Constraints[0].LTarget == '${attr.cpu.arch}' + body.Job.TaskGroups[0].Tasks[0].Constraints[0].RTarget == '286' + body.Job.TaskGroups[0].Tasks[0].Constraints[0].Operand == '=' + } +} 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 2904264..4683617 100644 --- a/plugins/nf-nomad/src/test/nextflow/nomad/executor/NomadServiceSpec.groovy +++ b/plugins/nf-nomad/src/test/nextflow/nomad/executor/NomadServiceSpec.groovy @@ -623,69 +623,4 @@ class NomadServiceSpec extends Specification{ ({ 'a'*10 }) | ['aaaaaaaaaa'] } - void "submit a task with a node constraint"(){ - given: - def config = new NomadConfig( - client:[ - address : "http://${mockWebServer.hostName}:${mockWebServer.port}" - ], - jobs:[ - constraints: { - node { - unique = [name:'test'] - } - } - ] - ) - 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"] - - def mockTask = Mock(TaskRun){ - getName() >> name - getContainer() >> image - getConfig() >> Mock(TaskConfig) - getWorkDirStr() >> workingDir - getContainer() >> "ubuntu" - getProcessor() >> Mock(TaskProcessor){ - getExecutor() >> Mock(Executor){ - isFusionEnabled() >> false - } - } - getWorkDir() >> Path.of(workingDir) - toTaskBean() >> Mock(TaskBean){ - getWorkDir() >> Path.of(workingDir) - getScript() >> "theScript" - getShell() >> ["bash"] - getInputFiles() >> [:] - } - } - - mockWebServer.enqueue(new MockResponse() - .setBody(JsonOutput.toJson(["EvalID":"test"]).toString()) - .addHeader("Content-Type", "application/json")); - when: - - def idJob = service.submitTask(id, mockTask, args, 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.TaskGroups[0].Tasks[0].Constraints[0].LTarget == '${node.unique.name}' - body.Job.TaskGroups[0].Tasks[0].Constraints[0].RTarget == 'test' - body.Job.TaskGroups[0].Tasks[0].Constraints[0].Operand == '=' - } - } From c072348c97aedfcd2e232d193b2a3e69695ca064 Mon Sep 17 00:00:00 2001 From: Abhinav Sharma Date: Mon, 15 Jul 2024 18:07:20 +0200 Subject: [PATCH 3/8] Constraints refactor to introduce a specific models package (#68) * refactor the package organization to create a separate package for models * Refactor the test folder and add license headers --- .../nomad/config/ConstraintAttrSpec.groovy | 38 ------- .../nomad/config/ConstraintNodeSpec.groovy | 81 -------------- .../nomad/config/ConstraintsSpec.groovy | 38 ------- .../nextflow/nomad/config/NomadJobOpts.groovy | 34 +++--- .../nomad/executor/ConstraintsBuilder.groovy | 12 +- .../nomad/executor/NomadService.groovy | 14 +-- .../JobAffinity.groovy} | 12 +- .../JobConstraint.groovy} | 10 +- .../nomad/models/JobConstraints.groovy | 61 ++++++++++ .../nomad/models/JobConstraintsAttr.groovy | 62 +++++++++++ .../nomad/models/JobConstraintsNode.groovy | 105 ++++++++++++++++++ .../JobVolume.groovy} | 14 +-- .../nomad/config/NomadConfigSpec.groovy | 22 ++-- ...Spec.groovy => NomadJobConstraints.groovy} | 2 +- .../JobConstraintsSpec.groovy} | 6 +- validation/sun-nomadlab/nextflow.config | 5 + 16 files changed, 299 insertions(+), 217 deletions(-) delete mode 100644 plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintAttrSpec.groovy delete mode 100644 plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintNodeSpec.groovy delete mode 100644 plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintsSpec.groovy rename plugins/nf-nomad/src/main/nextflow/nomad/{config/AffinitySpec.groovy => models/JobAffinity.groovy} (86%) rename plugins/nf-nomad/src/main/nextflow/nomad/{config/ConstraintSpec.groovy => models/JobConstraint.groovy} (86%) create mode 100644 plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraints.groovy create mode 100644 plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraintsAttr.groovy create mode 100644 plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraintsNode.groovy rename plugins/nf-nomad/src/main/nextflow/nomad/{config/VolumeSpec.groovy => models/JobVolume.groovy} (92%) rename plugins/nf-nomad/src/test/nextflow/nomad/config/{NomadConstraintsSpec.groovy => NomadJobConstraints.groovy} (98%) rename plugins/nf-nomad/src/test/nextflow/nomad/{executor/NomadServiceConstraintsSpec.groovy => models/JobConstraintsSpec.groovy} (98%) diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintAttrSpec.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintAttrSpec.groovy deleted file mode 100644 index 0e3cda8..0000000 --- a/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintAttrSpec.groovy +++ /dev/null @@ -1,38 +0,0 @@ -package nextflow.nomad.config - -class ConstraintAttrSpec { - - private String arch = null - private Integer numcores= null - private Integer reservablecores= null - private Double totalcompute= null - - String getArch() { - return arch - } - - Integer getNumcores() { - return numcores - } - - Integer getReservablecores() { - return reservablecores - } - - Double getTotalcompute() { - return totalcompute - } - - ConstraintAttrSpec setCpu(Map map){ - cpu(map) - } - - ConstraintAttrSpec cpu(Map map){ - this.arch = map.containsKey("arch") ? map["arch"].toString() : null - this.numcores = map.containsKey("numcores") ? map["numcores"] as int : null - this.reservablecores = map.containsKey("reservablecores") ? map["reservablecores"] as int : null - this.totalcompute = map.containsKey("totalcompute") ? map["totalcompute"] as double : null - this - } - -} diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintNodeSpec.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintNodeSpec.groovy deleted file mode 100644 index 6260175..0000000 --- a/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintNodeSpec.groovy +++ /dev/null @@ -1,81 +0,0 @@ -package nextflow.nomad.config - -class ConstraintNodeSpec { - - private String id = null - private String name = null - private String clientClass = null - private String pool = null - private String dataCenter = null - private String region = null - - String getId() { - return id - } - - String getName() { - return name - } - - String getClientClass() { - return clientClass - } - - String getPool() { - return pool - } - - String getDataCenter() { - return dataCenter - } - - String getRegion() { - return region - } - - ConstraintNodeSpec setUnique(Map map){ - unique(map) - } - - ConstraintNodeSpec unique(Map map){ - this.id = map.containsKey("id") ? map["id"].toString() : null - this.name = map.containsKey("name") ? map["name"].toString() : null - this - } - - ConstraintNodeSpec setClientClass(Object map){ - clientClass(map) - } - - ConstraintNodeSpec clientClass(Object clientClass){ - this.clientClass = clientClass.toString() - this - } - - ConstraintNodeSpec setPool(Object map){ - pool(map) - } - - ConstraintNodeSpec pool(Object pool){ - this.pool = pool.toString() - this - } - - ConstraintNodeSpec setDataCenter(Object map){ - dataCenter(map) - } - - ConstraintNodeSpec dataCenter(Object dataCenter){ - this.dataCenter = dataCenter.toString() - this - } - - ConstraintNodeSpec setRegion(Object map){ - region(map) - } - - ConstraintNodeSpec region(Object region){ - this.region = region.toString() - this - } -} diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintsSpec.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintsSpec.groovy deleted file mode 100644 index 0de58da..0000000 --- a/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintsSpec.groovy +++ /dev/null @@ -1,38 +0,0 @@ -package nextflow.nomad.config - -class ConstraintsSpec { - - List nodeSpecs = [] - List attrSpecs = [] - - ConstraintsSpec node( @DelegatesTo(ConstraintNodeSpec)Closure closure){ - ConstraintNodeSpec constraintSpec = new ConstraintNodeSpec() - def clone = closure.rehydrate(constraintSpec, closure.owner, closure.thisObject) - clone.resolveStrategy = Closure.DELEGATE_FIRST - clone() - nodeSpecs << constraintSpec - this - } - - ConstraintsSpec attr( @DelegatesTo(ConstraintAttrSpec)Closure closure){ - ConstraintAttrSpec constraintSpec = new ConstraintAttrSpec() - def clone = closure.rehydrate(constraintSpec, closure.owner, closure.thisObject) - clone.resolveStrategy = Closure.DELEGATE_FIRST - clone() - attrSpecs << constraintSpec - this - } - - void validate(){ - - } - - static ConstraintsSpec parse(@DelegatesTo(ConstraintsSpec)Closure closure){ - ConstraintsSpec constraintsSpec = new ConstraintsSpec() - def clone = closure.rehydrate(constraintsSpec, closure.owner, closure.thisObject) - clone.resolveStrategy = Closure.DELEGATE_FIRST - clone() - constraintsSpec.validate() - constraintsSpec - } -} diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/config/NomadJobOpts.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/config/NomadJobOpts.groovy index ea0b001..a31dd3f 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/config/NomadJobOpts.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/config/NomadJobOpts.groovy @@ -19,6 +19,10 @@ package nextflow.nomad.config import groovy.transform.CompileStatic import groovy.util.logging.Slf4j +import nextflow.nomad.models.JobAffinity +import nextflow.nomad.models.JobConstraint +import nextflow.nomad.models.JobConstraints +import nextflow.nomad.models.JobVolume /** @@ -37,11 +41,11 @@ class NomadJobOpts{ String region String namespace String dockerVolume - VolumeSpec[] volumeSpec - AffinitySpec affinitySpec - ConstraintSpec constraintSpec + JobVolume[] volumeSpec + JobAffinity affinitySpec + JobConstraint constraintSpec - ConstraintsSpec constraintsSpec + JobConstraints constraintsSpec NomadJobOpts(Map nomadJobOpts, Map env=null){ assert nomadJobOpts!=null @@ -74,10 +78,10 @@ class NomadJobOpts{ this.constraintsSpec = parseConstraints(nomadJobOpts) } - VolumeSpec[] parseVolumes(Map nomadJobOpts){ - List ret = [] + JobVolume[] parseVolumes(Map nomadJobOpts){ + List ret = [] if( nomadJobOpts.volume && nomadJobOpts.volume instanceof Closure){ - def volumeSpec = new VolumeSpec() + def volumeSpec = new JobVolume() def closure = (nomadJobOpts.volume as Closure) def clone = closure.rehydrate(volumeSpec, closure.owner, closure.thisObject) clone.resolveStrategy = Closure.DELEGATE_FIRST @@ -89,7 +93,7 @@ class NomadJobOpts{ if( nomadJobOpts.volumes && nomadJobOpts.volumes instanceof List){ nomadJobOpts.volumes.each{ closure -> if( closure instanceof Closure){ - def volumeSpec = new VolumeSpec() + def volumeSpec = new JobVolume() def clone = closure.rehydrate(volumeSpec, closure.owner, closure.thisObject) clone.resolveStrategy = Closure.DELEGATE_FIRST clone() @@ -108,13 +112,13 @@ class NomadJobOpts{ throw new IllegalArgumentException("No more than a workdir volume allowed") } - return ret as VolumeSpec[] + return ret as JobVolume[] } - AffinitySpec parseAffinity(Map nomadJobOpts) { + JobAffinity parseAffinity(Map nomadJobOpts) { if (nomadJobOpts.affinity && nomadJobOpts.affinity instanceof Closure) { log.info "affinity config will be deprecated, use affinities closure instead" - def affinitySpec = new AffinitySpec() + def affinitySpec = new JobAffinity() def closure = (nomadJobOpts.affinity as Closure) def clone = closure.rehydrate(affinitySpec, closure.owner, closure.thisObject) clone.resolveStrategy = Closure.DELEGATE_FIRST @@ -126,10 +130,10 @@ class NomadJobOpts{ } } - ConstraintSpec parseConstraint(Map nomadJobOpts){ + JobConstraint parseConstraint(Map nomadJobOpts){ if (nomadJobOpts.constraint && nomadJobOpts.constraint instanceof Closure) { log.info "constraint config will be deprecated, use constraints closure instead" - def constraintSpec = new ConstraintSpec() + def constraintSpec = new JobConstraint() def closure = (nomadJobOpts.constraint as Closure) def clone = closure.rehydrate(constraintSpec, closure.owner, closure.thisObject) clone.resolveStrategy = Closure.DELEGATE_FIRST @@ -141,9 +145,9 @@ class NomadJobOpts{ } } - ConstraintsSpec parseConstraints(Map nomadJobOpts){ + JobConstraints parseConstraints(Map nomadJobOpts){ if (nomadJobOpts.constraints && nomadJobOpts.constraints instanceof Closure) { - def constraintsSpec = new ConstraintsSpec() + def constraintsSpec = new JobConstraints() def closure = (nomadJobOpts.constraints as Closure) def clone = closure.rehydrate(constraintsSpec, closure.owner, closure.thisObject) clone.resolveStrategy = Closure.DELEGATE_FIRST diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/executor/ConstraintsBuilder.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/executor/ConstraintsBuilder.groovy index 5c5cc05..bfd187b 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/executor/ConstraintsBuilder.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/executor/ConstraintsBuilder.groovy @@ -1,13 +1,13 @@ package nextflow.nomad.executor import io.nomadproject.client.model.Constraint -import nextflow.nomad.config.ConstraintAttrSpec -import nextflow.nomad.config.ConstraintNodeSpec -import nextflow.nomad.config.ConstraintsSpec +import nextflow.nomad.models.JobConstraintsAttr +import nextflow.nomad.models.JobConstraintsNode +import nextflow.nomad.models.JobConstraints class ConstraintsBuilder { - protected static List constraintsSpecToList(ConstraintsSpec spec){ + protected static List constraintsSpecToList(JobConstraints spec){ def constraints = [] as List if( spec?.nodeSpecs ){ def nodes = spec.nodeSpecs @@ -24,7 +24,7 @@ class ConstraintsBuilder { return constraints } - protected static List nodeConstraints(ConstraintNodeSpec nodeSpec){ + protected static List nodeConstraints(JobConstraintsNode nodeSpec){ def ret = [] as List if( nodeSpec.id ){ ret.add new Constraint() @@ -65,7 +65,7 @@ class ConstraintsBuilder { ret } - protected static List attrConstraints(ConstraintAttrSpec nodeSpec) { + protected static List attrConstraints(JobConstraintsAttr nodeSpec) { def ret = [] as List if (nodeSpec.arch) { ret.add new Constraint() 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 cd800c9..bc9ca50 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/executor/NomadService.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/executor/NomadService.groovy @@ -22,9 +22,9 @@ import groovy.util.logging.Slf4j import io.nomadproject.client.ApiClient import io.nomadproject.client.api.JobsApi import io.nomadproject.client.model.* -import nextflow.nomad.config.ConstraintsSpec +import nextflow.nomad.models.JobConstraints import nextflow.nomad.config.NomadConfig -import nextflow.nomad.config.VolumeSpec +import nextflow.nomad.models.JobVolume import nextflow.processor.TaskRun import nextflow.util.MemoryUnit import nextflow.exception.ProcessSubmitException @@ -136,7 +136,7 @@ class NomadService implements Closeable{ if( config.jobOpts().volumeSpec ) { taskGroup.volumes = [:] config.jobOpts().volumeSpec.eachWithIndex { volumeSpec , idx-> - if (volumeSpec && volumeSpec.type == VolumeSpec.VOLUME_CSI_TYPE) { + if (volumeSpec && volumeSpec.type == JobVolume.VOLUME_CSI_TYPE) { taskGroup.volumes["vol_${idx}".toString()] = new VolumeRequest( type: volumeSpec.type, source: volumeSpec.name, @@ -146,7 +146,7 @@ class NomadService implements Closeable{ ) } - if (volumeSpec && volumeSpec.type == VolumeSpec.VOLUME_HOST_TYPE) { + if (volumeSpec && volumeSpec.type == JobVolume.VOLUME_HOST_TYPE) { taskGroup.volumes["vol_${idx}".toString()] = new VolumeRequest( type: volumeSpec.type, source: volumeSpec.name, @@ -183,7 +183,7 @@ class NomadService implements Closeable{ volumes(task, taskDef, workingDir) affinity(task, taskDef) - constrain(task, taskDef) + constraint(task, taskDef) constraints(task, taskDef) return taskDef @@ -235,7 +235,7 @@ class NomadService implements Closeable{ taskDef } - protected Task constrain(TaskRun task, Task taskDef){ + protected Task constraint(TaskRun task, Task taskDef){ if( config.jobOpts().constraintSpec ){ def constraint = new Constraint() if(config.jobOpts().constraintSpec.attribute){ @@ -264,7 +264,7 @@ class NomadService implements Closeable{ if( task.processor?.config?.get(TaskDirectives.CONSTRAINTS) && task.processor?.config?.get(TaskDirectives.CONSTRAINTS) instanceof Closure) { Closure closure = task.processor?.config?.get(TaskDirectives.CONSTRAINTS) as Closure - ConstraintsSpec constraintsSpec = ConstraintsSpec.parse(closure) + JobConstraints constraintsSpec = JobConstraints.parse(closure) def list = ConstraintsBuilder.constraintsSpecToList(constraintsSpec) constraints.addAll(list) } diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/config/AffinitySpec.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobAffinity.groovy similarity index 86% rename from plugins/nf-nomad/src/main/nextflow/nomad/config/AffinitySpec.groovy rename to plugins/nf-nomad/src/main/nextflow/nomad/models/JobAffinity.groovy index 7d8277e..9e96584 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/config/AffinitySpec.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobAffinity.groovy @@ -15,13 +15,13 @@ * limitations under the License. */ -package nextflow.nomad.config +package nextflow.nomad.models /** * Nomad Job Affinity Spec * * @author Jorge Aguilera */ -class AffinitySpec{ +class JobAffinity { private String attribute private String operator @@ -44,22 +44,22 @@ class AffinitySpec{ return weight } - AffinitySpec attribute(String attribute){ + JobAffinity attribute(String attribute){ this.attribute=attribute this } - AffinitySpec operator(String operator){ + JobAffinity operator(String operator){ this.operator = operator this } - AffinitySpec value(String value){ + JobAffinity value(String value){ this.value = value this } - AffinitySpec weight(int weight){ + JobAffinity weight(int weight){ this.weight = weight this } diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintSpec.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraint.groovy similarity index 86% rename from plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintSpec.groovy rename to plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraint.groovy index 266bfd2..22a57f1 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/config/ConstraintSpec.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraint.groovy @@ -15,14 +15,14 @@ * limitations under the License. */ -package nextflow.nomad.config +package nextflow.nomad.models /** * Nomad Job Constraint Spec * * @author Jorge Aguilera */ -class ConstraintSpec { +class JobConstraint { private String attribute private String operator @@ -40,17 +40,17 @@ class ConstraintSpec { return value } - ConstraintSpec attribute(String attribute){ + JobConstraint attribute(String attribute){ this.attribute=attribute this } - ConstraintSpec operator(String operator){ + JobConstraint operator(String operator){ this.operator = operator this } - ConstraintSpec value(String value){ + JobConstraint value(String value){ this.value = value this } diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraints.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraints.groovy new file mode 100644 index 0000000..dff45d5 --- /dev/null +++ b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraints.groovy @@ -0,0 +1,61 @@ +/* + * Copyright 2023-, Stellenbosch University, South Africa + * Copyright 2024, Evaluacion y Desarrollo de Negocios, Spain + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nextflow.nomad.models + +/** + * Nomad Job Constraint Spec + * + * @author Jorge Aguilera + */ + +class JobConstraints { + + List nodeSpecs = [] + List attrSpecs = [] + + JobConstraints node(@DelegatesTo(JobConstraintsNode)Closure closure){ + JobConstraintsNode constraintSpec = new JobConstraintsNode() + def clone = closure.rehydrate(constraintSpec, closure.owner, closure.thisObject) + clone.resolveStrategy = Closure.DELEGATE_FIRST + clone() + nodeSpecs << constraintSpec + this + } + + JobConstraints attr(@DelegatesTo(JobConstraintsAttr)Closure closure){ + JobConstraintsAttr constraintSpec = new JobConstraintsAttr() + def clone = closure.rehydrate(constraintSpec, closure.owner, closure.thisObject) + clone.resolveStrategy = Closure.DELEGATE_FIRST + clone() + attrSpecs << constraintSpec + this + } + + void validate(){ + + } + + static JobConstraints parse(@DelegatesTo(JobConstraints)Closure closure){ + JobConstraints constraintsSpec = new JobConstraints() + def clone = closure.rehydrate(constraintsSpec, closure.owner, closure.thisObject) + clone.resolveStrategy = Closure.DELEGATE_FIRST + clone() + constraintsSpec.validate() + constraintsSpec + } +} diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraintsAttr.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraintsAttr.groovy new file mode 100644 index 0000000..5b0c1eb --- /dev/null +++ b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraintsAttr.groovy @@ -0,0 +1,62 @@ +/* + * Copyright 2023-, Stellenbosch University, South Africa + * Copyright 2024, Evaluacion y Desarrollo de Negocios, Spain + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package nextflow.nomad.models + +/** + * Nomad Job Constraint Spec + * + * @author Jorge Aguilera + */ + +class JobConstraintsAttr { + + private String arch = null + private Integer numcores= null + private Integer reservablecores= null + private Double totalcompute= null + + String getArch() { + return arch + } + + Integer getNumcores() { + return numcores + } + + Integer getReservablecores() { + return reservablecores + } + + Double getTotalcompute() { + return totalcompute + } + + JobConstraintsAttr setCpu(Map map){ + cpu(map) + } + + JobConstraintsAttr cpu(Map map){ + this.arch = map.containsKey("arch") ? map["arch"].toString() : null + this.numcores = map.containsKey("numcores") ? map["numcores"] as int : null + this.reservablecores = map.containsKey("reservablecores") ? map["reservablecores"] as int : null + this.totalcompute = map.containsKey("totalcompute") ? map["totalcompute"] as double : null + this + } + +} diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraintsNode.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraintsNode.groovy new file mode 100644 index 0000000..f02f4fa --- /dev/null +++ b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraintsNode.groovy @@ -0,0 +1,105 @@ +/* + * Copyright 2023-, Stellenbosch University, South Africa + * Copyright 2024, Evaluacion y Desarrollo de Negocios, Spain + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package nextflow.nomad.models + +/** + * Nomad Job Constraint Spec + * + * @author Jorge Aguilera + */ + +class JobConstraintsNode { + + private String id = null + private String name = null + private String clientClass = null + private String pool = null + private String dataCenter = null + private String region = null + + String getId() { + return id + } + + String getName() { + return name + } + + String getClientClass() { + return clientClass + } + + String getPool() { + return pool + } + + String getDataCenter() { + return dataCenter + } + + String getRegion() { + return region + } + + JobConstraintsNode setUnique(Map map){ + unique(map) + } + + JobConstraintsNode unique(Map map){ + this.id = map.containsKey("id") ? map["id"].toString() : null + this.name = map.containsKey("name") ? map["name"].toString() : null + this + } + + JobConstraintsNode setClientClass(Object map){ + clientClass(map) + } + + JobConstraintsNode clientClass(Object clientClass){ + this.clientClass = clientClass.toString() + this + } + + JobConstraintsNode setPool(Object map){ + pool(map) + } + + JobConstraintsNode pool(Object pool){ + this.pool = pool.toString() + this + } + + JobConstraintsNode setDataCenter(Object map){ + dataCenter(map) + } + + JobConstraintsNode dataCenter(Object dataCenter){ + this.dataCenter = dataCenter.toString() + this + } + + JobConstraintsNode setRegion(Object map){ + region(map) + } + + JobConstraintsNode region(Object region){ + this.region = region.toString() + this + } +} diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/config/VolumeSpec.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobVolume.groovy similarity index 92% rename from plugins/nf-nomad/src/main/nextflow/nomad/config/VolumeSpec.groovy rename to plugins/nf-nomad/src/main/nextflow/nomad/models/JobVolume.groovy index 529439b..70d3c80 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/config/VolumeSpec.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobVolume.groovy @@ -15,13 +15,13 @@ * limitations under the License. */ -package nextflow.nomad.config +package nextflow.nomad.models /** * Nomad Volume Spec * * @author Jorge Aguilera */ -class VolumeSpec { +class JobVolume { final static public String VOLUME_DOCKER_TYPE = "docker" final static public String VOLUME_CSI_TYPE = "csi" @@ -57,27 +57,27 @@ class VolumeSpec { return readOnly } - VolumeSpec type(String type){ + JobVolume type(String type){ this.type = type this } - VolumeSpec name(String name){ + JobVolume name(String name){ this.name = name this } - VolumeSpec workDir(boolean b){ + JobVolume workDir(boolean b){ this.workDir = b this } - VolumeSpec path(String path){ + JobVolume path(String path){ this.path = path this } - VolumeSpec readOnly(boolean readOnly){ + JobVolume readOnly(boolean readOnly){ this.readOnly = readOnly this } diff --git a/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadConfigSpec.groovy b/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadConfigSpec.groovy index 749b87f..3856a57 100644 --- a/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadConfigSpec.groovy +++ b/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadConfigSpec.groovy @@ -17,8 +17,8 @@ package nextflow.nomad.config -import nextflow.nomad.config.NomadConfig -import nextflow.nomad.config.VolumeSpec + +import nextflow.nomad.models.JobVolume import spock.lang.Specification import spock.lang.Unroll @@ -140,7 +140,7 @@ class NomadConfigSpec extends Specification { then: config.jobOpts.volumeSpec - config.jobOpts.volumeSpec[0].type == VolumeSpec.VOLUME_DOCKER_TYPE + config.jobOpts.volumeSpec[0].type == JobVolume.VOLUME_DOCKER_TYPE config.jobOpts.volumeSpec[0].name == "test" when: @@ -150,7 +150,7 @@ class NomadConfigSpec extends Specification { then: config2.jobOpts.volumeSpec - config2.jobOpts.volumeSpec[0].type == VolumeSpec.VOLUME_CSI_TYPE + config2.jobOpts.volumeSpec[0].type == JobVolume.VOLUME_CSI_TYPE config2.jobOpts.volumeSpec[0].name == "test" when: @@ -160,7 +160,7 @@ class NomadConfigSpec extends Specification { then: config3.jobOpts.volumeSpec - config3.jobOpts.volumeSpec[0].type == VolumeSpec.VOLUME_HOST_TYPE + config3.jobOpts.volumeSpec[0].type == JobVolume.VOLUME_HOST_TYPE config3.jobOpts.volumeSpec[0].name == "test" when: @@ -220,7 +220,7 @@ class NomadConfigSpec extends Specification { then: config.jobOpts.volumeSpec - config.jobOpts.volumeSpec[0].type == VolumeSpec.VOLUME_DOCKER_TYPE + config.jobOpts.volumeSpec[0].type == JobVolume.VOLUME_DOCKER_TYPE config.jobOpts.volumeSpec[0].name == "test" config.jobOpts.volumeSpec[0].workDir @@ -249,9 +249,9 @@ class NomadConfigSpec extends Specification { then: config2.jobOpts.volumeSpec.size()==2 - config2.jobOpts.volumeSpec[0].type == VolumeSpec.VOLUME_CSI_TYPE + config2.jobOpts.volumeSpec[0].type == JobVolume.VOLUME_CSI_TYPE config2.jobOpts.volumeSpec[0].name == "test" - config2.jobOpts.volumeSpec[1].type == VolumeSpec.VOLUME_DOCKER_TYPE + config2.jobOpts.volumeSpec[1].type == JobVolume.VOLUME_DOCKER_TYPE config2.jobOpts.volumeSpec[1].name == "test" config.jobOpts.volumeSpec[0].workDir @@ -270,9 +270,9 @@ class NomadConfigSpec extends Specification { then: config3.jobOpts.volumeSpec.size()==3 - config3.jobOpts.volumeSpec[0].type == VolumeSpec.VOLUME_CSI_TYPE - config3.jobOpts.volumeSpec[1].type == VolumeSpec.VOLUME_CSI_TYPE - config3.jobOpts.volumeSpec[2].type == VolumeSpec.VOLUME_DOCKER_TYPE + config3.jobOpts.volumeSpec[0].type == JobVolume.VOLUME_CSI_TYPE + config3.jobOpts.volumeSpec[1].type == JobVolume.VOLUME_CSI_TYPE + config3.jobOpts.volumeSpec[2].type == JobVolume.VOLUME_DOCKER_TYPE config3.jobOpts.volumeSpec[0].workDir config3.jobOpts.volumeSpec.findAll{ it.workDir}.size() == 1 diff --git a/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadConstraintsSpec.groovy b/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadJobConstraints.groovy similarity index 98% rename from plugins/nf-nomad/src/test/nextflow/nomad/config/NomadConstraintsSpec.groovy rename to plugins/nf-nomad/src/test/nextflow/nomad/config/NomadJobConstraints.groovy index 43bb352..d4b9692 100644 --- a/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadConstraintsSpec.groovy +++ b/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadJobConstraints.groovy @@ -25,7 +25,7 @@ import spock.lang.Specification * @author : Jorge Aguilera * @author : Abhinav Sharma */ -class NomadConstraintsSpec extends Specification { +class NomadJobConstraints extends Specification { void "should instantiate a constraints spec if specified"() { diff --git a/plugins/nf-nomad/src/test/nextflow/nomad/executor/NomadServiceConstraintsSpec.groovy b/plugins/nf-nomad/src/test/nextflow/nomad/models/JobConstraintsSpec.groovy similarity index 98% rename from plugins/nf-nomad/src/test/nextflow/nomad/executor/NomadServiceConstraintsSpec.groovy rename to plugins/nf-nomad/src/test/nextflow/nomad/models/JobConstraintsSpec.groovy index 11a2d77..1e57c6c 100644 --- a/plugins/nf-nomad/src/test/nextflow/nomad/executor/NomadServiceConstraintsSpec.groovy +++ b/plugins/nf-nomad/src/test/nextflow/nomad/models/JobConstraintsSpec.groovy @@ -14,12 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package nextflow.nomad.executor + +package nextflow.nomad.models import groovy.json.JsonOutput import groovy.json.JsonSlurper import nextflow.executor.Executor import nextflow.nomad.config.NomadConfig +import nextflow.nomad.executor.NomadService import nextflow.processor.TaskBean import nextflow.processor.TaskConfig import nextflow.processor.TaskProcessor @@ -39,7 +41,7 @@ import java.nio.file.Path * * @author : Jorge Aguilera */ -class NomadServiceConstraintsSpec extends Specification{ +class JobConstraintsSpec extends Specification{ MockWebServer mockWebServer diff --git a/validation/sun-nomadlab/nextflow.config b/validation/sun-nomadlab/nextflow.config index 5affd7b..71be7dd 100644 --- a/validation/sun-nomadlab/nextflow.config +++ b/validation/sun-nomadlab/nextflow.config @@ -34,5 +34,10 @@ nomad { volumes = [ { type "csi" name "juicefs-volume" } ] + constraints: { + attr { + unique = [hostName:'nomad02'] + } + } } } From c110afc960bd304b354a6df642bdf5d916729944 Mon Sep 17 00:00:00 2001 From: Jorge Aguilera Date: Mon, 15 Jul 2024 18:11:40 +0200 Subject: [PATCH 4/8] move contraintsbuilder to model Signed-off-by: Jorge Aguilera --- .../src/main/nextflow/nomad/executor/NomadService.groovy | 1 + .../nomad/{executor => models}/ConstraintsBuilder.groovy | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) rename plugins/nf-nomad/src/main/nextflow/nomad/{executor => models}/ConstraintsBuilder.groovy (96%) 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 bc9ca50..21cc80c 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/executor/NomadService.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/executor/NomadService.groovy @@ -22,6 +22,7 @@ import groovy.util.logging.Slf4j import io.nomadproject.client.ApiClient import io.nomadproject.client.api.JobsApi import io.nomadproject.client.model.* +import nextflow.nomad.models.ConstraintsBuilder import nextflow.nomad.models.JobConstraints import nextflow.nomad.config.NomadConfig import nextflow.nomad.models.JobVolume diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/executor/ConstraintsBuilder.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/models/ConstraintsBuilder.groovy similarity index 96% rename from plugins/nf-nomad/src/main/nextflow/nomad/executor/ConstraintsBuilder.groovy rename to plugins/nf-nomad/src/main/nextflow/nomad/models/ConstraintsBuilder.groovy index bfd187b..3659f31 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/executor/ConstraintsBuilder.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/models/ConstraintsBuilder.groovy @@ -1,4 +1,4 @@ -package nextflow.nomad.executor +package nextflow.nomad.models import io.nomadproject.client.model.Constraint import nextflow.nomad.models.JobConstraintsAttr @@ -7,7 +7,7 @@ import nextflow.nomad.models.JobConstraints class ConstraintsBuilder { - protected static List constraintsSpecToList(JobConstraints spec){ + static List constraintsSpecToList(JobConstraints spec){ def constraints = [] as List if( spec?.nodeSpecs ){ def nodes = spec.nodeSpecs From 6e7cebf959cf0866951c184b5478aea0efade4a0 Mon Sep 17 00:00:00 2001 From: Jorge Aguilera Date: Mon, 15 Jul 2024 18:59:45 +0200 Subject: [PATCH 5/8] refactor and new "raw" dsl to include open constraints Signed-off-by: Jorge Aguilera --- .../nomad/models/ConstraintsBuilder.groovy | 74 +++---------------- .../nomad/models/JobConstraintsAttr.groovy | 59 ++++++++++----- .../nomad/models/JobConstraintsNode.groovy | 58 ++++++--------- .../nomad/config/NomadJobConstraints.groovy | 44 +++++------ .../nomad/models/JobConstraintsSpec.groovy | 67 +++++++++++++++++ 5 files changed, 162 insertions(+), 140 deletions(-) diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/models/ConstraintsBuilder.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/models/ConstraintsBuilder.groovy index 3659f31..3275e19 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/models/ConstraintsBuilder.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/models/ConstraintsBuilder.groovy @@ -25,72 +25,22 @@ class ConstraintsBuilder { } protected static List nodeConstraints(JobConstraintsNode nodeSpec){ - def ret = [] as List - if( nodeSpec.id ){ - ret.add new Constraint() - .ltarget('${node.unique.id}') - .operand("=") - .rtarget(nodeSpec.id) - } - if( nodeSpec.name ){ - ret.add new Constraint() - .ltarget('${node.unique.name}') - .operand("=") - .rtarget(nodeSpec.name) - } - if( nodeSpec.clientClass ){ - ret.add new Constraint() - .ltarget('${node.class}') - .operand("=") - .rtarget(nodeSpec.clientClass) - } - if( nodeSpec.dataCenter ){ - ret.add new Constraint() - .ltarget('${node.datacenter}') - .operand("=") - .rtarget(nodeSpec.dataCenter) - } - if( nodeSpec.region ){ - ret.add new Constraint() - .ltarget('${node.region}') - .operand("=") - .rtarget(nodeSpec.region) - } - if( nodeSpec.pool ){ - ret.add new Constraint() - .ltarget('${node.pool}') - .operand("=") - .rtarget(nodeSpec.pool) - } + def ret = nodeSpec.raws?.collect{ triple-> + return new Constraint() + .ltarget('${'+triple.left+'}') + .operand(triple.middle) + .rtarget(triple.right) + } as List ret } protected static List attrConstraints(JobConstraintsAttr nodeSpec) { - def ret = [] as List - if (nodeSpec.arch) { - ret.add new Constraint() - .ltarget('${attr.cpu.arch}') - .operand("=") - .rtarget(nodeSpec.arch) - } - if (nodeSpec.numcores) { - ret.add new Constraint() - .ltarget('${attr.cpu.numcores}') - .operand("=") - .rtarget("$nodeSpec.numcores") - } - if (nodeSpec.reservablecores) { - ret.add new Constraint() - .ltarget('${attr.cpu.reservablecores}') - .operand("=") - .rtarget("$nodeSpec.reservablecores") - } - if (nodeSpec.totalcompute) { - ret.add new Constraint() - .ltarget('${attr.cpu.totalcompute}') - .operand("=") - .rtarget("$nodeSpec.totalcompute") - } + def ret = nodeSpec.raws?.collect{ triple-> + return new Constraint() + .ltarget('${'+triple.left+'}') + .operand(triple.middle) + .rtarget(triple.right) + } as List ret } diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraintsAttr.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraintsAttr.groovy index 5b0c1eb..875b158 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraintsAttr.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraintsAttr.groovy @@ -18,6 +18,8 @@ package nextflow.nomad.models +import org.apache.commons.lang3.tuple.Triple + /** * Nomad Job Constraint Spec * @@ -26,37 +28,56 @@ package nextflow.nomad.models class JobConstraintsAttr { - private String arch = null - private Integer numcores= null - private Integer reservablecores= null - private Double totalcompute= null + private List> raws= [] - String getArch() { - return arch + List> getRaws() { + return raws } - Integer getNumcores() { - return numcores + JobConstraintsAttr setCpu(Map map){ + cpu(map) } - Integer getReservablecores() { - return reservablecores + JobConstraintsAttr cpu(Map map){ + if( map.containsKey('arch')) + raw("cpu.arch","=", map['arch'].toString()) + if( map.containsKey('numcores')) + raw("cpu.numcores",">=", map['numcores'].toString()) + if( map.containsKey('reservablecores')) + raw("cpu.reservablecores",">=", map['reservablecores'].toString()) + if( map.containsKey('totalcompute')) + raw("cpu.totalcompute","=", map['totalcompute'].toString()) + this } - Double getTotalcompute() { - return totalcompute + JobConstraintsAttr setUnique(Map map){ + unique(map) } - JobConstraintsAttr setCpu(Map map){ - cpu(map) + JobConstraintsAttr unique(Map map){ + if( map.containsKey('hostname')) + raw("unique.hostname","=", map['hostname'].toString()) + if( map.containsKey('ip-address')) + raw("unique.network.ip-address","=", map['ip-address'].toString()) + this } - JobConstraintsAttr cpu(Map map){ - this.arch = map.containsKey("arch") ? map["arch"].toString() : null - this.numcores = map.containsKey("numcores") ? map["numcores"] as int : null - this.reservablecores = map.containsKey("reservablecores") ? map["reservablecores"] as int : null - this.totalcompute = map.containsKey("totalcompute") ? map["totalcompute"] as double : null + JobConstraintsAttr setKernel(Map map){ + kernel(map) + } + + JobConstraintsAttr kernel(Map map){ + if( map.containsKey('arch')) + raw("kernel.arch","=", map['arch'].toString()) + if( map.containsKey('name')) + raw("kernel.name","=", map['name'].toString()) + if( map.containsKey('version')) + raw("kernel.version","=", map['version'].toString()) this } + JobConstraintsAttr raw(String attr, String operator, String value){ + raws.add Triple.of("attr."+attr, operator, value) + this + } } diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraintsNode.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraintsNode.groovy index f02f4fa..3ac0f9e 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraintsNode.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraintsNode.groovy @@ -18,6 +18,8 @@ package nextflow.nomad.models +import org.apache.commons.lang3.tuple.Triple + /** * Nomad Job Constraint Spec * @@ -26,35 +28,10 @@ package nextflow.nomad.models class JobConstraintsNode { - private String id = null - private String name = null - private String clientClass = null - private String pool = null - private String dataCenter = null - private String region = null - - String getId() { - return id - } - - String getName() { - return name - } - - String getClientClass() { - return clientClass - } - - String getPool() { - return pool - } - - String getDataCenter() { - return dataCenter - } + private List> raws= [] - String getRegion() { - return region + List> getRaws() { + return raws } JobConstraintsNode setUnique(Map map){ @@ -62,17 +39,19 @@ class JobConstraintsNode { } JobConstraintsNode unique(Map map){ - this.id = map.containsKey("id") ? map["id"].toString() : null - this.name = map.containsKey("name") ? map["name"].toString() : null + ['id', 'name'].each { key-> + if( map.containsKey(key)) + raw("unique.${key}","=", map[key].toString()) + } this } - JobConstraintsNode setClientClass(Object map){ - clientClass(map) + JobConstraintsNode setClazz(Object map){ // class is a reserved word, in java we used clazz + clazz(map) } - JobConstraintsNode clientClass(Object clientClass){ - this.clientClass = clientClass.toString() + JobConstraintsNode clazz(Object cls){ + raw("class","=", cls.toString()) this } @@ -81,7 +60,7 @@ class JobConstraintsNode { } JobConstraintsNode pool(Object pool){ - this.pool = pool.toString() + raw("pool","=", pool.toString()) this } @@ -90,7 +69,7 @@ class JobConstraintsNode { } JobConstraintsNode dataCenter(Object dataCenter){ - this.dataCenter = dataCenter.toString() + raw("datacenter","=", dataCenter.toString()) this } @@ -99,7 +78,12 @@ class JobConstraintsNode { } JobConstraintsNode region(Object region){ - this.region = region.toString() + raw("region","=", region.toString()) + this + } + + JobConstraintsNode raw(String attr, String operator, String value){ + raws.add Triple.of("node."+attr, operator, value) this } } diff --git a/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadJobConstraints.groovy b/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadJobConstraints.groovy index d4b9692..f67006c 100644 --- a/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadJobConstraints.groovy +++ b/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadJobConstraints.groovy @@ -35,7 +35,7 @@ class NomadJobConstraints extends Specification { constraints: { node { unique = [id :"node-id", name: "node-name"] - clientClass = "linux-64bit" + clazz = "linux-64bit" pool = "custom-pool" dataCenter = 'dc1' region = 'us' @@ -50,15 +50,15 @@ class NomadJobConstraints extends Specification { then: config.jobOpts.constraintsSpec config.jobOpts.constraintsSpec.nodeSpecs.size() == 1 - config.jobOpts.constraintsSpec.nodeSpecs[0].id == "node-id" - config.jobOpts.constraintsSpec.nodeSpecs[0].name == "node-name" - config.jobOpts.constraintsSpec.nodeSpecs[0].clientClass == "linux-64bit" - config.jobOpts.constraintsSpec.nodeSpecs[0].pool == "custom-pool" - config.jobOpts.constraintsSpec.nodeSpecs[0].dataCenter == "dc1" - config.jobOpts.constraintsSpec.nodeSpecs[0].region == "us" + config.jobOpts.constraintsSpec.nodeSpecs[0].raws.find{it.left.endsWith("id")}.right == "node-id" + config.jobOpts.constraintsSpec.nodeSpecs[0].raws.find{it.left.endsWith("name")}.right == "node-name" + config.jobOpts.constraintsSpec.nodeSpecs[0].raws.find{it.left.endsWith("class")}.right == "linux-64bit" + config.jobOpts.constraintsSpec.nodeSpecs[0].raws.find{it.left.endsWith("pool")}.right == "custom-pool" + config.jobOpts.constraintsSpec.nodeSpecs[0].raws.find{it.left.endsWith("datacenter")}.right == "dc1" + config.jobOpts.constraintsSpec.nodeSpecs[0].raws.find{it.left.endsWith("region")}.right == "us" config.jobOpts.constraintsSpec.attrSpecs.size() == 1 - config.jobOpts.constraintsSpec.attrSpecs[0].arch == '286' + config.jobOpts.constraintsSpec.attrSpecs[0].raws[0].right == '286' } void "should instantiate a no completed constraints spec"() { @@ -68,7 +68,7 @@ class NomadJobConstraints extends Specification { constraints: { node { unique = [id :"node-id", name: "node-name"] - clientClass = "linux-64bit" + clazz = "linux-64bit" } } ] @@ -77,11 +77,11 @@ class NomadJobConstraints extends Specification { then: config.jobOpts.constraintsSpec config.jobOpts.constraintsSpec.nodeSpecs.size() - config.jobOpts.constraintsSpec.nodeSpecs[0].id == "node-id" - config.jobOpts.constraintsSpec.nodeSpecs[0].name == "node-name" - config.jobOpts.constraintsSpec.nodeSpecs[0].clientClass == "linux-64bit" - !config.jobOpts.constraintsSpec.nodeSpecs[0].pool - !config.jobOpts.constraintsSpec.nodeSpecs[0].dataCenter + config.jobOpts.constraintsSpec.nodeSpecs[0].raws.find{it.left.endsWith("id")}.right == "node-id" + config.jobOpts.constraintsSpec.nodeSpecs[0].raws.find{it.left.endsWith("name")}.right == "node-name" + config.jobOpts.constraintsSpec.nodeSpecs[0].raws.find{it.left.endsWith("class")}.right == "linux-64bit" + !config.jobOpts.constraintsSpec.nodeSpecs[0].raws.find{it.left.endsWith("pool")} + !config.jobOpts.constraintsSpec.nodeSpecs[0].raws.find{it.left.endsWith("datacenter")} } void "should instantiate a list of constraints spec if specified"() { @@ -91,14 +91,14 @@ class NomadJobConstraints extends Specification { constraints: { node { unique = [id :"node-id", name: "node-name"] - clientClass = "linux-64bit" + clazz = "linux-64bit" pool = "custom-pool" dataCenter = 'dc1' region = 'us' } node { unique = [id :"node-id", name: "node-name"] - clientClass = "linux-64bit" + clazz = "linux-64bit" pool = "custom-pool" dataCenter = 'dc1' region = 'us' @@ -110,11 +110,11 @@ class NomadJobConstraints extends Specification { then: config.jobOpts.constraintsSpec config.jobOpts.constraintsSpec.nodeSpecs.size() - config.jobOpts.constraintsSpec.nodeSpecs[0].id == "node-id" - config.jobOpts.constraintsSpec.nodeSpecs[0].name == "node-name" - config.jobOpts.constraintsSpec.nodeSpecs[0].clientClass == "linux-64bit" - config.jobOpts.constraintsSpec.nodeSpecs[0].pool == "custom-pool" - config.jobOpts.constraintsSpec.nodeSpecs[0].dataCenter == "dc1" - config.jobOpts.constraintsSpec.nodeSpecs[0].region == "us" + config.jobOpts.constraintsSpec.nodeSpecs[0].raws.find{it.left.endsWith("id")}.right == "node-id" + config.jobOpts.constraintsSpec.nodeSpecs[0].raws.find{it.left.endsWith("name")}.right == "node-name" + config.jobOpts.constraintsSpec.nodeSpecs[0].raws.find{it.left.endsWith("class")}.right == "linux-64bit" + config.jobOpts.constraintsSpec.nodeSpecs[0].raws.find{it.left.endsWith("pool")}.right == "custom-pool" + config.jobOpts.constraintsSpec.nodeSpecs[0].raws.find{it.left.endsWith("datacenter")}.right == "dc1" + config.jobOpts.constraintsSpec.nodeSpecs[0].raws.find{it.left.endsWith("region")}.right == "us" } } diff --git a/plugins/nf-nomad/src/test/nextflow/nomad/models/JobConstraintsSpec.groovy b/plugins/nf-nomad/src/test/nextflow/nomad/models/JobConstraintsSpec.groovy index 1e57c6c..0fc3ae6 100644 --- a/plugins/nf-nomad/src/test/nextflow/nomad/models/JobConstraintsSpec.groovy +++ b/plugins/nf-nomad/src/test/nextflow/nomad/models/JobConstraintsSpec.groovy @@ -250,4 +250,71 @@ class JobConstraintsSpec extends Specification{ body.Job.TaskGroups[0].Tasks[0].Constraints[0].RTarget == '286' body.Job.TaskGroups[0].Tasks[0].Constraints[0].Operand == '=' } + + void "submit a task with a raw attr constraint"(){ + given: + def config = new NomadConfig( + client:[ + address : "http://${mockWebServer.hostName}:${mockWebServer.port}" + ], + ) + 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"] + + def contraints = { + attr { + raw 'platform.aws.instance-type', '=', 'm4.xlarge' + } + } + + def mockTask = Mock(TaskRun){ + getName() >> name + getContainer() >> image + getConfig() >> Mock(TaskConfig) + getWorkDirStr() >> workingDir + getContainer() >> "ubuntu" + getProcessor() >> Mock(TaskProcessor){ + getExecutor() >> Mock(Executor){ + isFusionEnabled() >> false + } + getConfig() >> Mock(ProcessConfig){ + get("constraints") >> contraints + } + } + getWorkDir() >> Path.of(workingDir) + toTaskBean() >> Mock(TaskBean){ + getWorkDir() >> Path.of(workingDir) + getScript() >> "theScript" + getShell() >> ["bash"] + getInputFiles() >> [:] + } + } + + mockWebServer.enqueue(new MockResponse() + .setBody(JsonOutput.toJson(["EvalID":"test"]).toString()) + .addHeader("Content-Type", "application/json")); + when: + + def idJob = service.submitTask(id, mockTask, args, 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.TaskGroups[0].Tasks[0].Constraints[0].LTarget == '${attr.platform.aws.instance-type}' + body.Job.TaskGroups[0].Tasks[0].Constraints[0].RTarget == 'm4.xlarge' + body.Job.TaskGroups[0].Tasks[0].Constraints[0].Operand == '=' + } } From 08b82b1d89f8a8ac79d5cfff9f1e949df27c54b9 Mon Sep 17 00:00:00 2001 From: Abhinav Sharma Date: Mon, 15 Jul 2024 19:49:51 +0200 Subject: [PATCH 6/8] iterate upon the constraint config [ci skip] --- gradle.properties | 2 +- ....groovy => NomadJobConstraintsSpec.groovy} | 2 +- validation/sun-nomadlab/nextflow.config | 29 ++++++++++++------- 3 files changed, 20 insertions(+), 13 deletions(-) rename plugins/nf-nomad/src/test/nextflow/nomad/config/{NomadJobConstraints.groovy => NomadJobConstraintsSpec.groovy} (98%) diff --git a/gradle.properties b/gradle.properties index 4ed0a15..9626efd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=0.1.1 +version=0.1.2 github_organization=nextflow-io \ No newline at end of file diff --git a/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadJobConstraints.groovy b/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadJobConstraintsSpec.groovy similarity index 98% rename from plugins/nf-nomad/src/test/nextflow/nomad/config/NomadJobConstraints.groovy rename to plugins/nf-nomad/src/test/nextflow/nomad/config/NomadJobConstraintsSpec.groovy index f67006c..c0b5d92 100644 --- a/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadJobConstraints.groovy +++ b/plugins/nf-nomad/src/test/nextflow/nomad/config/NomadJobConstraintsSpec.groovy @@ -25,7 +25,7 @@ import spock.lang.Specification * @author : Jorge Aguilera * @author : Abhinav Sharma */ -class NomadJobConstraints extends Specification { +class NomadJobConstraintsSpec extends Specification { void "should instantiate a constraints spec if specified"() { diff --git a/validation/sun-nomadlab/nextflow.config b/validation/sun-nomadlab/nextflow.config index 71be7dd..75cd8bf 100644 --- a/validation/sun-nomadlab/nextflow.config +++ b/validation/sun-nomadlab/nextflow.config @@ -15,29 +15,36 @@ aws { } wave { - enabled = true + enabled = true } fusion { - enabled = true - exportStorageCredentials = true + enabled = true + exportStorageCredentials = true } nomad { client { - address = 'http://100.119.165.23:4646' + address = 'http://100.119.165.23:4646' } jobs { - deleteOnCompletion = false - volumes = [ - { type "csi" name "juicefs-volume" } - ] - constraints: { - attr { - unique = [hostName:'nomad02'] + deleteOnCompletion = false + volumes = [ + { type "csi" name "juicefs-volume" } + ] + constraints { + + node { + unique = [name: "nomad02"] } + /* + attr { + raw 'platform.aws.instance-type', '=', 'm4.xlarge' + } + + */ } } } From 1e14510cf1fba4dcdcf3b0de33896eaa0f066c7a Mon Sep 17 00:00:00 2001 From: Jorge Aguilera Date: Mon, 15 Jul 2024 20:19:45 +0200 Subject: [PATCH 7/8] fix nomadlab typos Signed-off-by: Jorge Aguilera --- validation/sun-nomadlab/nextflow.config | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/validation/sun-nomadlab/nextflow.config b/validation/sun-nomadlab/nextflow.config index 75cd8bf..0d2a595 100644 --- a/validation/sun-nomadlab/nextflow.config +++ b/validation/sun-nomadlab/nextflow.config @@ -34,17 +34,16 @@ nomad { volumes = [ { type "csi" name "juicefs-volume" } ] - constraints { + constraints = { node { unique = [name: "nomad02"] } - /* - attr { - raw 'platform.aws.instance-type', '=', 'm4.xlarge' - } - */ + attr { + unique = [hostname:'nomad02'] + //raw 'platform.aws.instance-type', '=', 'm4.xlarge' + } } } } From 7efc44ed60419b730b51a020fc58f0b321d5076d Mon Sep 17 00:00:00 2001 From: Abhinav Sharma Date: Mon, 15 Jul 2024 21:40:01 +0200 Subject: [PATCH 8/8] use cloud cache in sun-nomadlab [ci skip] --- validation/run-all.sh | 6 +++--- validation/sun-nomadlab/nextflow.config | 8 +++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/validation/run-all.sh b/validation/run-all.sh index 7fade60..f1136c0 100755 --- a/validation/run-all.sh +++ b/validation/run-all.sh @@ -71,8 +71,8 @@ fi #NOTE: In this use-case you need to be in the same network of sun-nomadlab server, for example using a tailscale connection #NOTE2: You need to have 2 secrets stored in your Nextlow: SUN_NOMADLAB_ACCESS_KEY and SUN_NOMADLAB_SECRET_KEY if [ "$NFSUN" == 1 ]; then - - if [ "$NFSLEEP" == 1 ]; then + NXF_CLOUDCACHE_PATH="s3://fusionfs/integration-test/cache" + if [ "$NFSLEEP" == 1 ]; then nextflow run -w s3://fusionfs/integration-test/work -c sun-nomadlab/nextflow.config abhi18av/nf-sleep --timeout 360 elif [ "$NFDEMO" == 1 ]; then @@ -86,7 +86,7 @@ if [ "$NFSUN" == 1 ]; then -w s3://fusionfs/integration-test/work -c sun-nomadlab/nextflow.config \ -profile test,docker --outdir s3://fusionfs/integration-test/bactopia/outdir \ --accession SRX4563634 --coverage 100 --genome_size 2800000 \ - --datasets_cache s3://fusionfs/integration-test/bactopia/assets + --datasets_cache s3://fusionfs/integration-test/bactopia/assets -resume fi else diff --git a/validation/sun-nomadlab/nextflow.config b/validation/sun-nomadlab/nextflow.config index 0d2a595..263a7e1 100644 --- a/validation/sun-nomadlab/nextflow.config +++ b/validation/sun-nomadlab/nextflow.config @@ -31,17 +31,15 @@ nomad { jobs { deleteOnCompletion = false - volumes = [ - { type "csi" name "juicefs-volume" } - ] + constraints = { node { - unique = [name: "nomad02"] + unique = [name: "nomad03"] } attr { - unique = [hostname:'nomad02'] + unique = [hostname:'nomad03'] //raw 'platform.aws.instance-type', '=', 'm4.xlarge' } }