Skip to content

Commit e85940e

Browse files
abhi18avjagedn
andauthored
Iteration on Nomad/Variables <-> Nextflow/Secrets (#75)
* first iteration in secrets implementation Signed-off-by: Jorge Aguilera <[email protected]> * tweak the start-nomad script for different platforms [ci skip] * nomad secrets, add get set and list commands Signed-off-by: Jorge Aguilera <[email protected]> * delete nomad with stop command [ci skip] * tweak the sun-nomadlab config to accommodate variables [ci skip] * update the sun-nomadlab config for config level secret vars [ci skip] * enable nomad secrets via nextflow config if false use Local implementation Signed-off-by: Jorge Aguilera <[email protected]> * add test Signed-off-by: Jorge Aguilera <[email protected]> * fix small bug Signed-off-by: Jorge Aguilera <[email protected]> * improve local testing env [ci skip] * test with sun-nomadlab and localsecretstore [ci skip] * rename enable -> enabled to comply with standard * use the functional config for sun-nomadlab [ci skip] * update authors [ci skip] --------- Signed-off-by: Jorge Aguilera <[email protected]> Co-authored-by: Jorge Aguilera <[email protected]>
1 parent db95a44 commit e85940e

File tree

17 files changed

+497
-19
lines changed

17 files changed

+497
-19
lines changed

buildSrc/src/main/groovy/nextflow/gradle/plugins/GenerateIdxTask.groovy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ abstract class GenerateIdxTask extends DefaultTask{
3434

3535
def matcher = new SourcesMatcher(project)
3636
def extensionsClassName = matcher.pluginExtensions
37+
extensionsClassName += matcher.providers
3738
def traceClassName = matcher.traceObservers
3839
def all = extensionsClassName+traceClassName
3940
output.text = all.join('\n')

buildSrc/src/main/groovy/nextflow/gradle/plugins/SourcesMatcher.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ class SourcesMatcher {
2121
findSources(/class (\w+) extends Executor implements ExtensionPoint/)
2222
}
2323

24+
List<String> getProviders(){
25+
return findSources(/implements SecretsProvider/)
26+
}
27+
28+
2429
List<String> getTraceObservers(){
2530
return findSources(/class (\w+) implements TraceObserverFactory/)
2631
}

plugins/nf-nomad/src/main/nextflow/nomad/NomadPlugin.groovy

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,50 @@
1818
package nextflow.nomad
1919

2020
import groovy.transform.CompileStatic
21+
import groovy.util.logging.Slf4j
22+
import nextflow.cli.PluginAbstractExec
23+
import nextflow.nomad.secrets.NomadSecretCmd
2124
import nextflow.nomad.executor.TaskDirectives
2225
import nextflow.plugin.BasePlugin
2326
import nextflow.script.ProcessConfig
27+
import nextflow.secret.SecretsLoader
2428
import org.pf4j.PluginWrapper
2529

2630
/**
2731
* Nextflow plugin for Nomad executor
2832
*
2933
* @author Abhinav Sharma <[email protected]>
30-
* @author : matthdsm <[email protected]>
34+
* @author Jorge Aguilera <[email protected]>
3135
*/
3236
@CompileStatic
33-
class NomadPlugin extends BasePlugin {
37+
@Slf4j
38+
class NomadPlugin extends BasePlugin implements PluginAbstractExec{
3439

3540
NomadPlugin(PluginWrapper wrapper) {
3641
super(wrapper)
3742
addCustomDirectives()
43+
SecretsLoader.instance.reset()
3844
}
3945

4046
private static void addCustomDirectives() {
4147
ProcessConfig.DIRECTIVES.addAll(TaskDirectives.ALL)
4248
}
49+
50+
@Override
51+
List<String> getCommands() {
52+
return ['secrets']
53+
}
54+
55+
@Override
56+
int exec(String cmd, List<String> args) {
57+
return switch (cmd){
58+
case 'secrets'-> secrets(args.first(), args.drop(1))
59+
default -> -1
60+
}
61+
}
62+
63+
int secrets(String action, List<String>args){
64+
NomadSecretCmd nomadSecretCmd = new NomadSecretCmd()
65+
nomadSecretCmd.runCommand( session.config , action, args)
66+
}
4367
}

plugins/nf-nomad/src/main/nextflow/nomad/config/NomadJobOpts.groovy

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ class NomadJobOpts{
4747

4848
JobConstraints constraintsSpec
4949

50+
NomadSecretOpts secretOpts
51+
5052
NomadJobOpts(Map nomadJobOpts, Map<String,String> env=null){
5153
assert nomadJobOpts!=null
5254

@@ -76,6 +78,7 @@ class NomadJobOpts{
7678
this.affinitySpec = parseAffinity(nomadJobOpts)
7779
this.constraintSpec = parseConstraint(nomadJobOpts)
7880
this.constraintsSpec = parseConstraints(nomadJobOpts)
81+
this.secretOpts = parseSecrets(nomadJobOpts)
7982
}
8083

8184
JobVolume[] parseVolumes(Map nomadJobOpts){
@@ -158,4 +161,14 @@ class NomadJobOpts{
158161
null
159162
}
160163
}
164+
165+
NomadSecretOpts parseSecrets(Map nomadJobOpts){
166+
if (nomadJobOpts.secrets && nomadJobOpts.secrets instanceof Map) {
167+
def secretOpts = new NomadSecretOpts(nomadJobOpts.secrets as Map)
168+
secretOpts
169+
}else{
170+
null
171+
}
172+
}
173+
161174
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package nextflow.nomad.config
2+
3+
class NomadSecretOpts {
4+
5+
final Boolean enabled
6+
final String path
7+
8+
NomadSecretOpts(Map map){
9+
this.enabled = map.containsKey('enabled') ? map.get('enabled') as boolean : false
10+
this.path = map.path ?: "secrets/nf-nomad"
11+
}
12+
13+
}

plugins/nf-nomad/src/main/nextflow/nomad/executor/NomadService.groovy

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ package nextflow.nomad.executor
2020
import groovy.transform.CompileStatic
2121
import groovy.util.logging.Slf4j
2222
import io.nomadproject.client.ApiClient
23+
import io.nomadproject.client.ApiException
2324
import io.nomadproject.client.api.JobsApi
25+
import io.nomadproject.client.api.VariablesApi
2426
import io.nomadproject.client.model.*
2527
import nextflow.nomad.models.ConstraintsBuilder
2628
import nextflow.nomad.models.JobConstraints
@@ -43,8 +45,9 @@ import java.nio.file.Path
4345
class NomadService implements Closeable{
4446

4547
NomadConfig config
46-
48+
ApiClient apiClient
4749
JobsApi jobsApi
50+
VariablesApi variablesApi
4851

4952
NomadService(NomadConfig config) {
5053
this.config = config
@@ -54,7 +57,7 @@ class NomadService implements Closeable{
5457
final READ_TIMEOUT_MILLISECONDS = 60000
5558
final WRITE_TIMEOUT_MILLISECONDS = 60000
5659

57-
ApiClient apiClient = new ApiClient( connectTimeout: CONNECTION_TIMEOUT_MILLISECONDS, readTimeout: READ_TIMEOUT_MILLISECONDS, writeTimeout: WRITE_TIMEOUT_MILLISECONDS)
60+
apiClient = new ApiClient( connectTimeout: CONNECTION_TIMEOUT_MILLISECONDS, readTimeout: READ_TIMEOUT_MILLISECONDS, writeTimeout: WRITE_TIMEOUT_MILLISECONDS)
5861
apiClient.basePath = config.clientOpts().address
5962
log.debug "[NOMAD] Client Address: ${config.clientOpts().address}"
6063

@@ -63,6 +66,7 @@ class NomadService implements Closeable{
6366
apiClient.apiKey = config.clientOpts().token
6467
}
6568
this.jobsApi = new JobsApi(apiClient)
69+
this.variablesApi = new VariablesApi(apiClient)
6670
}
6771

6872
protected Resources getResources(TaskRun task) {
@@ -110,6 +114,9 @@ class NomadService implements Closeable{
110114
try {
111115
JobRegisterResponse jobRegisterResponse = jobsApi.registerJob(jobRegisterRequest, config.jobOpts().region, config.jobOpts().namespace, null, null)
112116
jobRegisterResponse.evalID
117+
} catch( ApiException apiException){
118+
log.debug("[NOMAD] Failed to submit ${job.name} -- Cause: ${apiException.responseBody ?: apiException}", apiException)
119+
throw new ProcessSubmitException("[NOMAD] Failed to submit ${job.name} -- Cause: ${apiException.responseBody ?: apiException}", apiException)
113120
} catch (Throwable e) {
114121
log.debug("[NOMAD] Failed to submit ${job.name} -- Cause: ${e.message ?: e}", e)
115122
throw new ProcessSubmitException("[NOMAD] Failed to submit ${job.name} -- Cause: ${e.message ?: e}", e)
@@ -186,7 +193,7 @@ class NomadService implements Closeable{
186193
affinity(task, taskDef)
187194
constraint(task, taskDef)
188195
constraints(task, taskDef)
189-
196+
secrets(task, taskDef)
190197
return taskDef
191198
}
192199

@@ -276,7 +283,21 @@ class NomadService implements Closeable{
276283
taskDef
277284
}
278285

279-
286+
protected Task secrets(TaskRun task, Task taskDef){
287+
if( config.jobOpts()?.secretOpts?.enabled) {
288+
def secrets = task.processor?.config?.get(TaskDirectives.SECRETS)
289+
if (secrets) {
290+
Template template = new Template(envvars: true, destPath: "/secrets/nf-nomad")
291+
String secretPath = config.jobOpts()?.secretOpts?.path
292+
String tmpl = secrets.collect { String name ->
293+
"${name}={{ with nomadVar \"$secretPath/${name}\" }}{{ .${name} }}{{ end }}"
294+
}.join('\n').stripIndent()
295+
template.embeddedTmpl(tmpl)
296+
taskDef.addTemplatesItem(template)
297+
}
298+
}
299+
taskDef
300+
}
280301

281302
protected Job assignDatacenters(TaskRun task, Job job){
282303
def datacenters = task.processor?.config?.get(TaskDirectives.DATACENTERS)
@@ -365,4 +386,49 @@ class NomadService implements Closeable{
365386
throw new ProcessSubmitException("[NOMAD] Failed to get alloactions ${jobId} -- Cause: ${e.message ?: e}", e)
366387
}
367388
}
389+
390+
String getVariableValue(String key){
391+
getVariableValue(config.jobOpts().secretOpts?.path, key)
392+
}
393+
394+
String getVariableValue(String path, String key){
395+
var variable = variablesApi.getVariableQuery("$path/$key",
396+
config.jobOpts().region,
397+
config.jobOpts().namespace,
398+
null, null, null, null, null, null, null)
399+
variable?.items?.find{ it.key == key }?.value
400+
}
401+
402+
void setVariableValue(String key, String value){
403+
setVariableValue(config.jobOpts().secretOpts?.path, key, value)
404+
}
405+
406+
void setVariableValue(String path, String key, String value){
407+
var content = Map.of(key,value)
408+
var variable = new Variable(path: path, items: content)
409+
variablesApi.postVariable("$path/$key", variable,
410+
config.jobOpts().region,
411+
config.jobOpts().namespace,
412+
null, null, null)
413+
}
414+
415+
List<String> getVariablesList(){
416+
var listRequest = variablesApi.getVariablesListRequest(
417+
config.jobOpts().region,
418+
config.jobOpts().namespace,
419+
null, null, null, null, null, null, null)
420+
listRequest.collect{ it.path}
421+
}
422+
423+
void deleteVariable(String key){
424+
deleteVariable(config.jobOpts().secretOpts?.path, key)
425+
}
426+
427+
void deleteVariable(String path, String key){
428+
var variable = new Variable( items: Map.of(key, ""))
429+
variablesApi.deleteVariable("$path/$key", variable,
430+
config.jobOpts().region,
431+
config.jobOpts().namespace,
432+
null, null, null)
433+
}
368434
}

plugins/nf-nomad/src/main/nextflow/nomad/executor/TaskDirectives.groovy

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ class TaskDirectives {
66

77
public static final String CONSTRAINTS = "constraints"
88

9+
public static final String SECRETS = "secret"
10+
911
public static final List<String> ALL = [
1012
DATACENTERS,
11-
CONSTRAINTS
13+
CONSTRAINTS,
14+
SECRETS
1215
]
1316
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package nextflow.nomad.secrets
2+
3+
import groovy.util.logging.Slf4j
4+
import nextflow.exception.AbortOperationException
5+
import nextflow.nomad.config.NomadConfig
6+
import nextflow.nomad.executor.NomadService
7+
import nextflow.plugin.Priority
8+
import nextflow.secret.Secret
9+
import nextflow.secret.SecretImpl
10+
import nextflow.secret.SecretsProvider
11+
12+
@Slf4j
13+
class NomadSecretCmd {
14+
15+
protected NomadService service
16+
protected NomadConfig nomadConfig
17+
18+
int runCommand(Map config, String action, List<String> args){
19+
nomadConfig = new NomadConfig((config.nomad ?: Collections.emptyMap()) as Map)
20+
service = new NomadService(nomadConfig)
21+
return switch (action){
22+
case 'get' ->execGetSecretNames(args.removeAt(0).toString())
23+
case 'set' ->execSetSecretNames(args.removeAt(0).toString(),args.removeAt(0).toString())
24+
case 'list'->execListSecretsNames()
25+
case 'delete'->execDeleteSecretNames(args.removeAt(0).toString())
26+
default -> -1
27+
}
28+
}
29+
30+
int execListSecretsNames(){
31+
def list = listSecretsNames()
32+
println list.join('\n')
33+
return 0
34+
}
35+
36+
int execGetSecretNames(String name){
37+
if(!name){
38+
throw new AbortOperationException("Wrong number of arguments")
39+
}
40+
def secret = getSecret(name)
41+
println secret
42+
return 0
43+
}
44+
45+
int execSetSecretNames(String name, String value){
46+
if(!name){
47+
throw new AbortOperationException("Wrong number of arguments")
48+
}
49+
setSecret(name, value)
50+
return 0
51+
}
52+
53+
int execDeleteSecretNames(String name){
54+
if(!name){
55+
throw new AbortOperationException("Wrong number of arguments")
56+
}
57+
deleteSecret(name)
58+
return 0
59+
}
60+
61+
String getSecret(String name) {
62+
String value = service.getVariableValue(name)
63+
if( !value )
64+
throw new AbortOperationException("Missing secret name")
65+
value
66+
}
67+
68+
Set<String> listSecretsNames() {
69+
service.variablesList
70+
}
71+
72+
void setSecret(String name, String value) {
73+
service.setVariableValue(name, value)
74+
}
75+
76+
void deleteSecret(String name){
77+
service.deleteVariable(name)
78+
}
79+
}

0 commit comments

Comments
 (0)