Skip to content

Commit 9e4c975

Browse files
Improve secret loading documentation and rename ConfigNullProvider to EmptySecretProvider
This commit enhances the documentation and naming of Nextflow's 2-phase configuration loading strategy to better explain how plugin-provided secrets are handled during startup. ## Changes ### Renamed ConfigNullProvider → EmptySecretProvider - Better reflects the class purpose as a mock provider - Updated all references and imports - Maintains backward compatibility in functionality ### Enhanced Documentation - Added comprehensive JavaDoc with ASCII flow chart - Improved inline comments in CmdRun explaining the rationale - Clarified that this is a mock provider, not a secret detector ## 2-Phase Configuration Loading Strategy ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ PHASE 1 │ │ BETWEEN │ │ PHASE 2 │ │ (Mock Mode) │ │ PHASES │ │ (Real Mode) │ ├─────────────────┤ ├─────────────────┤ ├─────────────────┤ │ EmptySecretProv │ │ Load Plugins │ │ Real Secrets │ │ secrets → "" │ │ Load Secret │ │ secrets → value │ │ Config: fallback│ -> │ Providers │ -> │ Config: actual │ │ accessed=true │ │ │ │ values │ └─────────────────┘ └─────────────────┘ └─────────────────┘ ## Problem Solved Configuration files may reference secrets provided by plugins (e.g., AWS secrets), but plugins are loaded AFTER configuration parsing. The 2-phase approach: 1. **Phase 1**: Parse config with EmptySecretProvider (returns empty strings) 2. **Load plugins**: Initialize plugin-provided secret providers 3. **Phase 2**: Conditionally reload config with real secret values ## Key Benefits - No parsing failures (config always parses successfully) - Plugin compatibility (supports plugin-provided secret providers) - Performance optimization (only reloads when secrets are actually used) - Forces defensive configuration patterns Example defensive config pattern: ```groovy outputDir = secrets.MY_SECRET ? "results-${secrets.MY_SECRET}" : "results" This improvement makes the codebase more maintainable by clearly documenting the sophisticated secret loading mechanism that enables plugin-provided secrets to work seamlessly with Nextflow's configuration system. Signed-off-by: Ben Sherman <bentshermann@gmail.com> Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com> Co-authored-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
1 parent c68c1e9 commit 9e4c975

13 files changed

Lines changed: 256 additions & 28 deletions

File tree

docs/cli.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,10 @@ Or in JSON format:
286286
}
287287
```
288288

289+
:::{note}
290+
Parameter values in the params file can reference the following {ref}`built-in variables <config-constants>`: `baseDir`, `projectDir`, `launchDir`.
291+
:::
292+
289293
Parameters are applied in the following order (from lowest to highest priority):
290294

291295
1. Parameters defined in pipeline scripts (e.g. `main.nf`)

docs/config.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ Relative paths are resolved against the location of the including file.
9393
Config includes can also be specified within config blocks. However, config files should only be included at the top level or in a [profile](#config-profiles) so that the included config file is valid on its own and in the context in which it is included.
9494
:::
9595

96+
(config-constants)=
97+
9698
## Constants
9799

98100
The following constants are globally available in a Nextflow configuration file:

docs/developer/index.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,13 @@ cd tests/checks
137137
./qrun.sh
138138
```
139139

140+
To run a specific integration test:
141+
142+
```bash
143+
cd tests/checks
144+
./qrun.sh <FOLDER>
145+
```
146+
140147
To test the documentation snippets:
141148

142149
```bash

docs/secrets.md

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ This feature allows you to decouple the use of secrets in your pipelines from th
1313

1414
When a pipeline is launched, Nextflow injects the secrets into the run without leaking them into temporary execution files. Secrets are provided to tasks as environment variables.
1515

16+
Secrets can be used with the local executor and grid executors (e.g., Slurm or Grid Engine). Secrets can be used with the AWS Batch executor when launched from [Seqera Platform](https://seqera.io/blog/pipeline-secrets-secure-handling-of-sensitive-information-in-tower/).
17+
1618
## Command line
1719

1820
The Nextflow {ref}`cli-secrets` sub-command can be used to manage secrets:
@@ -45,9 +47,32 @@ aws {
4547
The above snippet accesses the secrets `MY_ACCESS_KEY` and `MY_SECRET_KEY` and assigns them to the corresponding AWS config settings.
4648

4749
:::{warning}
48-
Secrets cannot be assigned to pipeline parameters.
50+
Secrets should not be assigned to pipeline parameters, as they can be leaked by the pipeline.
51+
:::
52+
53+
:::{versionadded} 25.10.0
4954
:::
5055

56+
Nextflow supports the use of secrets provided by plugins (e.g., AWS secrets) in configuration. However, due to the way that plugins are loaded, there are specific considerations when using config secrets:
57+
58+
- **Initial config load**: Nextflow first loads the configuration _without_ secrets enabled. Any reference to a secret will return the empty string `''`.
59+
60+
- **Plugin resolution**: Plugins are resolved after the initial configuration load. This is because the configuration can specify additional plugins.
61+
62+
- **Config reloading**: If secrets are accessed during configuration and the initial load succeeds, Nextflow will reload the configuration with secrets enabled.
63+
64+
As a result, config secrets must be used in a way that does not cause the config resolution to fail when secrets are not present.
65+
66+
For example:
67+
68+
```groovy
69+
includeConfig secrets.MY_SECRET
70+
? "https://example.com/extra.config?secret=${secrets.MY_SECRET}"
71+
: '/dev/null'
72+
```
73+
74+
The above snippet includes a secured config only if the secret is present. Otherwise, it includes `/dev/null`, which is equivalent to including an empty file. The reference to `secrets.MY_SECRET` in the condition causes the config to be reloaded with secrets enabled, including secrets from plugins such as AWS secrets.
75+
5176
(secrets-pipeline-script)=
5277

5378
## Pipeline script
@@ -67,10 +92,6 @@ workflow {
6792
The above example is only meant to demonstrate how to access a secret, not how to use it. In practice, sensitive information should not be printed to the console or output files.
6893
:::
6994

70-
:::{note}
71-
Secrets can only be used with the local or grid executors (e.g., Slurm or Grid Engine). Secrets can be used with the AWS Batch executor when launched from [Seqera Platform](https://seqera.io/blog/pipeline-secrets-secure-handling-of-sensitive-information-in-tower/).
72-
:::
73-
7495
## Process directive
7596

7697
Secrets can be accesses by processes using the {ref}`process-secret` directive. For example:
@@ -92,7 +113,3 @@ In the above example, the secrets `MY_ACCESS_KEY` and `MY_SECRET_KEY` are inject
92113
:::{warning}
93114
Secrets are made available as environment variables in the process script. To prevent evaluation in the Nextflow script context, escape variable names with a backslash (e.g., `\$MY_ACCESS_KEY`) as shown above.
94115
:::
95-
96-
:::{note}
97-
Secrets can only be used with the local or grid executors (e.g., Slurm or Grid Engine). Secrets can be used with the AWS Batch executor when launched from [Seqera Platform](https://seqera.io/blog/pipeline-secrets-secure-handling-of-sensitive-information-in-tower/).
98-
:::

modules/nextflow/src/main/groovy/nextflow/cli/CmdRun.groovy

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ import nextflow.plugin.Plugins
4444
import nextflow.scm.AssetManager
4545
import nextflow.script.ScriptFile
4646
import nextflow.script.ScriptRunner
47+
import nextflow.secret.EmptySecretProvider
48+
import nextflow.secret.SecretsLoader
4749
import nextflow.util.CustomPoolFactory
4850
import nextflow.util.Duration
4951
import nextflow.util.HistoryFile
@@ -320,31 +322,72 @@ class CmdRun extends CmdBase implements HubOptions {
320322
checkRunName()
321323

322324
printBanner()
323-
Plugins.init()
324325

325-
// -- specify the arguments
326+
// -- resolve main script
326327
final scriptFile = getScriptFile(pipeline)
327328

328329
// -- load command line params
329330
final baseDir = scriptFile.parent
330-
final cliParams = parsedParams(ConfigBuilder.getConfigVars(baseDir))
331+
final cliParams = parsedParams(ConfigBuilder.getConfigVars(baseDir, null))
332+
333+
/*
334+
* 2-PHASE CONFIGURATION LOADING STRATEGY
335+
*
336+
* Problem: Configuration files may reference secrets provided by plugins (e.g., AWS secrets),
337+
* but plugins are loaded AFTER configuration parsing. This creates a chicken-and-egg problem:
338+
* - Config parsing needs secret values to complete
339+
* - Plugin loading needs config to determine which plugins to load
340+
* - Secret providers are registered by plugins
341+
*
342+
* Solution: Parse configuration twice when secrets are referenced
343+
*
344+
* PHASE 1: Parse config with EmptySecretProvider (returns "" for all secrets)
345+
* - Configuration must use defensive patterns: secrets.FOO ? "value-${secrets.FOO}" : "fallback"
346+
* - Config parses successfully with fallback values
347+
* - EmptySecretProvider tracks if ANY secrets were accessed
348+
*/
349+
350+
// -- PHASE 1: Load config with mock secrets provider
351+
final secretsProvider = new EmptySecretProvider()
352+
ConfigBuilder builder = new ConfigBuilder()
353+
.setOptions(launcher.options)
354+
.setCmdRun(this)
355+
.setBaseDir(scriptFile.parent)
356+
.setCliParams(cliParams)
357+
.setSecretsProvider(secretsProvider) // Mock provider returns empty strings
358+
ConfigMap config = builder.build()
359+
Map configParams = builder.getConfigParams()
360+
361+
// -- Load plugins (may register secret providers)
362+
Plugins.init()
363+
Plugins.load(config)
331364

332-
// create the config object
333-
final builder = new ConfigBuilder()
365+
// -- Initialize real secrets system
366+
SecretsLoader.getInstance().load()
367+
368+
/*
369+
* PHASE 2: Conditionally reload config with real secrets
370+
* - Only reload if Phase 1 actually accessed any secrets
371+
* - This time, real secret providers are available (including plugin-provided ones)
372+
* - Same config expressions now resolve with actual secret values
373+
*/
374+
375+
// -- PHASE 2: Reload config if secrets were used in Phase 1
376+
if( secretsProvider.usedSecrets() ) {
377+
log.debug "Config file used secrets -- reloading config with secrets provider"
378+
builder = new ConfigBuilder()
334379
.setOptions(launcher.options)
335380
.setCmdRun(this)
336-
.setBaseDir(baseDir)
381+
.setBaseDir(scriptFile.parent)
337382
.setCliParams(cliParams)
338-
final config = builder.build()
339-
final configParams = builder.getConfigParams()
383+
// No .setSecretsProvider() - uses real secrets system now
384+
config = builder.build()
385+
configParams = builder.getConfigParams()
386+
}
340387

341388
// check DSL syntax in the config
342389
launchInfo(config, scriptFile)
343390

344-
// -- load plugins
345-
final cfg = plugins ? [plugins: plugins.tokenize(',')] : config
346-
Plugins.load(cfg)
347-
348391
// -- validate config options
349392
if( NF.isSyntaxParserV2() )
350393
new ConfigValidator().validate(config)

modules/nextflow/src/main/groovy/nextflow/config/ConfigBuilder.groovy

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import nextflow.cli.CmdRun
3434
import nextflow.exception.AbortOperationException
3535
import nextflow.exception.ConfigParseException
3636
import nextflow.secret.SecretsLoader
37+
import nextflow.secret.SecretsProvider
3738
import nextflow.util.HistoryFile
3839
import nextflow.util.SecretHelper
3940
/**
@@ -76,6 +77,8 @@ class ConfigBuilder {
7677

7778
boolean showMissingVariables
7879

80+
SecretsProvider secretsProvider
81+
7982
Map<ConfigObject, String> emptyVariables = new LinkedHashMap<>(10)
8083

8184
Map<String,String> env = new HashMap<>(SysEnv.get())
@@ -104,6 +107,11 @@ class ConfigBuilder {
104107
return this
105108
}
106109

110+
ConfigBuilder setSecretsProvider(SecretsProvider value) {
111+
this.secretsProvider = value
112+
return this
113+
}
114+
107115
ConfigBuilder setOptions( CliOptions options ) {
108116
this.options = options
109117
return this
@@ -337,17 +345,20 @@ class ConfigBuilder {
337345
// this is needed to make sure to reuse the same
338346
// instance of the config vars across different instances of the ConfigBuilder
339347
// and prevent multiple parsing of the same params file (which can even be remote resource)
340-
return getConfigVars(baseDir)
348+
final secretContext = secretsProvider
349+
? SecretsLoader.secretContext(secretsProvider)
350+
: SecretsLoader.secretContext()
351+
return getConfigVars(baseDir, secretContext)
341352
}
342353

343354
@Memoized
344-
static Map getConfigVars(Path base) {
355+
static Map getConfigVars(Path base, Object secretContext) {
345356
final binding = new HashMap(10)
346357
binding.put('baseDir', base)
347358
binding.put('projectDir', base)
348359
binding.put('launchDir', Paths.get('.').toRealPath())
349360
binding.put('outputDir', Paths.get('results').complete())
350-
binding.put('secrets', SecretsLoader.secretContext())
361+
binding.put('secrets', secretContext)
351362
return binding
352363
}
353364

@@ -560,6 +571,9 @@ class ConfigBuilder {
560571
if( cmdRun.preview )
561572
config.preview = cmdRun.preview
562573

574+
if( cmdRun.plugins )
575+
config.plugins = cmdRun.plugins.tokenize(',')
576+
563577
// -- sets the working directory
564578
if( cmdRun.workDir )
565579
config.workDir = cmdRun.workDir
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright 2013-2024, 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 nextflow.secret
19+
20+
import groovy.transform.CompileStatic
21+
22+
/**
23+
* A mock secrets provider that returns empty strings for all secret requests.
24+
*
25+
* <p>This provider is a critical component of Nextflow's 2-phase configuration loading
26+
* strategy, which solves the chicken-and-egg problem between configuration parsing
27+
* and plugin loading.
28+
*
29+
* <h2>The Problem</h2>
30+
* Configuration files may reference secrets provided by plugins (e.g., AWS secrets),
31+
* but plugins are loaded AFTER configuration parsing. This creates a dependency cycle:
32+
* <ul>
33+
* <li>Config parsing needs secret values to complete</li>
34+
* <li>Plugin loading needs config to determine which plugins to load</li>
35+
* <li>Secret providers are registered by plugins</li>
36+
* </ul>
37+
*
38+
* <h2>The Solution: 2-Phase Configuration Loading</h2>
39+
* <pre>
40+
* ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
41+
* │ PHASE 1 │ │ BETWEEN │ │ PHASE 2 │
42+
* │ (Mock Mode) │ │ PHASES │ │ (Real Mode) │
43+
* ├─────────────────┤ ├─────────────────┤ ├─────────────────┤
44+
* │ EmptySecretProv │ │ Load Plugins │ │ Real Secrets │
45+
* │ secrets → "" │ │ Load Secret │ │ secrets → value │
46+
* │ Config: fallback│ -> │ Providers │ -> │ Config: actual │
47+
* │ accessed=true │ │ │ │ values │
48+
* └─────────────────┘ └─────────────────┘ └─────────────────┘
49+
* </pre>
50+
*
51+
* <h3>Phase 1: Mock Configuration Loading</h3>
52+
* <ol>
53+
* <li>EmptySecretProvider is used as the secrets provider</li>
54+
* <li>Configuration is parsed normally</li>
55+
* <li>When {@code secrets.SOME_NAME} is referenced, this provider returns empty string</li>
56+
* <li>The {@code accessed} flag is set to {@code true}</li>
57+
* <li>Config must use defensive patterns: {@code secrets.FOO ? "value-${secrets.FOO}" : "fallback"}</li>
58+
* <li>Result: Configuration parses successfully with fallback values</li>
59+
* </ol>
60+
*
61+
* <h3>Between Phases: Plugin and Secrets Loading</h3>
62+
* <ol>
63+
* <li>Plugins are loaded using the successfully parsed configuration</li>
64+
* <li>Plugin-provided secrets providers are registered</li>
65+
* <li>The real secrets loading system is initialized</li>
66+
* </ol>
67+
*
68+
* <h3>Phase 2: Real Configuration Loading (Conditional)</h3>
69+
* <ol>
70+
* <li>Check if {@code usedSecrets()} returns {@code true}</li>
71+
* <li>If secrets were accessed in Phase 1, reload the entire configuration</li>
72+
* <li>This time, real secret providers are available</li>
73+
* <li>Same config expressions now resolve with actual secret values</li>
74+
* <li>Result: Configuration contains real secret values instead of fallbacks</li>
75+
* </ol>
76+
*
77+
* <h2>Example Usage</h2>
78+
* Given a config file:
79+
* <pre>
80+
* outputDir = secrets.MY_SECRET ? "results-${secrets.MY_SECRET}" : "results"
81+
* </pre>
82+
*
83+
* <b>Phase 1:</b> {@code secrets.MY_SECRET} returns {@code ""} → {@code outputDir = "results"}
84+
* <b>Phase 2:</b> {@code secrets.MY_SECRET} returns {@code "hello-world"} → {@code outputDir = "results-hello-world"}
85+
*
86+
* <h2>Key Benefits</h2>
87+
* <ul>
88+
* <li><b>No parsing failures:</b> Configuration always parses successfully</li>
89+
* <li><b>Plugin compatibility:</b> Supports plugin-provided secret providers</li>
90+
* <li><b>Performance:</b> Only reloads config when secrets are actually used</li>
91+
* <li><b>Defensive configs:</b> Forces robust configuration patterns</li>
92+
* </ul>
93+
*
94+
* <h2>Important Notes</h2>
95+
* <ul>
96+
* <li>This provider does NOT remember which specific secrets were accessed</li>
97+
* <li>It only tracks WHETHER any secrets were accessed at all</li>
98+
* <li>Configuration files MUST handle empty secret values gracefully</li>
99+
* <li>The 2-phase loading is transparent to the user</li>
100+
* </ul>
101+
*
102+
* @author Ben Sherman <bentshermann@gmail.com>
103+
* @see nextflow.cli.CmdRun#run()
104+
* @see nextflow.secret.SecretsLoader
105+
*/
106+
@CompileStatic
107+
class EmptySecretProvider extends NullProvider {
108+
109+
private boolean accessed
110+
111+
@Override
112+
Secret getSecret(String name) {
113+
accessed = true
114+
return new SecretImpl(name, '')
115+
}
116+
117+
boolean usedSecrets() {
118+
return accessed
119+
}
120+
}

modules/nextflow/src/main/groovy/nextflow/secret/SecretsLoader.groovy

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,9 @@ class SecretsLoader {
7878
final provider = isEnabled() ? getInstance().load() : new NullProvider()
7979
return makeSecretsContext(provider)
8080
}
81-
81+
82+
static Object secretContext(SecretsProvider provider) {
83+
return makeSecretsContext(provider)
84+
}
85+
8286
}

modules/nextflow/src/main/groovy/nextflow/trace/TraceObserverFactoryV2.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import org.pf4j.ExtensionPoint
2121
/**
2222
* Factory class for creating {@link TraceObserverV2} instances
2323
*
24-
* @author Ben Shermann <bentshermann@gmail.com>
24+
* @author Ben Sherman <bentshermann@gmail.com>
2525
*/
2626
interface TraceObserverFactoryV2 extends ExtensionPoint {
2727

0 commit comments

Comments
 (0)