Configuration & viper integration
This guide covers how the CLI loads, reads, and writes its persistent
configuration (drconfig.yaml), and the rules contributors must follow when
adding new flags or persisted config keys.
Table of contents
- The viperx wrapper
- Where config lives
- How values reach viper
- Writing back to drconfig.yaml
- Rules for new flags
- Rules for new env vars
- Rules for new persisted keys
- Common pitfalls
The viperx wrapper
Outside of internal/config/..., the only viper entry point is
internal/config/viperx. Direct imports of github.com/spf13/viper
are blocked by depguard everywhere else in the tree.
// good
import "github.com/datarobot/cli/internal/config/viperx"
if viperx.GetBool("debug") { /* ... */ }
// blocked by golangci-lint outside internal/config/**
import "github.com/spf13/viper"
viperx re-exports only the safe subset of viper's API. The following
symbols are deliberately not re-exported:
viper.WriteConfig,viper.SafeWriteConfig— they serialize the entireviper.AllSettings()map, leaking transient flag state intodrconfig.yaml. Useconfig.UpdateConfigFile(...)instead, which only writes keys in theconfig.PersistableKeysallowlist.viper.BindPFlags(cmd.Flags())— bulk-binds every subcommand flag into viper, with the same leakage problem. Useviperx.BindPFlagfor individual persistent flags; read subcommand flags directly viacmd.Flags().GetX(...).
If you need a viper symbol that is not currently re-exported, add it to
internal/config/viperx/viperx.go consciously and document why. New
additions should be reviewed for whether they expand the leakage surface.
Where config lives
The CLI stores user configuration in a single YAML file:
- Default location:
$XDG_CONFIG_HOME/datarobot/drconfig.yaml(falls back to~/.config/datarobot/drconfig.yaml) - Override with
--config <path>orDATAROBOT_CLI_CONFIG=<path>
Today this file holds connection credentials and a small set of sticky CLI preferences. It is not a dumping ground for transient flag state.
How values reach viper
Viper resolves a key from these sources in priority order:
- Explicit
viperx.Set(key, value)call (e.g. after a successful login) - Flag bound via
viperx.BindPFlag(key, flag)(only persistent root flags today — see below) - Environment variable bound via
viperx.BindEnv(key, "DATAROBOT_…")or auto-mapped viaviperx.SetEnvPrefix("DATAROBOT_CLI") - Value loaded from
drconfig.yaml - Default registered via
viperx.SetDefault
Persistent root flags bound to viper
Only the persistent root flags listed in cmd/root.go::init() are bound
explicitly with viperx.BindPFlag. We do not bulk-bind subcommand
flags (and viperx does not even expose a BindPFlags function), because
that would slurp every subcommand flag (such as --yes, --if-needed)
into viper.AllSettings() and risk leaking transient flag state into
drconfig.yaml.
Subcommand flags
Read subcommand flag values directly from cobra:
If a subcommand flag also needs an environment variable override, register
the env var only with viperx.BindEnv(...) and merge the two sources
explicitly in your handler:
// cmd/dotenv/cmd.go
_ = viperx.BindEnv("yes", "DATAROBOT_CLI_NON_INTERACTIVE")
// In RunE:
yesFlag, _ := cmd.Flags().GetBool("yes")
yes := yesFlag || viperx.GetBool("yes")
This keeps the explicit --yes flag value out of viper.AllSettings()
while preserving env-var support.
Writing back to drconfig.yaml
Use the allowlisted writer in internal/config:
// Write all allowlisted keys that are currently set in viper:
config.UpdateConfigFile()
// Or write only specific keys (recommended when the call site knows
// exactly what changed):
config.UpdateConfigFile(config.DataRobotURL)
config.UpdateConfigFile(config.DataRobotAPIKey, config.DataRobotURL)
UpdateConfigFile reads the existing YAML, overlays only the allowlisted
keys (config.PersistableKeys), and writes the result back. Any other
viper state — including transient flags such as --yes, --verbose,
--debug — is intentionally dropped.
The wrappers in the auth package (auth.WriteConfigFileSilent,
auth.WriteConfigFile) call this writer under the hood.
Rules for new flags
When adding a new flag, decide which category it falls into:
| Category | Bind to viper? | Persist to drconfig.yaml? |
|---|---|---|
Transient (per-invocation, e.g. --yes, --all) |
No | No |
Sticky preference (e.g. --external-editor) |
Yes (root only) | Yes — add to PersistableKeys |
Connection credential (e.g. --token) |
Yes | Yes |
For transient flags:
- Define with
cmd.Flags().Bool(...) - Read with
cmd.Flags().GetBool(...) - Do not call
viperx.BindPFlag(...)
Rules for new env vars
viperx.AutomaticEnv() with prefix DATAROBOT_CLI is enabled in
initializeConfig, so any key you viperx.Get will already check
DATAROBOT_CLI_<KEY> (with - replaced by _).
For env vars that should map to a different name (e.g.
DATAROBOT_CLI_NON_INTERACTIVE → key yes), use viperx.BindEnv and
read the merged value as shown in Subcommand flags.
Rules for new persisted keys
To make a key writable to drconfig.yaml:
- Add the key to
PersistableKeysininternal/config/write.go - Update its production write call sites to pass the key explicitly:
config.UpdateConfigFile("my-new-key") - Add a regression test under
internal/auth/writeConfig_test.go(or a dedicated test file) verifying the key round-trips correctly and that transient flags still do not leak.
Use viper dotted-path notation if the value is nested, e.g.
"pulumi.config.passphrase". The writer handles nested map creation.
Marking keys as sensitive
Sensitive keys (credentials, passphrases, tokens) must be redacted in debug output to prevent accidental exposure of secrets in logs. To mark a key as sensitive:
- Add the key to
sensitiveDebugKeysininternal/config/config.go:
- When
--debugis enabled, the key will be redacted as****in console output fromDebugViperConfig().
Common pitfalls
- Don't import
github.com/spf13/viperoutsideinternal/config/.depguardwill reject it. Useinternal/config/viperxinstead. - Don't add
viper.WriteConfigorBindPFlagstoviperx. Those omissions are deliberate. If you need to persist new state, extendconfig.PersistableKeysand callconfig.UpdateConfigFile. - Don't read transient flags through viper.
viperx.GetBool("yes")hides whether the value came from a flag, an env var, or a stale drconfig entry. Read flags directly from cobra and merge in env vars explicitly. - Don't write to
drconfig.yamlfrom tests withoutviperx.Reset()and a tempXDG_CONFIG_HOME. Seeinternal/auth/writeConfig_test.gofor the recommended test pattern.