diff --git a/cmd/confd/cli.go b/cmd/confd/cli.go index e25ee9595..aa352c051 100644 --- a/cmd/confd/cli.go +++ b/cmd/confd/cli.go @@ -37,20 +37,20 @@ const ( // CLI is the root command structure type CLI struct { // Global flags - ConfDir string `name:"confdir" help:"confd conf directory" default:"/etc/confd"` - ConfigFile string `name:"config-file" help:"confd config file" default:"/etc/confd/confd.toml"` - Interval int `help:"backend polling interval" default:"600"` - LogLevel string `name:"log-level" help:"log level (debug, info, warn, error)" default:""` - LogFormat string `name:"log-format" help:"log format (text, json)" default:""` - Noop bool `help:"only show pending changes"` - Onetime bool `help:"run once and exit"` - Prefix string `help:"key path prefix"` - SyncOnly bool `name:"sync-only" help:"sync without check_cmd and reload_cmd"` - Watch bool `help:"enable watch support"` - FailureMode string `name:"failure-mode" help:"error handling mode: 'best-effort' or 'fail-fast'" default:"best-effort" enum:"best-effort,fail-fast"` - KeepStageFile bool `name:"keep-stage-file" help:"keep staged files"` - SRVDomain string `name:"srv-domain" help:"DNS SRV domain"` - SRVRecord string `name:"srv-record" help:"SRV record for backend node discovery"` + ConfDir string `name:"confdir" help:"confd conf directory" default:"/etc/confd" env:"CONFD_CONFDIR"` + ConfigFile string `name:"config-file" help:"confd config file" default:"/etc/confd/confd.toml" env:"CONFD_CONFIG_FILE"` + Interval int `help:"backend polling interval" default:"600" env:"CONFD_INTERVAL"` + LogLevel string `name:"log-level" help:"log level (debug, info, warn, error)" default:"" env:"CONFD_LOG_LEVEL"` + LogFormat string `name:"log-format" help:"log format (text, json)" default:"" env:"CONFD_LOG_FORMAT"` + Noop bool `help:"only show pending changes" env:"CONFD_NOOP"` + Onetime bool `help:"run once and exit" env:"CONFD_ONETIME"` + Prefix string `help:"key path prefix" env:"CONFD_PREFIX"` + SyncOnly bool `name:"sync-only" help:"sync without check_cmd and reload_cmd" env:"CONFD_SYNC_ONLY"` + Watch bool `help:"enable watch support" env:"CONFD_WATCH"` + FailureMode string `name:"failure-mode" help:"error handling mode: 'best-effort' or 'fail-fast'" default:"best-effort" enum:"best-effort,fail-fast" env:"CONFD_FAILURE_MODE"` + KeepStageFile bool `name:"keep-stage-file" help:"keep staged files" env:"CONFD_KEEP_STAGE_FILE"` + SRVDomain string `name:"srv-domain" help:"DNS SRV domain" env:"CONFD_SRV_DOMAIN"` + SRVRecord string `name:"srv-record" help:"SRV record for backend node discovery" env:"CONFD_SRV_RECORD"` // Validation flags CheckConfig bool `name:"check-config" help:"validate configuration files and exit"` @@ -65,38 +65,38 @@ type CLI struct { Color bool `help:"colorize diff output"` // Watch mode flags - DebounceStr string `name:"debounce" help:"debounce duration for watch mode (e.g., 2s, 500ms)"` - BatchIntervalStr string `name:"batch-interval" help:"batch processing interval for watch mode (e.g., 5s)"` + DebounceStr string `name:"debounce" help:"debounce duration for watch mode (e.g., 2s, 500ms)" env:"CONFD_DEBOUNCE"` + BatchIntervalStr string `name:"batch-interval" help:"batch processing interval for watch mode (e.g., 5s)" env:"CONFD_BATCH_INTERVAL"` // Performance flags - TemplateCache bool `name:"template-cache" help:"enable template compilation caching" default:"true" negatable:""` - StatCacheTTL time.Duration `name:"stat-cache-ttl" help:"TTL for template file stat cache (e.g., 1s, 500ms)" default:"1s"` - BackendTimeout time.Duration `name:"backend-timeout" help:"timeout for backend operations (e.g., 30s, 1m)" default:"30s"` - CheckCmdTimeout time.Duration `name:"check-cmd-timeout" help:"default timeout for check commands (e.g., 30s)" default:"30s"` - ReloadCmdTimeout time.Duration `name:"reload-cmd-timeout" help:"default timeout for reload commands (e.g., 60s)" default:"60s"` + TemplateCache bool `name:"template-cache" help:"enable template compilation caching" default:"true" negatable:"" env:"CONFD_TEMPLATE_CACHE"` + StatCacheTTL time.Duration `name:"stat-cache-ttl" help:"TTL for template file stat cache (e.g., 1s, 500ms)" default:"1s" env:"CONFD_STAT_CACHE_TTL"` + BackendTimeout time.Duration `name:"backend-timeout" help:"timeout for backend operations (e.g., 30s, 1m)" default:"30s" env:"CONFD_BACKEND_TIMEOUT"` + CheckCmdTimeout time.Duration `name:"check-cmd-timeout" help:"default timeout for check commands (e.g., 30s)" default:"30s" env:"CONFD_CHECK_CMD_TIMEOUT"` + ReloadCmdTimeout time.Duration `name:"reload-cmd-timeout" help:"default timeout for reload commands (e.g., 60s)" default:"60s" env:"CONFD_RELOAD_CMD_TIMEOUT"` // Connection timeouts - DialTimeout time.Duration `name:"dial-timeout" help:"connection timeout for backends" default:"5s"` - ReadTimeout time.Duration `name:"read-timeout" help:"read timeout for backend operations" default:"1s"` - WriteTimeout time.Duration `name:"write-timeout" help:"write timeout for backend operations" default:"1s"` + DialTimeout time.Duration `name:"dial-timeout" help:"connection timeout for backends" default:"5s" env:"CONFD_DIAL_TIMEOUT"` + ReadTimeout time.Duration `name:"read-timeout" help:"read timeout for backend operations" default:"1s" env:"CONFD_READ_TIMEOUT"` + WriteTimeout time.Duration `name:"write-timeout" help:"write timeout for backend operations" default:"1s" env:"CONFD_WRITE_TIMEOUT"` // Retry configuration - RetryMaxAttempts int `name:"retry-max-attempts" help:"max retry attempts for connections" default:"3"` - RetryBaseDelay time.Duration `name:"retry-base-delay" help:"initial backoff delay" default:"100ms"` - RetryMaxDelay time.Duration `name:"retry-max-delay" help:"maximum backoff delay" default:"5s"` + RetryMaxAttempts int `name:"retry-max-attempts" help:"max retry attempts for connections" default:"3" env:"CONFD_RETRY_MAX_ATTEMPTS"` + RetryBaseDelay time.Duration `name:"retry-base-delay" help:"initial backoff delay" default:"100ms" env:"CONFD_RETRY_BASE_DELAY"` + RetryMaxDelay time.Duration `name:"retry-max-delay" help:"maximum backoff delay" default:"5s" env:"CONFD_RETRY_MAX_DELAY"` // Watch mode timeouts - WatchErrorBackoff time.Duration `name:"watch-error-backoff" help:"backoff after watch errors" default:"2s"` + WatchErrorBackoff time.Duration `name:"watch-error-backoff" help:"backoff after watch errors" default:"2s" env:"CONFD_WATCH_ERROR_BACKOFF"` // Preflight timeout - PreflightTimeout time.Duration `name:"preflight-timeout" help:"preflight check timeout" default:"10s"` + PreflightTimeout time.Duration `name:"preflight-timeout" help:"preflight check timeout" default:"10s" env:"CONFD_PREFLIGHT_TIMEOUT"` // Shutdown timeout - ShutdownTimeout time.Duration `name:"shutdown-timeout" help:"graceful shutdown timeout" default:"30s"` + ShutdownTimeout time.Duration `name:"shutdown-timeout" help:"graceful shutdown timeout" default:"30s" env:"CONFD_SHUTDOWN_TIMEOUT"` // Systemd integration SystemdNotify bool `name:"systemd-notify" help:"enable systemd sd_notify support" env:"CONFD_SYSTEMD_NOTIFY"` - WatchdogInterval time.Duration `name:"watchdog-interval" help:"systemd watchdog ping interval (0=disabled)" default:"0"` + WatchdogInterval time.Duration `name:"watchdog-interval" help:"systemd watchdog ping interval (0=disabled)" default:"0" env:"CONFD_WATCHDOG_INTERVAL"` // Metrics and observability MetricsAddr string `name:"metrics-addr" help:"Address for metrics endpoint (e.g., :9100). Disabled if empty." env:"CONFD_METRICS_ADDR"` diff --git a/cmd/confd/cli_test.go b/cmd/confd/cli_test.go index d4dd3c77b..dd1027354 100644 --- a/cmd/confd/cli_test.go +++ b/cmd/confd/cli_test.go @@ -4,6 +4,7 @@ import ( "os" "path/filepath" "testing" + "time" "github.com/abtreece/confd/pkg/backends" "github.com/abtreece/confd/pkg/log" @@ -665,3 +666,185 @@ func TestProcessEnvDoesNotOverride(t *testing.T) { t.Errorf("Expected ClientCert '/cli/cert' (not overridden), got %q", cfg.ClientCert) } } + +// clearCONFDEnvVars clears all CONFD_* and backend-specific env vars that could +// interfere with Kong parsing, saving original values for restoration via t.Cleanup. +func clearCONFDEnvVars(t *testing.T) { + t.Helper() + + // List of env vars that Kong parses and could interfere with tests + envVars := []string{ + "ACM_EXPORT_PRIVATE_KEY", + "CONFD_CONFDIR", "CONFD_CONFIG_FILE", "CONFD_INTERVAL", + "CONFD_LOG_LEVEL", "CONFD_LOG_FORMAT", "CONFD_PREFIX", + "CONFD_WATCH", "CONFD_ONETIME", "CONFD_NOOP", "CONFD_SYNC_ONLY", + "CONFD_FAILURE_MODE", "CONFD_KEEP_STAGE_FILE", + "CONFD_BACKEND_TIMEOUT", "CONFD_CHECK_CMD_TIMEOUT", "CONFD_RELOAD_CMD_TIMEOUT", + "CONFD_DIAL_TIMEOUT", "CONFD_READ_TIMEOUT", "CONFD_WRITE_TIMEOUT", + "CONFD_PREFLIGHT_TIMEOUT", "CONFD_SHUTDOWN_TIMEOUT", + "CONFD_RETRY_MAX_ATTEMPTS", "CONFD_RETRY_BASE_DELAY", "CONFD_RETRY_MAX_DELAY", + "CONFD_DEBOUNCE", "CONFD_BATCH_INTERVAL", "CONFD_WATCH_ERROR_BACKOFF", + "CONFD_TEMPLATE_CACHE", "CONFD_STAT_CACHE_TTL", + "CONFD_METRICS_ADDR", "CONFD_SYSTEMD_NOTIFY", + "CONFD_CLIENT_CERT", "CONFD_CLIENT_KEY", "CONFD_CLIENT_CAKEYS", + "CONFD_SRV_DOMAIN", "CONFD_SRV_RECORD", "CONFD_WATCHDOG_INTERVAL", + } + + // Save original values and clear + origValues := make(map[string]string) + for _, env := range envVars { + if val, exists := os.LookupEnv(env); exists { + origValues[env] = val + } + os.Unsetenv(env) + } + + // Restore original values on cleanup + t.Cleanup(func() { + for _, env := range envVars { + if val, exists := origValues[env]; exists { + os.Setenv(env, val) + } else { + os.Unsetenv(env) + } + } + }) +} + +func TestCLIEnvVars(t *testing.T) { + // Clear all CONFD_* env vars to ensure hermetic tests + clearCONFDEnvVars(t) + + tests := []struct { + name string + envVar string + envValue string + check func(*CLI) bool + desc string + }{ + { + name: "CONFD_CONFDIR", + envVar: "CONFD_CONFDIR", + envValue: "/custom/confd", + check: func(cli *CLI) bool { return cli.ConfDir == "/custom/confd" }, + desc: "ConfDir", + }, + { + name: "CONFD_INTERVAL", + envVar: "CONFD_INTERVAL", + envValue: "300", + check: func(cli *CLI) bool { return cli.Interval == 300 }, + desc: "Interval", + }, + { + name: "CONFD_LOG_LEVEL", + envVar: "CONFD_LOG_LEVEL", + envValue: "debug", + check: func(cli *CLI) bool { return cli.LogLevel == "debug" }, + desc: "LogLevel", + }, + { + name: "CONFD_LOG_FORMAT", + envVar: "CONFD_LOG_FORMAT", + envValue: "json", + check: func(cli *CLI) bool { return cli.LogFormat == "json" }, + desc: "LogFormat", + }, + { + name: "CONFD_PREFIX", + envVar: "CONFD_PREFIX", + envValue: "/production", + check: func(cli *CLI) bool { return cli.Prefix == "/production" }, + desc: "Prefix", + }, + { + name: "CONFD_WATCH", + envVar: "CONFD_WATCH", + envValue: "true", + check: func(cli *CLI) bool { return cli.Watch == true }, + desc: "Watch", + }, + { + name: "CONFD_ONETIME", + envVar: "CONFD_ONETIME", + envValue: "true", + check: func(cli *CLI) bool { return cli.Onetime == true }, + desc: "Onetime", + }, + { + name: "CONFD_NOOP", + envVar: "CONFD_NOOP", + envValue: "true", + check: func(cli *CLI) bool { return cli.Noop == true }, + desc: "Noop", + }, + { + name: "CONFD_SYNC_ONLY", + envVar: "CONFD_SYNC_ONLY", + envValue: "true", + check: func(cli *CLI) bool { return cli.SyncOnly == true }, + desc: "SyncOnly", + }, + { + name: "CONFD_BACKEND_TIMEOUT", + envVar: "CONFD_BACKEND_TIMEOUT", + envValue: "45s", + check: func(cli *CLI) bool { return cli.BackendTimeout == 45*time.Second }, + desc: "BackendTimeout", + }, + { + name: "CONFD_METRICS_ADDR", + envVar: "CONFD_METRICS_ADDR", + envValue: ":9100", + check: func(cli *CLI) bool { return cli.MetricsAddr == ":9100" }, + desc: "MetricsAddr", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Use t.Setenv for automatic cleanup + t.Setenv(tt.envVar, tt.envValue) + + cli := &CLI{} + parser, err := kong.New(cli, kong.NoDefaultHelp()) + if err != nil { + t.Fatalf("Failed to create kong parser: %v", err) + } + + // Parse with minimal args (just the env subcommand to satisfy required cmd) + _, err = parser.Parse([]string{"env"}) + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + + if !tt.check(cli) { + t.Errorf("Environment variable %s=%s did not set %s correctly", tt.envVar, tt.envValue, tt.desc) + } + }) + } +} + +func TestCLIEnvVarPrecedence(t *testing.T) { + // Clear all CONFD_* env vars to ensure hermetic tests + clearCONFDEnvVars(t) + + // Test that CLI flags take precedence over env vars + t.Setenv("CONFD_INTERVAL", "300") + + cli := &CLI{} + parser, err := kong.New(cli, kong.NoDefaultHelp()) + if err != nil { + t.Fatalf("Failed to create kong parser: %v", err) + } + + // CLI flag should override env var + _, err = parser.Parse([]string{"--interval", "600", "env"}) + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + + if cli.Interval != 600 { + t.Errorf("CLI flag should override env var: expected 600, got %d", cli.Interval) + } +} diff --git a/docs/command-line-flags.md b/docs/command-line-flags.md index b0d287444..fa4e1bb0e 100644 --- a/docs/command-line-flags.md +++ b/docs/command-line-flags.md @@ -499,21 +499,85 @@ confd --stat-cache-ttl 5s etcd --watch ## Environment Variables -Global configuration can also be set via environment variables with the `CONFD_` prefix: +Global configuration can also be set via environment variables with the `CONFD_` prefix. Command-line flags take precedence over environment variables. + +### Core Settings + +| Variable | Description | Default | +|----------|-------------|---------| +| `CONFD_CONFDIR` | Configuration directory | `/etc/confd` | +| `CONFD_CONFIG_FILE` | Configuration file path | `/etc/confd/confd.toml` | +| `CONFD_INTERVAL` | Polling interval (seconds) | `600` | +| `CONFD_LOG_LEVEL` | Log level (debug, info, warn, error) | `info` | +| `CONFD_LOG_FORMAT` | Log format (text, json) | `text` | +| `CONFD_PREFIX` | Key prefix | | +| `CONFD_WATCH` | Enable watch mode | `false` | +| `CONFD_ONETIME` | Run once and exit | `false` | +| `CONFD_NOOP` | Dry-run mode (no changes) | `false` | +| `CONFD_SYNC_ONLY` | Sync without check/reload commands | `false` | +| `CONFD_FAILURE_MODE` | Error handling (best-effort, fail-fast) | `best-effort` | +| `CONFD_KEEP_STAGE_FILE` | Keep staged files after processing | `false` | + +### Timeouts and Performance + +| Variable | Description | Default | +|----------|-------------|---------| +| `CONFD_BACKEND_TIMEOUT` | Backend operation timeout | `30s` | +| `CONFD_CHECK_CMD_TIMEOUT` | Check command timeout | `30s` | +| `CONFD_RELOAD_CMD_TIMEOUT` | Reload command timeout | `60s` | +| `CONFD_DIAL_TIMEOUT` | Connection timeout | `5s` | +| `CONFD_READ_TIMEOUT` | Read timeout | `1s` | +| `CONFD_WRITE_TIMEOUT` | Write timeout | `1s` | +| `CONFD_PREFLIGHT_TIMEOUT` | Preflight check timeout | `10s` | +| `CONFD_SHUTDOWN_TIMEOUT` | Graceful shutdown timeout | `30s` | + +### Retry Configuration + +| Variable | Description | Default | +|----------|-------------|---------| +| `CONFD_RETRY_MAX_ATTEMPTS` | Max retry attempts | `3` | +| `CONFD_RETRY_BASE_DELAY` | Initial backoff delay | `100ms` | +| `CONFD_RETRY_MAX_DELAY` | Maximum backoff delay | `5s` | + +### Watch Mode Settings + +| Variable | Description | Default | +|----------|-------------|---------| +| `CONFD_DEBOUNCE` | Debounce duration for watch mode | | +| `CONFD_BATCH_INTERVAL` | Batch processing interval | | +| `CONFD_WATCH_ERROR_BACKOFF` | Backoff after watch errors | `2s` | + +### Caching + +| Variable | Description | Default | +|----------|-------------|---------| +| `CONFD_TEMPLATE_CACHE` | Enable template caching | `true` | +| `CONFD_STAT_CACHE_TTL` | Template file stat cache TTL | `1s` | + +### Service Discovery + +| Variable | Description | Default | +|----------|-------------|---------| +| `CONFD_SRV_DOMAIN` | DNS SRV domain for backend discovery | | +| `CONFD_SRV_RECORD` | SRV record for backend node discovery | | + +### Observability + +| Variable | Description | Default | +|----------|-------------|---------| +| `CONFD_METRICS_ADDR` | Metrics endpoint address (e.g., `:9100`) | | +| `CONFD_SYSTEMD_NOTIFY` | Enable systemd sd_notify | `false` | +| `CONFD_WATCHDOG_INTERVAL` | Systemd watchdog ping interval (0=disabled) | `0` | + +### TLS/Authentication | Variable | Description | |----------|-------------| -| `CONFD_CONFDIR` | Configuration directory | -| `CONFD_INTERVAL` | Polling interval | -| `CONFD_LOG_LEVEL` | Log level | -| `CONFD_PREFIX` | Key prefix | -| `CONFD_METRICS_ADDR` | Metrics endpoint address | -| `CONFD_SYSTEMD_NOTIFY` | Enable systemd notify | | `CONFD_CLIENT_CERT` | Client certificate file | | `CONFD_CLIENT_KEY` | Client key file | | `CONFD_CLIENT_CAKEYS` | CA certificate file | -Backend-specific environment variables: +### Backend-Specific Variables | Variable | Backend | Description | |----------|---------|-------------|