---
title: Feature Flag Consolidation
version: v1alpha1
authors: Jean-Philippe Evrard
creation-date: 2026-01-05
status: draft
---
Consolidate the 76+ configuration points scattered across build tags, CLI flags, environment variables, and feature registration into a unified, documented configuration system. This proposal addresses broken registrations, deprecated dead code, and inconsistent patterns while maintaining full backward compatibility.
The External Secrets Operator has accumulated configuration sprawl across multiple mechanisms:
| Type | Count | Location |
|---|---|---|
| Build tags | 30+ | pkg/register/*.go |
| CLI flags (root) | 40 | cmd/controller/root.go |
| Feature-registered flags | 6 | Distributed across 5 packages |
| Environment variables | 2 | providers/v1/doppler/ |
| Total | around 80 | 6+ locations |
Critical issues identified:
InitializeFlags() function (doppler/provider.go:65-78) is never called, making --doppler-oidc-cache-size unavailable--experimental-enable-aws-session-cache flag is marked DEPRECATED but still registered (aws/auth/auth.go:68-74)enable-*, experimental-*, unsafe-* prefixes used inconsistentlyCurrent (broken):
// providers/v1/doppler/provider.go:65-78
func InitializeFlags() *feature.Feature { // Never called!
var dopplerOIDCCacheSize int
fs := pflag.NewFlagSet("doppler", pflag.ExitOnError)
fs.IntVar(&dopplerOIDCCacheSize, "doppler-oidc-cache-size", defaultCacheSize, "...")
return &feature.Feature{
Flags: fs,
Initialize: func() { initCache(dopplerOIDCCacheSize) },
}
}
Fixed:
// providers/v1/doppler/provider.go
func init() {
var dopplerOIDCCacheSize int
fs := pflag.NewFlagSet("doppler", pflag.ExitOnError)
fs.IntVar(&dopplerOIDCCacheSize, "doppler-oidc-cache-size", defaultCacheSize,
"Size of the Doppler OIDC token cache")
feature.Register(feature.Feature{
Flags: fs,
Initialize: func() { initCache(dopplerOIDCCacheSize) },
})
}
Current (dead code):
// providers/v1/aws/auth/auth.go:68-74
func init() {
fs := pflag.NewFlagSet("aws-auth", pflag.ExitOnError)
fs.BoolVar(&enableSessionCache, "experimental-enable-aws-session-cache", false,
"DEPRECATED: this flag is no longer used and will be removed...")
feature.Register(feature.Feature{Flags: fs})
}
Action: Remove entirely. The flag is documented as deprecated and the variable enableSessionCache is never read.
Establish a single pattern for all feature-registered flags:
// Standard pattern for provider-specific flags
func init() {
fs := pflag.NewFlagSet("<provider-name>", pflag.ExitOnError)
// Define flags
fs.TypeVar(&variable, "flag-name", defaultValue, "Description")
// Register with optional late initialization
feature.Register(feature.Feature{
Flags: fs,
Initialize: func() { /* optional setup */ },
})
}
Current state by provider:
| Provider | Pattern | Status |
|---|---|---|
| Vault | init() + feature.Register() |
Correct |
| AWS Auth | init() + feature.Register() |
Has deprecated flag |
| Doppler | InitializeFlags() (standalone) |
Broken - never called |
| Template | init() + feature.Register() |
Correct |
| StateManager | init() + feature.Register() |
Correct |
After Phase 2: All use init() + feature.Register() pattern.
Establish clear naming conventions for flags:
| Prefix | Meaning | Example |
|---|---|---|
experimental-* |
Experimental feature (may change) | --experimental-enable-vault-token-cache |
enable-* |
Core feature toggle (stable) | --enable-leader-election |
unsafe-* |
Potentially dangerous option | --unsafe-allow-generic-targets |
<provider>-* |
Provider-specific setting | --doppler-oidc-cache-size |
We do not have a policy when two or more are combined.
My suggestion is to take the previous table from top to bottom, and use them from left to right.
It means some flags could, in the future, look like this:
--experimental-enable-unsafe-vault-provider-specific-flag-name
When the flag would not be experimental anymore, it would become:
--enable-unsafe-vault-providerspecificflagname
Next to that, some internal features are NOT provider specific, but match the pattern:
--template-left-delimiter / --template-right-delimiter.
I suggest we keep them as is, as they are okayish (provider-style naming)
I propose the following:
Info:
SetNormalizeFunc to call a function to normalize flag names which would register a deprecated state (it will be useful in later phases)feature can then ensure flags are consistent with the maturity/safety level of the feature, until a compile time manner is implemented.Create docs/flags.md (or equivalent in documentation site) listing all flags:
# Configuration Reference
## Controller Flags
| Feature name | Flag | Default | Description |
|-----------------|----------------------------|------------|-----------------------------|
| Metrics | `--metrics-addr` | `:8080` | Metrics server bind address |
| Leader election | `--enable-leader-election` | `true` | Enable leader election |
| ... | ... | ... | |
## Provider Flags
### Vault
| Feature name | Flag | Default | Description |
|--------------|--------------------------------------------|----------|----------------------|
| Token Cache | `--experimental-enable-vault-token-cache` | `false` | Enable token caching |
| Token Cache | `--experimental-vault-token-cache-size` | `1000` | Cache size |
### Doppler
| Feature name | Flag | Default | Description |
|--------------|-----------------------------|----------|-----------------------|
| OIDC Cache | `--doppler-oidc-cache-size` | `1000` | OIDC token cache size |
Enhance the runtime/feature package to support introspection:
// runtime/feature/feature.go
type Feature struct {
Name string // Human-readable/understandable short name?
Flags *pflag.FlagSet
Initialize func()
Maturity Maturity // Experimental, Stable, Deprecated
Safety Safety // Safe, Unsafe
}
type Maturity int
type Safety int
const (
Experimental Maturity = iota
Stable
Deprecated
Unknown
)
const (
Unsafe Safety = iota
Safe
Unknown
)
// ListFeatures returns all registered features for documentation/help
func ListFeatures() []Feature {
return features
}
This enables:
--help output grouped by featureAs a contributor, I want a clear pattern for adding provider flags, so I don't repeat existing mistakes.
As a user, I want to see all available flags documented, so I can configure the operator correctly.
As an operator, I want --doppler-oidc-cache-size to actually work, so I can tune Doppler performance.
No CRD or API changes.
--doppler-oidc-cache-size becomes functional (currently silently ignored)--experimental-enable-aws-session-cache removed (was already non-functional)--experimental-enable-aws-session-cache will get "unknown flag" errorPhase 1 (Bug Fixes):
--doppler-oidc-cache-size flag is functionalPhase 2 (Standardization):
init() + feature.Register() patternInitializeFlags() patternPhase 3 (Naming):
Phase 4 (Documentation):
Phase 5 (Automation):
Rollout:
Support YAML/TOML configuration file instead of CLI flags.
# eso-config.yaml
controller:
metricsAddr: ":8080"
leaderElection: true
providers:
vault:
tokenCache:
enabled: true
size: 1000
Pros:
Cons:
Decision: Deferred - valuable but higher effort. CLI flags remain primary interface.
Accept the scattered configuration. No migration effort. We can fix the Doppler flag and AWS dead code. However,the contributors and our users will be confused about the maturity of the flags, and the docs might become outdated if we forget to update it.
design/007-provider-versioning-strategy: When providers become separate CRDs, their flags may move to CRD spec fields. The feature registration pattern supports this transition - flags can be deprecated in favor of CRD configuration.
design/006-LTS-release: Flag documentation is essential for LTS releases. Users need to know which flags are stable vs experimental when planning upgrades.