Skip to content

Commit 171e140

Browse files
committed
feat(nf-seqera): filter autoLabels to selected workflow-metadata fields [ci fast]
Widen seqera.executor.autoLabels to accept a list or comma-separated string of short names (e.g. ['runName','projectName']) in addition to true/false. Valid names: projectName, userName, runName, sessionId, resume, revision, commitId, repository, manifestName, runtimeVersion, workflowId. Unknown names throw IllegalArgumentException at config parse time. true still emits all labels; false/null/empty remain disabled. Labels.withWorkflowMetadata gains a 2-arg overload that filters by the given include set; the single-arg form delegates with the full set so existing call sites and tests are unchanged. Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
1 parent 29a4340 commit 171e140

5 files changed

Lines changed: 212 additions & 20 deletions

File tree

plugins/nf-seqera/src/main/io/seqera/config/ExecutorOpts.groovy

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ import nextflow.util.Duration
3333
@CompileStatic
3434
class ExecutorOpts implements ConfigScope {
3535

36+
static final Set<String> VALID_AUTO_LABELS = Collections.unmodifiableSet(new LinkedHashSet<>([
37+
'projectName', 'userName', 'runName', 'sessionId', 'resume',
38+
'revision', 'commitId', 'repository', 'manifestName',
39+
'runtimeVersion', 'workflowId'
40+
]))
41+
3642
final RetryOpts retryPolicy
3743

3844
@ConfigOption
@@ -73,10 +79,17 @@ class ExecutorOpts implements ConfigScope {
7379

7480
@ConfigOption
7581
@Description("""
76-
When `true`, automatically adds workflow metadata labels (e.g. project name,
77-
run name, session ID) with the `nextflow.io/` prefix to the session (default: `false`).
82+
Automatically attach workflow metadata labels (with the `nextflow.io/` and
83+
`seqera.io/platform/` prefixes) to the session. Accepts:
84+
- `true`: include all available metadata labels
85+
- `false` (default): disable
86+
- a list or comma-separated string of short names: e.g.
87+
`['runName', 'projectName']` or `'runName,projectName'`
88+
Valid names: `projectName`, `userName`, `runName`, `sessionId`, `resume`,
89+
`revision`, `commitId`, `repository`, `manifestName`, `runtimeVersion`,
90+
`workflowId`.
7891
""")
79-
final boolean autoLabels
92+
final Set<String> autoLabels
8093

8194
@ConfigOption
8295
@Description("""
@@ -119,7 +132,7 @@ class ExecutorOpts implements ConfigScope {
119132
: Duration.of('1 sec')
120133
// machine requirement settings
121134
this.machineRequirement = new MachineRequirementOpts(opts.machineRequirement as Map ?: Map.of())
122-
this.autoLabels = opts.autoLabels as boolean ?: false
135+
this.autoLabels = parseAutoLabels(opts.get('autoLabels'))
123136
// prediction model
124137
this.predictionModel = opts.predictionModel as String ?: null
125138
// custom task environment variables
@@ -156,10 +169,28 @@ class ExecutorOpts implements ConfigScope {
156169
return machineRequirement
157170
}
158171

159-
boolean getAutoLabels() {
172+
Set<String> getAutoLabels() {
160173
return autoLabels
161174
}
162175

176+
protected static Set<String> parseAutoLabels(Object value) {
177+
if( value == null || value == false )
178+
return Collections.<String>emptySet()
179+
if( value == true )
180+
return VALID_AUTO_LABELS
181+
List<String> raw
182+
if( value instanceof CharSequence )
183+
raw = value.toString().tokenize(',').collect { String s -> s.trim() }.findAll { String s -> s }
184+
else if( value instanceof List )
185+
raw = ((List) value).collect { it?.toString()?.trim() }.findAll { String s -> s } as List<String>
186+
else
187+
throw new IllegalArgumentException("Invalid 'seqera.executor.autoLabels' value '${value}' - expected true, false, a list, or a comma-separated string")
188+
final invalid = raw.findAll { String s -> !(s in VALID_AUTO_LABELS) }
189+
if( invalid )
190+
throw new IllegalArgumentException("Invalid 'seqera.executor.autoLabels' name(s) ${invalid} - valid names are: ${VALID_AUTO_LABELS.join(', ')}")
191+
return Collections.unmodifiableSet(new LinkedHashSet<>(raw))
192+
}
193+
163194
String getPredictionModel() {
164195
return predictionModel
165196
}

plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,32 +33,50 @@ import nextflow.script.WorkflowMetadata
3333
@CompileStatic
3434
class Labels {
3535

36+
static final Set<String> ALL_AUTO_LABELS = Collections.unmodifiableSet(new LinkedHashSet<>([
37+
'projectName', 'userName', 'runName', 'sessionId', 'resume',
38+
'revision', 'commitId', 'repository', 'manifestName',
39+
'runtimeVersion', 'workflowId'
40+
]))
41+
3642
private final Map<String,String> entries = new LinkedHashMap<>(20)
3743

3844
/**
39-
* Add {@code nextflow.io/*} labels from workflow metadata
45+
* Add all {@code nextflow.io/*} and {@code seqera.io/platform/*} labels
46+
* derived from workflow metadata.
4047
*/
4148
Labels withWorkflowMetadata(WorkflowMetadata workflow) {
42-
if( workflow.projectName )
49+
return withWorkflowMetadata(workflow, ALL_AUTO_LABELS)
50+
}
51+
52+
/**
53+
* Add workflow metadata labels filtered by the {@code include} set of
54+
* short names (e.g. {@code 'runName'}). Unknown names are ignored; the
55+
* caller is expected to validate membership upstream.
56+
*/
57+
Labels withWorkflowMetadata(WorkflowMetadata workflow, Set<String> include) {
58+
if( !include ) return this
59+
if( include.contains('projectName') && workflow.projectName )
4360
entries.put('nextflow.io/projectName', workflow.projectName)
44-
if( workflow.userName )
61+
if( include.contains('userName') && workflow.userName )
4562
entries.put('nextflow.io/userName', workflow.userName)
46-
if( workflow.runName )
63+
if( include.contains('runName') && workflow.runName )
4764
entries.put('nextflow.io/runName', workflow.runName)
48-
if( workflow.sessionId )
65+
if( include.contains('sessionId') && workflow.sessionId )
4966
entries.put('nextflow.io/sessionId', workflow.sessionId.toString())
50-
entries.put('nextflow.io/resume', String.valueOf(workflow.resume))
51-
if( workflow.revision )
67+
if( include.contains('resume') )
68+
entries.put('nextflow.io/resume', String.valueOf(workflow.resume))
69+
if( include.contains('revision') && workflow.revision )
5270
entries.put('nextflow.io/revision', workflow.revision)
53-
if( workflow.commitId )
71+
if( include.contains('commitId') && workflow.commitId )
5472
entries.put('nextflow.io/commitId', workflow.commitId)
55-
if( workflow.repository )
73+
if( include.contains('repository') && workflow.repository )
5674
entries.put('nextflow.io/repository', workflow.repository)
57-
if( workflow.manifest?.name )
75+
if( include.contains('manifestName') && workflow.manifest?.name )
5876
entries.put('nextflow.io/manifestName', workflow.manifest.name)
59-
if( NextflowMeta.instance.version )
77+
if( include.contains('runtimeVersion') && NextflowMeta.instance.version )
6078
entries.put('nextflow.io/runtimeVersion', NextflowMeta.instance.version.toString())
61-
if( workflow.platform?.workflowId )
79+
if( include.contains('workflowId') && workflow.platform?.workflowId )
6280
entries.put('seqera.io/platform/workflowId', workflow.platform.workflowId)
6381
return this
6482
}

plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ class SeqeraExecutor extends Executor implements ExtensionPoint {
120120
computeRunResourceLabels()
121121
final labels = new Labels()
122122
if( seqeraConfig.autoLabels )
123-
labels.withWorkflowMetadata(session.workflowMetadata)
123+
labels.withWorkflowMetadata(session.workflowMetadata, seqeraConfig.autoLabels)
124124
labels.withProcessResourceLabels(runResourceLabels)
125125
final predictionModel = seqeraConfig.predictionModel ? PredictionModel.fromValue(seqeraConfig.predictionModel) : null
126126
final pipeline = new PipelineSpec()

plugins/nf-seqera/src/test/io/seqera/config/ExecutorOptsTest.groovy

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,106 @@ class ExecutorOptsTest extends Specification {
128128
config.machineRequirement.provisioning == 'spot'
129129
}
130130

131-
def 'should enable auto labels' () {
131+
def 'should enable all auto labels when set to true' () {
132132
when:
133133
def config = new ExecutorOpts([
134134
endpoint: 'https://sched.example.com',
135135
autoLabels: true
136136
])
137137

138138
then:
139-
config.autoLabels
139+
config.autoLabels == ExecutorOpts.VALID_AUTO_LABELS
140+
}
141+
142+
def 'should disable auto labels when set to false' () {
143+
when:
144+
def config = new ExecutorOpts([
145+
endpoint: 'https://sched.example.com',
146+
autoLabels: false
147+
])
148+
149+
then:
150+
config.autoLabels.isEmpty()
151+
}
152+
153+
def 'should accept auto labels as a list of short names' () {
154+
when:
155+
def config = new ExecutorOpts([
156+
endpoint: 'https://sched.example.com',
157+
autoLabels: ['runName', 'projectName']
158+
])
159+
160+
then:
161+
config.autoLabels == ['runName', 'projectName'] as Set
162+
}
163+
164+
def 'should trim whitespace in auto labels list entries' () {
165+
when:
166+
def config = new ExecutorOpts([
167+
endpoint: 'https://sched.example.com',
168+
autoLabels: [' runName', 'projectName ']
169+
])
170+
171+
then:
172+
config.autoLabels == ['runName', 'projectName'] as Set
173+
}
174+
175+
def 'should accept auto labels as a comma-separated string' () {
176+
when:
177+
def config = new ExecutorOpts([
178+
endpoint: 'https://sched.example.com',
179+
autoLabels: 'runName,projectName,workflowId'
180+
])
181+
182+
then:
183+
config.autoLabels == ['runName', 'projectName', 'workflowId'] as Set
184+
}
185+
186+
def 'should tolerate whitespace around comma-separated auto labels' () {
187+
when:
188+
def config = new ExecutorOpts([
189+
endpoint: 'https://sched.example.com',
190+
autoLabels: 'runName, projectName ,workflowId'
191+
])
192+
193+
then:
194+
config.autoLabels == ['runName', 'projectName', 'workflowId'] as Set
195+
}
196+
197+
def 'should treat empty auto labels list as disabled' () {
198+
when:
199+
def config = new ExecutorOpts([
200+
endpoint: 'https://sched.example.com',
201+
autoLabels: []
202+
])
203+
204+
then:
205+
config.autoLabels.isEmpty()
206+
}
207+
208+
def 'should treat empty auto labels string as disabled' () {
209+
when:
210+
def config = new ExecutorOpts([
211+
endpoint: 'https://sched.example.com',
212+
autoLabels: ''
213+
])
214+
215+
then:
216+
config.autoLabels.isEmpty()
217+
}
218+
219+
def 'should reject unknown auto labels name' () {
220+
when:
221+
new ExecutorOpts([
222+
endpoint: 'https://sched.example.com',
223+
autoLabels: ['runName', 'foo']
224+
])
225+
226+
then:
227+
def err = thrown(IllegalArgumentException)
228+
err.message.contains("'seqera.executor.autoLabels'")
229+
err.message.contains('foo')
230+
err.message.contains('valid names')
140231
}
141232

142233
def 'should create config with prediction model' () {

plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,58 @@ class LabelsTest extends Specification {
164164
!labels.entries.containsKey('seqera.io/platform/workflowId')
165165
}
166166

167+
def 'should emit only included workflow metadata labels'() {
168+
given:
169+
def workflow = Mock(WorkflowMetadata) {
170+
getProjectName() >> 'nf-core/rnaseq'
171+
getRunName() >> 'crazy_darwin'
172+
getSessionId() >> UUID.randomUUID()
173+
isResume() >> false
174+
getRevision() >> '3.12.0'
175+
getManifest() >> new Manifest([name: 'nf-core/rnaseq'])
176+
}
177+
178+
when:
179+
def labels = new Labels()
180+
.withWorkflowMetadata(workflow, ['runName', 'revision'] as Set)
181+
182+
then:
183+
labels.entries.keySet() == ['nextflow.io/runName', 'nextflow.io/revision'] as Set
184+
}
185+
186+
def 'should emit only the workflowId label when filtered to workflowId'() {
187+
given:
188+
def workflow = Mock(WorkflowMetadata) {
189+
getProjectName() >> 'hello'
190+
getRunName() >> 'happy_turing'
191+
getSessionId() >> UUID.randomUUID()
192+
isResume() >> false
193+
getManifest() >> new Manifest([:])
194+
getPlatform() >> new PlatformMetadata('wf-abc123')
195+
}
196+
197+
when:
198+
def labels = new Labels()
199+
.withWorkflowMetadata(workflow, ['workflowId'] as Set)
200+
201+
then:
202+
labels.entries.keySet() == ['seqera.io/platform/workflowId'] as Set
203+
}
204+
205+
def 'should emit nothing for an empty include set'() {
206+
given:
207+
def workflow = Mock(WorkflowMetadata) {
208+
getProjectName() >> 'hello'
209+
}
210+
211+
when:
212+
def labels = new Labels()
213+
.withWorkflowMetadata(workflow, [] as Set)
214+
215+
then:
216+
labels.entries.isEmpty()
217+
}
218+
167219
def 'should add process resource labels coercing values to string'() {
168220
when:
169221
def labels = new Labels()

0 commit comments

Comments
 (0)