Skip to content

Commit 96d06fe

Browse files
pditommasoclaude
andcommitted
Add autoLabels setting and Labels helper for Seqera executor [ci skip]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
1 parent 9810fcc commit 96d06fe

File tree

6 files changed

+304
-2
lines changed

6 files changed

+304
-2
lines changed

docs/reference/config.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1512,8 +1512,11 @@ The following settings are available:
15121512
`seqera.executor.region`
15131513
: The AWS region for task execution (default: `'eu-central-1'`).
15141514

1515+
`seqera.executor.autoLabels`
1516+
: When `true`, automatically adds workflow metadata labels to the session with the `nextflow.io/` prefix (default: `false`). The following labels are added: `projectName`, `userName`, `runName`, `sessionId`, `resume`, `revision`, `commitId`, `repository`, `manifestName`, `runtimeVersion`. A `seqera.io/runId` label is also added, computed as a SipHash of the session ID and run name.
1517+
15151518
`seqera.executor.labels`
1516-
: Custom labels to apply to AWS resources for cost tracking and resource organization. Labels are propagated to ECS tasks, capacity providers, and EC2 instances.
1519+
: Custom labels to apply to AWS resources for cost tracking and resource organization. Labels are propagated to ECS tasks, capacity providers, and EC2 instances. When used together with `autoLabels`, user-defined labels take precedence over auto-generated labels.
15171520

15181521
`seqera.executor.machineRequirement.arch`
15191522
: The CPU architecture for task execution, e.g. `'x86_64'` or `'arm64'`.

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ class ExecutorOpts implements ConfigScope {
7272
""")
7373
final Map<String, String> labels
7474

75+
@ConfigOption
76+
@Description("""
77+
When `true`, automatically adds workflow metadata labels (e.g. project name,
78+
run name, session ID) with the `nextflow.io/` prefix to the session (default: `false`).
79+
""")
80+
final boolean autoLabels
81+
7582
/* required by config scope -- do not remove */
7683

7784
ExecutorOpts() {}
@@ -91,6 +98,7 @@ class ExecutorOpts implements ConfigScope {
9198
this.machineRequirement = new MachineRequirementOpts(opts.machineRequirement as Map ?: Map.of())
9299
// labels for cost tracking
93100
this.labels = opts.labels as Map<String, String>
101+
this.autoLabels = opts.autoLabels as boolean ?: false
94102
}
95103

96104
RetryOpts retryOpts() {
@@ -120,4 +128,8 @@ class ExecutorOpts implements ConfigScope {
120128
Map<String, String> getLabels() {
121129
return labels
122130
}
131+
132+
boolean getAutoLabels() {
133+
return autoLabels
134+
}
123135
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2013-2025, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package io.seqera.executor
19+
20+
import com.google.common.hash.Hashing
21+
22+
import groovy.transform.CompileStatic
23+
import nextflow.NextflowMeta
24+
import nextflow.script.WorkflowMetadata
25+
26+
/**
27+
* Helper class to manage session labels.
28+
*
29+
* Builds the labels map from workflow metadata ({@code nextflow.io/*}),
30+
* scheduler metadata ({@code seqera:sched:*}), and user-configured labels.
31+
*
32+
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
33+
*/
34+
@CompileStatic
35+
class Labels {
36+
37+
private final Map<String,String> entries = new LinkedHashMap<>(20)
38+
39+
/**
40+
* Add {@code nextflow.io/*} labels from workflow metadata
41+
*/
42+
Labels withWorkflowMetadata(WorkflowMetadata workflow) {
43+
if( workflow.projectName )
44+
entries.put('nextflow.io/projectName', workflow.projectName)
45+
if( workflow.userName )
46+
entries.put('nextflow.io/userName', workflow.userName)
47+
if( workflow.runName )
48+
entries.put('nextflow.io/runName', workflow.runName)
49+
if( workflow.sessionId )
50+
entries.put('nextflow.io/sessionId', workflow.sessionId.toString())
51+
entries.put('nextflow.io/resume', String.valueOf(workflow.resume))
52+
if( workflow.sessionId && workflow.runName )
53+
entries.put('seqera.io/runId', runId(workflow.sessionId.toString(), workflow.runName))
54+
if( workflow.revision )
55+
entries.put('nextflow.io/revision', workflow.revision)
56+
if( workflow.commitId )
57+
entries.put('nextflow.io/commitId', workflow.commitId)
58+
if( workflow.repository )
59+
entries.put('nextflow.io/repository', workflow.repository)
60+
if( workflow.manifest?.name )
61+
entries.put('nextflow.io/manifestName', workflow.manifest.name)
62+
if( NextflowMeta.instance.version )
63+
entries.put('nextflow.io/runtimeVersion', NextflowMeta.instance.version.toString())
64+
return this
65+
}
66+
67+
/**
68+
* Add {@code seqera:sched:*} scheduler labels
69+
*/
70+
Labels withSchedSessionId(String sessionId) {
71+
if( sessionId )
72+
entries.put('seqera:sched:sessionId', sessionId)
73+
return this
74+
}
75+
76+
Labels withSchedClusterId(String clusterId) {
77+
if( clusterId )
78+
entries.put('seqera:sched:clusterId', clusterId)
79+
return this
80+
}
81+
82+
/**
83+
* Add user-configured labels. These take precedence over implicit labels.
84+
*/
85+
Labels withUserLabels(Map<String,String> labels) {
86+
if( labels )
87+
entries.putAll(labels)
88+
return this
89+
}
90+
91+
/**
92+
* @return all labels as an unmodifiable map
93+
*/
94+
Map<String,String> getEntries() {
95+
return Collections.unmodifiableMap(entries)
96+
}
97+
98+
/**
99+
* Compute a run identifier as SipHash of sessionId + runName
100+
*/
101+
protected static String runId(String sessionId, String runName) {
102+
return Hashing
103+
.sipHash24()
104+
.newHasher()
105+
.putUnencodedChars(sessionId)
106+
.putUnencodedChars(runName)
107+
.hash()
108+
.toString()
109+
}
110+
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,15 @@ class SeqeraExecutor extends Executor implements ExtensionPoint {
9090

9191
protected void createSession() {
9292
final towerConfig = session.config.tower as Map ?: Collections.emptyMap()
93+
final labels = new Labels()
94+
if( seqeraConfig.autoLabels )
95+
labels.withWorkflowMetadata(session.workflowMetadata)
96+
labels.withUserLabels(seqeraConfig.labels)
9397
final request = new CreateSessionRequest()
9498
.region(seqeraConfig.region)
9599
.name(session.runName)
96100
.machineRequirement(MapperUtil.toMachineRequirement(seqeraConfig.machineRequirement))
97-
.labels(seqeraConfig.labels)
101+
.labels(labels.entries)
98102
.workspaceId(PlatformHelper.getWorkspaceId(towerConfig, SysEnv.get()) as Long)
99103
log.debug "[SEQERA] Creating session: ${request}"
100104
final response = client.createSession(request)

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class ExecutorOptsTest extends Specification {
4848
config.machineRequirement != null
4949
config.machineRequirement.arch == null
5050
config.machineRequirement.provisioning == null
51+
!config.autoLabels
5152
}
5253

5354
def 'should create config with custom region' () {
@@ -163,4 +164,15 @@ class ExecutorOptsTest extends Specification {
163164
config.labels == [:]
164165
}
165166

167+
def 'should enable auto labels' () {
168+
when:
169+
def config = new ExecutorOpts([
170+
endpoint: 'https://sched.example.com',
171+
autoLabels: true
172+
])
173+
174+
then:
175+
config.autoLabels
176+
}
177+
166178
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Copyright 2013-2025, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package io.seqera.executor
19+
20+
import nextflow.NextflowMeta
21+
import nextflow.config.Manifest
22+
import nextflow.script.WorkflowMetadata
23+
import spock.lang.Specification
24+
25+
/**
26+
* Tests for Labels helper
27+
*
28+
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
29+
*/
30+
class LabelsTest extends Specification {
31+
32+
def 'should create labels with all workflow metadata'() {
33+
given:
34+
def sessionId = UUID.randomUUID()
35+
def workflow = Mock(WorkflowMetadata) {
36+
getProjectName() >> 'nf-core/rnaseq'
37+
getUserName() >> 'pditommaso'
38+
getRunName() >> 'crazy_darwin'
39+
getSessionId() >> sessionId
40+
isResume() >> true
41+
getRevision() >> '3.12.0'
42+
getCommitId() >> 'abc1234'
43+
getRepository() >> 'https://github.com/nf-core/rnaseq'
44+
getManifest() >> new Manifest([name: 'nf-core/rnaseq'])
45+
}
46+
47+
when:
48+
def labels = new Labels()
49+
.withWorkflowMetadata(workflow)
50+
51+
then:
52+
labels.entries['nextflow.io/projectName'] == 'nf-core/rnaseq'
53+
labels.entries['nextflow.io/userName'] == 'pditommaso'
54+
labels.entries['nextflow.io/runName'] == 'crazy_darwin'
55+
labels.entries['nextflow.io/sessionId'] == sessionId.toString()
56+
labels.entries['nextflow.io/resume'] == 'true'
57+
labels.entries['nextflow.io/revision'] == '3.12.0'
58+
labels.entries['nextflow.io/commitId'] == 'abc1234'
59+
labels.entries['nextflow.io/repository'] == 'https://github.com/nf-core/rnaseq'
60+
labels.entries['nextflow.io/manifestName'] == 'nf-core/rnaseq'
61+
labels.entries['nextflow.io/runtimeVersion'] == NextflowMeta.instance.version.toString()
62+
labels.entries['seqera.io/runId'] == Labels.runId(sessionId.toString(), 'crazy_darwin')
63+
}
64+
65+
def 'should compute stable runId from sessionId and runName'() {
66+
given:
67+
def sid = 'e2315a82-49b0-4langc3-a58a-0d7d52f7e3a1'
68+
def runName = 'crazy_darwin'
69+
70+
expect:
71+
Labels.runId(sid, runName) == Labels.runId(sid, runName)
72+
Labels.runId(sid, runName) != Labels.runId(sid, 'other_name')
73+
Labels.runId(sid, runName) != Labels.runId(UUID.randomUUID().toString(), runName)
74+
}
75+
76+
def 'should omit null workflow metadata from labels'() {
77+
given:
78+
def workflow = Mock(WorkflowMetadata) {
79+
getProjectName() >> 'hello'
80+
getUserName() >> 'user1'
81+
getRunName() >> 'happy_turing'
82+
getSessionId() >> UUID.randomUUID()
83+
isResume() >> false
84+
getRevision() >> null
85+
getCommitId() >> null
86+
getRepository() >> null
87+
getManifest() >> new Manifest([:])
88+
}
89+
90+
when:
91+
def labels = new Labels()
92+
.withWorkflowMetadata(workflow)
93+
94+
then:
95+
labels.entries.containsKey('nextflow.io/projectName')
96+
labels.entries.containsKey('nextflow.io/userName')
97+
labels.entries.containsKey('nextflow.io/runName')
98+
labels.entries.containsKey('nextflow.io/sessionId')
99+
labels.entries['nextflow.io/resume'] == 'false'
100+
!labels.entries.containsKey('nextflow.io/revision')
101+
!labels.entries.containsKey('nextflow.io/commitId')
102+
!labels.entries.containsKey('nextflow.io/repository')
103+
!labels.entries.containsKey('nextflow.io/manifestName')
104+
}
105+
106+
def 'should add scheduler labels'() {
107+
when:
108+
def labels = new Labels()
109+
.withSchedSessionId('sched-session-123')
110+
.withSchedClusterId('cluster-456')
111+
112+
then:
113+
labels.entries['seqera:sched:sessionId'] == 'sched-session-123'
114+
labels.entries['seqera:sched:clusterId'] == 'cluster-456'
115+
}
116+
117+
def 'should skip null scheduler labels'() {
118+
when:
119+
def labels = new Labels()
120+
.withSchedSessionId(null)
121+
.withSchedClusterId(null)
122+
123+
then:
124+
!labels.entries.containsKey('seqera:sched:sessionId')
125+
!labels.entries.containsKey('seqera:sched:clusterId')
126+
}
127+
128+
def 'should allow user labels to override implicit labels'() {
129+
given:
130+
def workflow = Mock(WorkflowMetadata) {
131+
getProjectName() >> 'hello'
132+
getUserName() >> 'user1'
133+
getRunName() >> 'happy_turing'
134+
getSessionId() >> UUID.randomUUID()
135+
isResume() >> false
136+
getManifest() >> new Manifest([:])
137+
}
138+
139+
when:
140+
def labels = new Labels()
141+
.withWorkflowMetadata(workflow)
142+
.withUserLabels([
143+
'nextflow.io/runName': 'custom_name',
144+
'team': 'research'
145+
])
146+
147+
then:
148+
labels.entries['nextflow.io/runName'] == 'custom_name'
149+
labels.entries['team'] == 'research'
150+
labels.entries['nextflow.io/projectName'] == 'hello'
151+
}
152+
153+
def 'should handle null user labels'() {
154+
when:
155+
def labels = new Labels()
156+
.withUserLabels(null)
157+
158+
then:
159+
labels.entries.isEmpty()
160+
}
161+
}

0 commit comments

Comments
 (0)