Skip to content

Commit e732f34

Browse files
committed
Add docs + rename ConfigNullProvider to EmptySecretProvider [ci fast]
Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
1 parent d00c892 commit e732f34

3 files changed

Lines changed: 152 additions & 49 deletions

File tree

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

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import nextflow.plugin.Plugins
4444
import nextflow.scm.AssetManager
4545
import nextflow.script.ScriptFile
4646
import nextflow.script.ScriptRunner
47-
import nextflow.secret.ConfigNullProvider
47+
import nextflow.secret.EmptySecretProvider
4848
import nextflow.secret.SecretsLoader
4949
import nextflow.util.CustomPoolFactory
5050
import nextflow.util.Duration
@@ -330,32 +330,57 @@ class CmdRun extends CmdBase implements HubOptions {
330330
final baseDir = scriptFile.parent
331331
final cliParams = parsedParams(ConfigBuilder.getConfigVars(baseDir, null))
332332

333-
// -- load config (without secrets)
334-
final secretsProvider = new ConfigNullProvider()
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()
335352
ConfigBuilder builder = new ConfigBuilder()
336353
.setOptions(launcher.options)
337354
.setCmdRun(this)
338355
.setBaseDir(scriptFile.parent)
339356
.setCliParams(cliParams)
340-
.setSecretsProvider(secretsProvider)
357+
.setSecretsProvider(secretsProvider) // Mock provider returns empty strings
341358
ConfigMap config = builder.build()
342359
Map configParams = builder.getConfigParams()
343360

344-
// -- load plugins
361+
// -- Load plugins (may register secret providers)
345362
Plugins.init()
346363
Plugins.load(config)
347364

348-
// -- load secrets provider
365+
// -- Initialize real secrets system
349366
SecretsLoader.getInstance().load()
350367

351-
// -- reload config if secrets were used
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
352376
if( secretsProvider.usedSecrets() ) {
353377
log.debug "Config file used secrets -- reloading config with secrets provider"
354378
builder = new ConfigBuilder()
355379
.setOptions(launcher.options)
356380
.setCmdRun(this)
357381
.setBaseDir(scriptFile.parent)
358382
.setCliParams(cliParams)
383+
// No .setSecretsProvider() - uses real secrets system now
359384
config = builder.build()
360385
configParams = builder.getConfigParams()
361386
}

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

Lines changed: 0 additions & 42 deletions
This file was deleted.
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+
}

0 commit comments

Comments
 (0)