diff --git a/plugins/nf-seqera/src/main/io/seqera/config/ExecutorOpts.groovy b/plugins/nf-seqera/src/main/io/seqera/config/ExecutorOpts.groovy index 1facffaa74..a0294dc29f 100644 --- a/plugins/nf-seqera/src/main/io/seqera/config/ExecutorOpts.groovy +++ b/plugins/nf-seqera/src/main/io/seqera/config/ExecutorOpts.groovy @@ -33,6 +33,12 @@ import nextflow.util.Duration @CompileStatic class ExecutorOpts implements ConfigScope { + static final Set VALID_AUTO_LABELS = Collections.unmodifiableSet(new LinkedHashSet<>([ + 'projectName', 'userName', 'runName', 'sessionId', 'resume', + 'revision', 'commitId', 'repository', 'manifestName', + 'runtimeVersion', 'workflowId' + ])) + final RetryOpts retryPolicy @ConfigOption @@ -73,10 +79,17 @@ class ExecutorOpts implements ConfigScope { @ConfigOption @Description(""" - When `true`, automatically adds workflow metadata labels (e.g. project name, - run name, session ID) with the `nextflow.io/` prefix to the session (default: `false`). + Automatically attach workflow metadata labels (with the `nextflow.io/` and + `seqera.io/platform/` prefixes) to the session. Accepts: + - `true`: include all available metadata labels + - `false` (default): disable + - a list or comma-separated string of short names: e.g. + `['runName', 'projectName']` or `'runName,projectName'` + Valid names: `projectName`, `userName`, `runName`, `sessionId`, `resume`, + `revision`, `commitId`, `repository`, `manifestName`, `runtimeVersion`, + `workflowId`. """) - final boolean autoLabels + final Set autoLabels @ConfigOption @Description(""" @@ -119,7 +132,7 @@ class ExecutorOpts implements ConfigScope { : Duration.of('1 sec') // machine requirement settings this.machineRequirement = new MachineRequirementOpts(opts.machineRequirement as Map ?: Map.of()) - this.autoLabels = opts.autoLabels as boolean ?: false + this.autoLabels = parseAutoLabels(opts.get('autoLabels')) // prediction model this.predictionModel = opts.predictionModel as String ?: null // custom task environment variables @@ -156,10 +169,28 @@ class ExecutorOpts implements ConfigScope { return machineRequirement } - boolean getAutoLabels() { + Set getAutoLabels() { return autoLabels } + protected static Set parseAutoLabels(Object value) { + if( value == null || value == false ) + return Collections.emptySet() + if( value == true ) + return VALID_AUTO_LABELS + List raw + if( value instanceof CharSequence ) + raw = value.toString().tokenize(',').collect { String s -> s.trim() }.findAll { String s -> s } + else if( value instanceof List ) + raw = ((List) value).collect { it?.toString()?.trim() }.findAll { String s -> s } as List + else + throw new IllegalArgumentException("Invalid 'seqera.executor.autoLabels' value '${value}' - expected true, false, a list, or a comma-separated string") + final invalid = raw.findAll { String s -> !(s in VALID_AUTO_LABELS) } + if( invalid ) + throw new IllegalArgumentException("Invalid 'seqera.executor.autoLabels' name(s) ${invalid} - valid names are: ${VALID_AUTO_LABELS.join(', ')}") + return Collections.unmodifiableSet(new LinkedHashSet<>(raw)) + } + String getPredictionModel() { return predictionModel } diff --git a/plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy b/plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy index 199814954c..762d7ecfa8 100644 --- a/plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy +++ b/plugins/nf-seqera/src/main/io/seqera/executor/Labels.groovy @@ -33,32 +33,50 @@ import nextflow.script.WorkflowMetadata @CompileStatic class Labels { + static final Set ALL_AUTO_LABELS = Collections.unmodifiableSet(new LinkedHashSet<>([ + 'projectName', 'userName', 'runName', 'sessionId', 'resume', + 'revision', 'commitId', 'repository', 'manifestName', + 'runtimeVersion', 'workflowId' + ])) + private final Map entries = new LinkedHashMap<>(20) /** - * Add {@code nextflow.io/*} labels from workflow metadata + * Add all {@code nextflow.io/*} and {@code seqera.io/platform/*} labels + * derived from workflow metadata. */ Labels withWorkflowMetadata(WorkflowMetadata workflow) { - if( workflow.projectName ) + return withWorkflowMetadata(workflow, ALL_AUTO_LABELS) + } + + /** + * Add workflow metadata labels filtered by the {@code include} set of + * short names (e.g. {@code 'runName'}). Unknown names are ignored; the + * caller is expected to validate membership upstream. + */ + Labels withWorkflowMetadata(WorkflowMetadata workflow, Set include) { + if( !include ) return this + if( include.contains('projectName') && workflow.projectName ) entries.put('nextflow.io/projectName', workflow.projectName) - if( workflow.userName ) + if( include.contains('userName') && workflow.userName ) entries.put('nextflow.io/userName', workflow.userName) - if( workflow.runName ) + if( include.contains('runName') && workflow.runName ) entries.put('nextflow.io/runName', workflow.runName) - if( workflow.sessionId ) + if( include.contains('sessionId') && workflow.sessionId ) entries.put('nextflow.io/sessionId', workflow.sessionId.toString()) - entries.put('nextflow.io/resume', String.valueOf(workflow.resume)) - if( workflow.revision ) + if( include.contains('resume') ) + entries.put('nextflow.io/resume', String.valueOf(workflow.resume)) + if( include.contains('revision') && workflow.revision ) entries.put('nextflow.io/revision', workflow.revision) - if( workflow.commitId ) + if( include.contains('commitId') && workflow.commitId ) entries.put('nextflow.io/commitId', workflow.commitId) - if( workflow.repository ) + if( include.contains('repository') && workflow.repository ) entries.put('nextflow.io/repository', workflow.repository) - if( workflow.manifest?.name ) + if( include.contains('manifestName') && workflow.manifest?.name ) entries.put('nextflow.io/manifestName', workflow.manifest.name) - if( NextflowMeta.instance.version ) + if( include.contains('runtimeVersion') && NextflowMeta.instance.version ) entries.put('nextflow.io/runtimeVersion', NextflowMeta.instance.version.toString()) - if( workflow.platform?.workflowId ) + if( include.contains('workflowId') && workflow.platform?.workflowId ) entries.put('seqera.io/platform/workflowId', workflow.platform.workflowId) return this } diff --git a/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy b/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy index bc09ae4a1e..663c8da391 100644 --- a/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy +++ b/plugins/nf-seqera/src/main/io/seqera/executor/SeqeraExecutor.groovy @@ -120,7 +120,7 @@ class SeqeraExecutor extends Executor implements ExtensionPoint { computeRunResourceLabels() final labels = new Labels() if( seqeraConfig.autoLabels ) - labels.withWorkflowMetadata(session.workflowMetadata) + labels.withWorkflowMetadata(session.workflowMetadata, seqeraConfig.autoLabels) labels.withProcessResourceLabels(runResourceLabels) final predictionModel = seqeraConfig.predictionModel ? PredictionModel.fromValue(seqeraConfig.predictionModel) : null final pipeline = new PipelineSpec() diff --git a/plugins/nf-seqera/src/test/io/seqera/config/ExecutorOptsTest.groovy b/plugins/nf-seqera/src/test/io/seqera/config/ExecutorOptsTest.groovy index 2d374b8a56..46afd4d532 100644 --- a/plugins/nf-seqera/src/test/io/seqera/config/ExecutorOptsTest.groovy +++ b/plugins/nf-seqera/src/test/io/seqera/config/ExecutorOptsTest.groovy @@ -128,7 +128,7 @@ class ExecutorOptsTest extends Specification { config.machineRequirement.provisioning == 'spot' } - def 'should enable auto labels' () { + def 'should enable all auto labels when set to true' () { when: def config = new ExecutorOpts([ endpoint: 'https://sched.example.com', @@ -136,7 +136,98 @@ class ExecutorOptsTest extends Specification { ]) then: - config.autoLabels + config.autoLabels == ExecutorOpts.VALID_AUTO_LABELS + } + + def 'should disable auto labels when set to false' () { + when: + def config = new ExecutorOpts([ + endpoint: 'https://sched.example.com', + autoLabels: false + ]) + + then: + config.autoLabels.isEmpty() + } + + def 'should accept auto labels as a list of short names' () { + when: + def config = new ExecutorOpts([ + endpoint: 'https://sched.example.com', + autoLabels: ['runName', 'projectName'] + ]) + + then: + config.autoLabels == ['runName', 'projectName'] as Set + } + + def 'should trim whitespace in auto labels list entries' () { + when: + def config = new ExecutorOpts([ + endpoint: 'https://sched.example.com', + autoLabels: [' runName', 'projectName '] + ]) + + then: + config.autoLabels == ['runName', 'projectName'] as Set + } + + def 'should accept auto labels as a comma-separated string' () { + when: + def config = new ExecutorOpts([ + endpoint: 'https://sched.example.com', + autoLabels: 'runName,projectName,workflowId' + ]) + + then: + config.autoLabels == ['runName', 'projectName', 'workflowId'] as Set + } + + def 'should tolerate whitespace around comma-separated auto labels' () { + when: + def config = new ExecutorOpts([ + endpoint: 'https://sched.example.com', + autoLabels: 'runName, projectName ,workflowId' + ]) + + then: + config.autoLabels == ['runName', 'projectName', 'workflowId'] as Set + } + + def 'should treat empty auto labels list as disabled' () { + when: + def config = new ExecutorOpts([ + endpoint: 'https://sched.example.com', + autoLabels: [] + ]) + + then: + config.autoLabels.isEmpty() + } + + def 'should treat empty auto labels string as disabled' () { + when: + def config = new ExecutorOpts([ + endpoint: 'https://sched.example.com', + autoLabels: '' + ]) + + then: + config.autoLabels.isEmpty() + } + + def 'should reject unknown auto labels name' () { + when: + new ExecutorOpts([ + endpoint: 'https://sched.example.com', + autoLabels: ['runName', 'foo'] + ]) + + then: + def err = thrown(IllegalArgumentException) + err.message.contains("'seqera.executor.autoLabels'") + err.message.contains('foo') + err.message.contains('valid names') } def 'should create config with prediction model' () { diff --git a/plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy b/plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy index 2339aef330..a618f02a3e 100644 --- a/plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy +++ b/plugins/nf-seqera/src/test/io/seqera/executor/LabelsTest.groovy @@ -164,6 +164,58 @@ class LabelsTest extends Specification { !labels.entries.containsKey('seqera.io/platform/workflowId') } + def 'should emit only included workflow metadata labels'() { + given: + def workflow = Mock(WorkflowMetadata) { + getProjectName() >> 'nf-core/rnaseq' + getRunName() >> 'crazy_darwin' + getSessionId() >> UUID.randomUUID() + isResume() >> false + getRevision() >> '3.12.0' + getManifest() >> new Manifest([name: 'nf-core/rnaseq']) + } + + when: + def labels = new Labels() + .withWorkflowMetadata(workflow, ['runName', 'revision'] as Set) + + then: + labels.entries.keySet() == ['nextflow.io/runName', 'nextflow.io/revision'] as Set + } + + def 'should emit only the workflowId label when filtered to workflowId'() { + given: + def workflow = Mock(WorkflowMetadata) { + getProjectName() >> 'hello' + getRunName() >> 'happy_turing' + getSessionId() >> UUID.randomUUID() + isResume() >> false + getManifest() >> new Manifest([:]) + getPlatform() >> new PlatformMetadata('wf-abc123') + } + + when: + def labels = new Labels() + .withWorkflowMetadata(workflow, ['workflowId'] as Set) + + then: + labels.entries.keySet() == ['seqera.io/platform/workflowId'] as Set + } + + def 'should emit nothing for an empty include set'() { + given: + def workflow = Mock(WorkflowMetadata) { + getProjectName() >> 'hello' + } + + when: + def labels = new Labels() + .withWorkflowMetadata(workflow, [] as Set) + + then: + labels.entries.isEmpty() + } + def 'should add process resource labels coercing values to string'() { when: def labels = new Labels()