Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 31 additions & 31 deletions cmd/confd/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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"`
Expand Down
183 changes: 183 additions & 0 deletions cmd/confd/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"os"
"path/filepath"
"testing"
"time"

"github.com/abtreece/confd/pkg/backends"
"github.com/abtreece/confd/pkg/log"
Expand Down Expand Up @@ -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)
}
}
80 changes: 72 additions & 8 deletions docs/command-line-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|----------|---------|-------------|
Expand Down