From 0157d7d6359e2a606af4c6158f4ba065758368ca Mon Sep 17 00:00:00 2001 From: Abhinav Sharma Date: Mon, 16 Sep 2024 17:56:01 +0200 Subject: [PATCH] refactor NomadService to rely on Builder classes Signed-off-by: Abhinav Sharma --- .../nextflow/nomad/builders/JobBuilder.groovy | 68 ++++++++++ .../JobConstraintsBuilder.groovy} | 28 ++++- .../nomad/builders/JobSpreadsBuilder.groovy | 61 +++++++++ .../nomad/builders/TaskBuilder.groovy | 58 +++++++++ .../nomad/builders/TaskGroupBuilder.groovy | 55 +++++++++ .../nextflow/nomad/config/NomadJobOpts.groovy | 115 ++--------------- .../nomad/config/NomadSecretOpts.groovy | 28 +++++ .../nomad/executor/NomadService.groovy | 116 +++++++----------- .../nomad/executor/TaskDirectives.groovy | 11 ++ .../nextflow/nomad/models/JobAffinity.groovy | 21 ++++ .../nomad/models/JobConstraint.groovy | 25 ++++ .../nomad/models/JobConstraints.groovy | 16 +++ .../nextflow/nomad/models/JobSecrets.groovy | 45 +++++++ .../nextflow/nomad/models/JobVolume.groovy | 46 +++++++ .../nomad/models/SpreadsBuilder.groovy | 30 ----- 15 files changed, 517 insertions(+), 206 deletions(-) create mode 100644 plugins/nf-nomad/src/main/nextflow/nomad/builders/JobBuilder.groovy rename plugins/nf-nomad/src/main/nextflow/nomad/{models/ConstraintsBuilder.groovy => builders/JobConstraintsBuilder.groovy} (62%) create mode 100644 plugins/nf-nomad/src/main/nextflow/nomad/builders/JobSpreadsBuilder.groovy create mode 100644 plugins/nf-nomad/src/main/nextflow/nomad/builders/TaskBuilder.groovy create mode 100644 plugins/nf-nomad/src/main/nextflow/nomad/builders/TaskGroupBuilder.groovy create mode 100644 plugins/nf-nomad/src/main/nextflow/nomad/models/JobSecrets.groovy delete mode 100644 plugins/nf-nomad/src/main/nextflow/nomad/models/SpreadsBuilder.groovy diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/builders/JobBuilder.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/builders/JobBuilder.groovy new file mode 100644 index 0000000..b297e2b --- /dev/null +++ b/plugins/nf-nomad/src/main/nextflow/nomad/builders/JobBuilder.groovy @@ -0,0 +1,68 @@ +/* + * 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.builders + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import io.nomadproject.client.model.Job +import io.nomadproject.client.model.TaskGroup + +/** + * + * @author Abhinav Sharma + */ + +@Slf4j +@CompileStatic +class JobBuilder { + private Job job = new Job() + + JobBuilder id(String id) { + job.ID = id + return this + } + + JobBuilder name(String name) { + job.name = name + return this + } + + JobBuilder type(String type) { + job.type = type + return this + } + + JobBuilder datacenters(List datacenters) { + job.datacenters = datacenters + return this + } + + JobBuilder namespace(String namespace) { + job.namespace = namespace + return this + } + + JobBuilder taskGroups(List taskGroups) { + job.taskGroups = taskGroups + return this + } + + Job build() { + return job + } +} \ No newline at end of file diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/models/ConstraintsBuilder.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/builders/JobConstraintsBuilder.groovy similarity index 62% rename from plugins/nf-nomad/src/main/nextflow/nomad/models/ConstraintsBuilder.groovy rename to plugins/nf-nomad/src/main/nextflow/nomad/builders/JobConstraintsBuilder.groovy index 3275e19..daf10d3 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/models/ConstraintsBuilder.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/builders/JobConstraintsBuilder.groovy @@ -1,11 +1,35 @@ -package nextflow.nomad.models +/* + * 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.builders +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j import io.nomadproject.client.model.Constraint import nextflow.nomad.models.JobConstraintsAttr import nextflow.nomad.models.JobConstraintsNode import nextflow.nomad.models.JobConstraints +/** + * + * @author Abhinav Sharma +**/ -class ConstraintsBuilder { +@Slf4j +@CompileStatic +class JobConstraintsBuilder { static List constraintsSpecToList(JobConstraints spec){ def constraints = [] as List diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/builders/JobSpreadsBuilder.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/builders/JobSpreadsBuilder.groovy new file mode 100644 index 0000000..562457c --- /dev/null +++ b/plugins/nf-nomad/src/main/nextflow/nomad/builders/JobSpreadsBuilder.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.builders + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import io.nomadproject.client.model.Spread +import io.nomadproject.client.model.SpreadTarget +import nextflow.nomad.models.JobSpreads + +/** + * Nomad Job Spread Spec Builder + * + * @author Jorge Aguilera + */ + +@Slf4j +@CompileStatic +class JobSpreadsBuilder { + + static List spreadsSpecToList(JobSpreads spreads){ + def ret = [] as List + + spreads.raws.each{raw-> + def targets = [] as List + raw.right.each { + targets.add( new SpreadTarget(value: it.left, percent: it.right) ) + } + ret.add new Spread(attribute: raw.left, weight: raw.middle, spreadTarget: targets) + } + + return ret + } + + + static JobSpreads parseJobSpreads(Map nomadJobOpts){ + if( nomadJobOpts.spreads && nomadJobOpts.spreads instanceof Closure){ + def spec = new JobSpreads() + def closure = (nomadJobOpts.spreads as Closure) + def clone = closure.rehydrate(spec, closure.owner, closure.thisObject) + clone.resolveStrategy = Closure.DELEGATE_FIRST + clone() + spec.validate() + spec + } + } +} diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/builders/TaskBuilder.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/builders/TaskBuilder.groovy new file mode 100644 index 0000000..5314c86 --- /dev/null +++ b/plugins/nf-nomad/src/main/nextflow/nomad/builders/TaskBuilder.groovy @@ -0,0 +1,58 @@ +/* + * 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.builders + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import io.nomadproject.client.model.Task +import io.nomadproject.client.model.Resources + +@Slf4j +@CompileStatic +class TaskBuilder { + private Task task = new Task() + + TaskBuilder name(String name) { + task.name = name + return this + } + + TaskBuilder driver(String driver) { + task.driver = driver + return this + } + + TaskBuilder resources(Resources resources) { + task.resources = resources + return this + } + + TaskBuilder config(Map config) { + task.config = config + return this + } + + TaskBuilder env(Map env) { + task.env = env + return this + } + + Task build() { + return task + } +} + diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/builders/TaskGroupBuilder.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/builders/TaskGroupBuilder.groovy new file mode 100644 index 0000000..a040ee5 --- /dev/null +++ b/plugins/nf-nomad/src/main/nextflow/nomad/builders/TaskGroupBuilder.groovy @@ -0,0 +1,55 @@ +/* + * 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.builders + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import io.nomadproject.client.model.TaskGroup +import io.nomadproject.client.model.ReschedulePolicy +import io.nomadproject.client.model.RestartPolicy +import io.nomadproject.client.model.Task + +@Slf4j +@CompileStatic +class TaskGroupBuilder { + private TaskGroup taskGroup = new TaskGroup() + + TaskGroupBuilder name(String name) { + taskGroup.name = name + return this + } + + TaskGroupBuilder tasks(List tasks) { + taskGroup.tasks = tasks + return this + } + + TaskGroupBuilder reschedulePolicy(ReschedulePolicy policy) { + taskGroup.reschedulePolicy = policy + return this + } + + TaskGroupBuilder restartPolicy(RestartPolicy policy) { + taskGroup.restartPolicy = policy + return this + } + + TaskGroup build() { + return taskGroup + } +} \ No newline at end of file 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 f68bcdc..6c99e7d 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/config/NomadJobOpts.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/config/NomadJobOpts.groovy @@ -25,9 +25,15 @@ import nextflow.nomad.models.JobConstraints import nextflow.nomad.models.JobSpreads import nextflow.nomad.models.JobVolume +import static nextflow.nomad.builders.JobSpreadsBuilder.parseJobSpreads +import static nextflow.nomad.models.JobAffinity.parseJobAffinity +import static nextflow.nomad.models.JobConstraint.parseJobConstraint +import static nextflow.nomad.models.JobConstraints.parseJobConstraints +import static nextflow.nomad.models.JobSecrets.parseJobSecrets +import static nextflow.nomad.models.JobVolume.parseJobVolumes + /** - * Nomad JobOpts * * @author Jorge Aguilera * @author Abhinav Sharma @@ -82,113 +88,18 @@ class NomadJobOpts{ log.info "dockerVolume config will be deprecated, use volume type:'docker' name:'name' instead" } - this.volumeSpec = parseVolumes(nomadJobOpts) - this.affinitySpec = parseAffinity(nomadJobOpts) - this.constraintSpec = parseConstraint(nomadJobOpts) - this.constraintsSpec = parseConstraints(nomadJobOpts) - this.secretOpts = parseSecrets(nomadJobOpts) - this.spreadsSpec = parseSpreads(nomadJobOpts) + this.volumeSpec = parseJobVolumes(nomadJobOpts) + this.affinitySpec = parseJobAffinity(nomadJobOpts) + this.constraintSpec = parseJobConstraint(nomadJobOpts) + this.constraintsSpec = parseJobConstraints(nomadJobOpts) + this.secretOpts = parseJobSecrets(nomadJobOpts) + this.spreadsSpec = parseJobSpreads(nomadJobOpts) } - JobVolume[] parseVolumes(Map nomadJobOpts){ - List ret = [] - if( nomadJobOpts.volume && nomadJobOpts.volume instanceof Closure){ - def volumeSpec = new JobVolume() - def closure = (nomadJobOpts.volume as Closure) - def clone = closure.rehydrate(volumeSpec, closure.owner, closure.thisObject) - clone.resolveStrategy = Closure.DELEGATE_FIRST - clone() - volumeSpec.workDir(true) - ret.add volumeSpec - } - - if( nomadJobOpts.volumes && nomadJobOpts.volumes instanceof List){ - nomadJobOpts.volumes.each{ closure -> - if( closure instanceof Closure){ - def volumeSpec = new JobVolume() - def clone = closure.rehydrate(volumeSpec, closure.owner, closure.thisObject) - clone.resolveStrategy = Closure.DELEGATE_FIRST - clone() - ret.add volumeSpec - } - } - } - if( ret.size() && !ret.find{ it.workDir } ){ - ret.first().workDir(true) - } - ret*.validate() - if( ret.findAll{ it.workDir}.size() > 1 ){ - throw new IllegalArgumentException("No more than a workdir volume allowed") - } - return ret as JobVolume[] - } - 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 JobAffinity() - def closure = (nomadJobOpts.affinity as Closure) - def clone = closure.rehydrate(affinitySpec, closure.owner, closure.thisObject) - clone.resolveStrategy = Closure.DELEGATE_FIRST - clone() - affinitySpec.validate() - affinitySpec - } else { - null - } - } - 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 JobConstraint() - def closure = (nomadJobOpts.constraint as Closure) - def clone = closure.rehydrate(constraintSpec, closure.owner, closure.thisObject) - clone.resolveStrategy = Closure.DELEGATE_FIRST - clone() - constraintSpec.validate() - constraintSpec - } else { - null - } - } - - JobConstraints parseConstraints(Map nomadJobOpts){ - if (nomadJobOpts.constraints && nomadJobOpts.constraints instanceof Closure) { - def constraintsSpec = new JobConstraints() - 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 - } - } - - NomadSecretOpts parseSecrets(Map nomadJobOpts){ - if (nomadJobOpts.secrets && nomadJobOpts.secrets instanceof Map) { - def secretOpts = new NomadSecretOpts(nomadJobOpts.secrets as Map) - secretOpts - }else{ - null - } - } - - JobSpreads parseSpreads(Map nomadJobOpts){ - if( nomadJobOpts.spreads && nomadJobOpts.spreads instanceof Closure){ - def spec = new JobSpreads() - def closure = (nomadJobOpts.spreads as Closure) - def clone = closure.rehydrate(spec, closure.owner, closure.thisObject) - clone.resolveStrategy = Closure.DELEGATE_FIRST - clone() - spec.validate() - spec - } - } } diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/config/NomadSecretOpts.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/config/NomadSecretOpts.groovy index ee6749e..3cadb75 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/config/NomadSecretOpts.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/config/NomadSecretOpts.groovy @@ -1,5 +1,33 @@ +/* + * 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 groovy.transform.CompileStatic +import groovy.util.logging.Slf4j + +/** + * + * @author Jorge Aguilera + * @author Abhinav Sharma + */ + +@Slf4j +@CompileStatic class NomadSecretOpts { final Boolean enabled 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 42b25a6..d9151f3 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/executor/NomadService.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/executor/NomadService.groovy @@ -24,12 +24,15 @@ import io.nomadproject.client.ApiException import io.nomadproject.client.api.JobsApi import io.nomadproject.client.api.VariablesApi import io.nomadproject.client.model.* -import nextflow.nomad.models.ConstraintsBuilder +import nextflow.nomad.builders.JobBuilder +import nextflow.nomad.builders.JobConstraintsBuilder +import nextflow.nomad.builders.TaskBuilder +import nextflow.nomad.builders.TaskGroupBuilder import nextflow.nomad.models.JobConstraints import nextflow.nomad.config.NomadConfig import nextflow.nomad.models.JobSpreads import nextflow.nomad.models.JobVolume -import nextflow.nomad.models.SpreadsBuilder +import nextflow.nomad.builders.JobSpreadsBuilder import nextflow.processor.TaskRun import nextflow.util.MemoryUnit import nextflow.exception.ProcessSubmitException @@ -40,6 +43,7 @@ import java.nio.file.Path * Nomad Service * * @author Abhinav Sharma + * @author Jorge Aguilera */ @Slf4j @@ -90,15 +94,16 @@ class NomadService implements Closeable{ void close() throws IOException { } - String submitTask(String id, TaskRun task, List args, Mapenv, Path saveJsonPath=null){ - Job job = new Job(); - job.ID = id - job.name = task.name - job.type = "batch" - job.datacenters = this.config.jobOpts().datacenters - job.namespace = this.config.jobOpts().namespace - job.taskGroups = [createTaskGroup(task, args, env)] + String submitTask(String id, TaskRun task, List args, Map env, Path saveJsonPath = null) { + Job job = new JobBuilder() + .id(id) + .name(task.name) + .type("batch") + .datacenters(this.config.jobOpts().datacenters) + .namespace(this.config.jobOpts().namespace) + .taskGroups([createTaskGroup(task, args, env)]) + .build() assignDatacenters(task, job) spreads(task, job) @@ -106,66 +111,39 @@ class NomadService implements Closeable{ JobRegisterRequest jobRegisterRequest = new JobRegisterRequest() jobRegisterRequest.setJob(job) - if( saveJsonPath ) try { + if (saveJsonPath) try { saveJsonPath.text = job.toString() - } - catch( Exception e ) { + } catch (Exception e) { log.debug "WARN: unable to save request json -- cause: ${e.message ?: e}" } - try { JobRegisterResponse jobRegisterResponse = jobsApi.registerJob(jobRegisterRequest, config.jobOpts().region, config.jobOpts().namespace, null, null) - jobRegisterResponse.evalID - } catch( ApiException apiException){ + return jobRegisterResponse.evalID + } catch (ApiException apiException) { log.debug("[NOMAD] Failed to submit ${job.name} -- Cause: ${apiException.responseBody ?: apiException}", apiException) throw new ProcessSubmitException("[NOMAD] Failed to submit ${job.name} -- Cause: ${apiException.responseBody ?: apiException}", apiException) } catch (Throwable e) { log.debug("[NOMAD] Failed to submit ${job.name} -- Cause: ${e.message ?: e}", e) throw new ProcessSubmitException("[NOMAD] Failed to submit ${job.name} -- Cause: ${e.message ?: e}", e) } - } - TaskGroup createTaskGroup(TaskRun taskRun, List args, Mapenv){ - final ReschedulePolicy taskReschedulePolicy = new ReschedulePolicy().attempts(this.config.jobOpts().rescheduleAttempts) - final RestartPolicy taskRestartPolicy = new RestartPolicy().attempts(this.config.jobOpts().restartAttempts) + TaskGroup createTaskGroup(TaskRun taskRun, List args, Map env) { + final ReschedulePolicy taskReschedulePolicy = new ReschedulePolicy().attempts(this.config.jobOpts().rescheduleAttempts) + final RestartPolicy taskRestartPolicy = new RestartPolicy().attempts(this.config.jobOpts().restartAttempts) def task = createTask(taskRun, args, env) - def taskGroup = new TaskGroup( - name: "group", - tasks: [ task ], - reschedulePolicy: taskReschedulePolicy, - restartPolicy: taskRestartPolicy - ) - - - if( config.jobOpts().volumeSpec ) { - taskGroup.volumes = [:] - config.jobOpts().volumeSpec.eachWithIndex { volumeSpec , idx-> - if (volumeSpec && volumeSpec.type == JobVolume.VOLUME_CSI_TYPE) { - taskGroup.volumes["vol_${idx}".toString()] = new VolumeRequest( - type: volumeSpec.type, - source: volumeSpec.name, - attachmentMode: volumeSpec.attachmentMode, - accessMode: volumeSpec.accessMode, - readOnly: volumeSpec.readOnly, - ) - } - - if (volumeSpec && volumeSpec.type == JobVolume.VOLUME_HOST_TYPE) { - taskGroup.volumes["vol_${idx}".toString()] = new VolumeRequest( - type: volumeSpec.type, - source: volumeSpec.name, - readOnly: volumeSpec.readOnly, - ) - } - } - } - return taskGroup + return new TaskGroupBuilder() + .name("group") + .tasks([task]) + .reschedulePolicy(taskReschedulePolicy) + .restartPolicy(taskRestartPolicy) + .build() } - Task createTask(TaskRun task, List args, Mapenv) { + + Task createTask(TaskRun task, List args, Map env) { final DRIVER = "docker" final DRIVER_PRIVILEGED = true @@ -173,29 +151,23 @@ class NomadService implements Closeable{ final workingDir = task.workDir.toAbsolutePath().toString() final taskResources = getResources(task) - - def taskDef = new Task( - name: "nf-task", - driver: DRIVER, - resources: taskResources, - config: [ + return new TaskBuilder() + .name("nf-task") + .driver(DRIVER) + .resources(taskResources) + .config([ image : imageName, privileged: DRIVER_PRIVILEGED, work_dir : workingDir, command : args.first(), args : args.tail(), - ] as Map, - env: env, - ) - - volumes(task, taskDef, workingDir) - affinity(task, taskDef) - constraint(task, taskDef) - constraints(task, taskDef) - secrets(task, taskDef) - return taskDef + ] as Map) + .env(env) + .build() } + + protected Task volumes(TaskRun task, Task taskDef, String workingDir){ if( config.jobOpts().dockerVolume){ String destinationDir = workingDir.split(File.separator).dropRight(2).join(File.separator) @@ -264,7 +236,7 @@ class NomadService implements Closeable{ def constraints = [] as List if( config.jobOpts().constraintsSpec ){ - def list = ConstraintsBuilder.constraintsSpecToList(config.jobOpts().constraintsSpec) + def list = JobConstraintsBuilder.constraintsSpecToList(config.jobOpts().constraintsSpec) constraints.addAll(list) } @@ -272,7 +244,7 @@ class NomadService implements Closeable{ task.processor?.config?.get(TaskDirectives.CONSTRAINTS) instanceof Closure) { Closure closure = task.processor?.config?.get(TaskDirectives.CONSTRAINTS) as Closure JobConstraints constraintsSpec = JobConstraints.parse(closure) - def list = ConstraintsBuilder.constraintsSpecToList(constraintsSpec) + def list = JobConstraintsBuilder.constraintsSpecToList(constraintsSpec) constraints.addAll(list) } @@ -319,7 +291,7 @@ class NomadService implements Closeable{ protected Job spreads(TaskRun task, Job jobDef){ def spreads = [] as List if( config.jobOpts().spreadsSpec ){ - def list = SpreadsBuilder.spreadsSpecToList(config.jobOpts().spreadsSpec) + def list = JobSpreadsBuilder.spreadsSpecToList(config.jobOpts().spreadsSpec) spreads.addAll(list) } if( task.processor?.config?.get(TaskDirectives.SPREAD) && @@ -327,7 +299,7 @@ class NomadService implements Closeable{ Map map = task.processor?.config?.get(TaskDirectives.SPREAD) as Map JobSpreads spreadSpec = new JobSpreads() spreadSpec.spread(map) - def list = SpreadsBuilder.spreadsSpecToList(spreadSpec) + def list = JobSpreadsBuilder.spreadsSpecToList(spreadSpec) spreads.addAll(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 473975b..4cccc96 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/executor/TaskDirectives.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/executor/TaskDirectives.groovy @@ -1,5 +1,16 @@ package nextflow.nomad.executor +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j + +/** + * + * @author Abhinav Sharma + * @author Jorge Aguilera + **/ + +@Slf4j +@CompileStatic class TaskDirectives { public static final String DATACENTERS = "datacenters" diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/models/JobAffinity.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobAffinity.groovy index 9e96584..47c87bc 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/models/JobAffinity.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobAffinity.groovy @@ -16,11 +16,18 @@ */ package nextflow.nomad.models + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j + /** * Nomad Job Affinity Spec * * @author Jorge Aguilera */ + +@Slf4j +@CompileStatic class JobAffinity { private String attribute @@ -64,6 +71,20 @@ class JobAffinity { this } + static JobAffinity parseJobAffinity(Map nomadJobOpts) { + if (nomadJobOpts.affinity && nomadJobOpts.affinity instanceof Closure) { + log.info "affinity config will be deprecated, use affinities closure instead" + def affinitySpec = new JobAffinity() + def closure = (nomadJobOpts.affinity as Closure) + def clone = closure.rehydrate(affinitySpec, closure.owner, closure.thisObject) + clone.resolveStrategy = Closure.DELEGATE_FIRST + clone() + affinitySpec.validate() + affinitySpec + } else { + null + } + } void validate(){ } } \ No newline at end of file diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraint.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraint.groovy index 22a57f1..a3f4202 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraint.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraint.groovy @@ -16,12 +16,19 @@ */ package nextflow.nomad.models + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j + /** * Nomad Job Constraint Spec * * @author Jorge Aguilera */ + +@Slf4j +@CompileStatic class JobConstraint { private String attribute @@ -55,6 +62,24 @@ class JobConstraint { this } + + + static JobConstraint parseJobConstraint(Map nomadJobOpts){ + if (nomadJobOpts.constraint && nomadJobOpts.constraint instanceof Closure) { + log.info "constraint config will be deprecated, use constraints closure instead" + def constraintSpec = new JobConstraint() + def closure = (nomadJobOpts.constraint as Closure) + def clone = closure.rehydrate(constraintSpec, closure.owner, closure.thisObject) + clone.resolveStrategy = Closure.DELEGATE_FIRST + clone() + constraintSpec.validate() + constraintSpec + } else { + null + } + } + + void validate(){ } } \ No newline at end of file diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraints.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraints.groovy index dff45d5..39c1268 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraints.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobConstraints.groovy @@ -58,4 +58,20 @@ class JobConstraints { constraintsSpec.validate() constraintsSpec } + + + static JobConstraints parseJobConstraints(Map nomadJobOpts) { + if (nomadJobOpts.constraints && nomadJobOpts.constraints instanceof Closure) { + def constraintsSpec = new JobConstraints() + 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 + } + } + } diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/models/JobSecrets.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobSecrets.groovy new file mode 100644 index 0000000..60fddb7 --- /dev/null +++ b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobSecrets.groovy @@ -0,0 +1,45 @@ +/* + * 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 + +import nextflow.nomad.config.NomadSecretOpts +import org.apache.commons.lang3.tuple.Pair +import org.apache.commons.lang3.tuple.Triple + +/** + * Nomad Job Secrets Spec + * + * @author Abhinav Sharma + */ + +class JobSecrets { + + static NomadSecretOpts parseJobSecrets(Map nomadJobOpts) { + if (nomadJobOpts.secrets && nomadJobOpts.secrets instanceof Map) { + def secretOpts = new NomadSecretOpts(nomadJobOpts.secrets as Map) + secretOpts + } else { + null + } + } + + void validate() { + + } +} diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/models/JobVolume.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobVolume.groovy index 70d3c80..1c98ce5 100644 --- a/plugins/nf-nomad/src/main/nextflow/nomad/models/JobVolume.groovy +++ b/plugins/nf-nomad/src/main/nextflow/nomad/models/JobVolume.groovy @@ -16,11 +16,19 @@ */ package nextflow.nomad.models + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j + /** * Nomad Volume Spec * * @author Jorge Aguilera */ + + +@Slf4j +@CompileStatic class JobVolume { final static public String VOLUME_DOCKER_TYPE = "docker" @@ -114,4 +122,42 @@ class JobVolume { throw new IllegalArgumentException("WorkingDir Volume can't be readOnly") } } + + static JobVolume[] parseJobVolumes(Map nomadJobOpts){ + List ret = [] + if( nomadJobOpts.volume && nomadJobOpts.volume instanceof Closure){ + def volumeSpec = new JobVolume() + def closure = (nomadJobOpts.volume as Closure) + def clone = closure.rehydrate(volumeSpec, closure.owner, closure.thisObject) + clone.resolveStrategy = Closure.DELEGATE_FIRST + clone() + volumeSpec.workDir(true) + ret.add volumeSpec + } + + if( nomadJobOpts.volumes && nomadJobOpts.volumes instanceof List){ + nomadJobOpts.volumes.each{ closure -> + if( closure instanceof Closure){ + def volumeSpec = new JobVolume() + def clone = closure.rehydrate(volumeSpec, closure.owner, closure.thisObject) + clone.resolveStrategy = Closure.DELEGATE_FIRST + clone() + ret.add volumeSpec + } + } + } + + if( ret.size() && !ret.find{ it.workDir } ){ + ret.first().workDir(true) + } + + ret*.validate() + + if( ret.findAll{ it.workDir}.size() > 1 ){ + throw new IllegalArgumentException("No more than a workdir volume allowed") + } + + return ret as JobVolume[] + } + } diff --git a/plugins/nf-nomad/src/main/nextflow/nomad/models/SpreadsBuilder.groovy b/plugins/nf-nomad/src/main/nextflow/nomad/models/SpreadsBuilder.groovy deleted file mode 100644 index 8bb601d..0000000 --- a/plugins/nf-nomad/src/main/nextflow/nomad/models/SpreadsBuilder.groovy +++ /dev/null @@ -1,30 +0,0 @@ -package nextflow.nomad.models - -import groovy.transform.CompileStatic -import io.nomadproject.client.model.Spread -import io.nomadproject.client.model.SpreadTarget - -/** - * Nomad Job Spread Spec Builder - * - * @author Jorge Aguilera - */ - -@CompileStatic -class SpreadsBuilder { - - static List spreadsSpecToList(JobSpreads spreads){ - def ret = [] as List - - spreads.raws.each{raw-> - def targets = [] as List - raw.right.each { - targets.add( new SpreadTarget(value: it.left, percent: it.right) ) - } - ret.add new Spread(attribute: raw.left, weight: raw.middle, spreadTarget: targets) - } - - return ret - } - -}