feat: Use viper for config handling and flags

This commit is contained in:
TheiLLeniumStudios
2025-12-28 11:31:27 +01:00
parent 9a5fbf190d
commit 2f9633c3f1
4 changed files with 358 additions and 212 deletions
+8
View File
@@ -39,6 +39,7 @@ require (
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
@@ -52,9 +53,16 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
+16
View File
@@ -65,6 +65,8 @@ github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
@@ -114,6 +116,8 @@ github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -132,17 +136,29 @@ github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+176 -164
View File
@@ -2,215 +2,211 @@ package config
import (
"strings"
"time"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
// flagValues holds intermediate string values from CLI flags
// that need further parsing into the Config struct.
type flagValues struct {
namespaceSelectors string
resourceSelectors string
ignoredResources string
ignoredWorkloads string
ignoredNamespaces string
isArgoRollouts string
reloadOnCreate string
reloadOnDelete string
}
// v is the viper instance for configuration.
var v *viper.Viper
var fv flagValues
func init() {
v = viper.New()
// Convert flag names like "alert-webhook-url" to env vars like "ALERT_WEBHOOK_URL"
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
v.AutomaticEnv()
}
// BindFlags binds configuration flags to the provided flag set.
// Call this before parsing flags, then call ApplyFlags after parsing.
func BindFlags(fs *pflag.FlagSet, cfg *Config) {
// Auto reload
fs.BoolVar(
&cfg.AutoReloadAll, "auto-reload-all", cfg.AutoReloadAll,
"Automatically reload all resources when their configmaps/secrets are updated, without requiring annotations",
)
fs.Bool("auto-reload-all", cfg.AutoReloadAll,
"Automatically reload all resources when their configmaps/secrets are updated, without requiring annotations")
// Reload strategy
fs.StringVar(
(*string)(&cfg.ReloadStrategy), "reload-strategy", string(cfg.ReloadStrategy),
"Strategy for triggering workload restart: 'env-vars' (default, GitOps friendly) or 'annotations'",
)
fs.String("reload-strategy", string(cfg.ReloadStrategy),
"Strategy for triggering workload restart: 'env-vars' (default, GitOps friendly) or 'annotations'")
// Argo Rollouts
fs.StringVar(
&fv.isArgoRollouts, "is-Argo-Rollouts", "false",
"Enable Argo Rollouts support (true/false)",
)
fs.String("is-Argo-Rollouts", "false",
"Enable Argo Rollouts support (true/false)")
// Event watching
fs.StringVar(
&fv.reloadOnCreate, "reload-on-create", "false",
"Reload when configmaps/secrets are created (true/false)",
)
fs.StringVar(
&fv.reloadOnDelete, "reload-on-delete", "false",
"Reload when configmaps/secrets are deleted (true/false)",
)
fs.String("reload-on-create", "false",
"Reload when configmaps/secrets are created (true/false)")
fs.String("reload-on-delete", "false",
"Reload when configmaps/secrets are deleted (true/false)")
// Sync after restart
fs.BoolVar(
&cfg.SyncAfterRestart, "sync-after-restart", cfg.SyncAfterRestart,
"Trigger sync operation after restart",
)
fs.Bool("sync-after-restart", cfg.SyncAfterRestart,
"Trigger sync operation after restart")
// High availability / Leader election
fs.BoolVar(
&cfg.EnableHA, "enable-ha", cfg.EnableHA,
"Enable high-availability mode with leader election",
)
fs.StringVar(
&cfg.LeaderElection.LockName, "leader-election-id", cfg.LeaderElection.LockName,
"Name of the lease resource for leader election",
)
fs.StringVar(
&cfg.LeaderElection.Namespace, "leader-election-namespace", cfg.LeaderElection.Namespace,
"Namespace for the leader election lease (defaults to pod namespace)",
)
fs.DurationVar(
&cfg.LeaderElection.LeaseDuration, "leader-election-lease-duration", cfg.LeaderElection.LeaseDuration,
"Duration that non-leader candidates will wait before attempting to acquire leadership",
)
fs.DurationVar(
&cfg.LeaderElection.RenewDeadline, "leader-election-renew-deadline", cfg.LeaderElection.RenewDeadline,
"Duration that the acting leader will retry refreshing leadership before giving up",
)
fs.DurationVar(
&cfg.LeaderElection.RetryPeriod, "leader-election-retry-period", cfg.LeaderElection.RetryPeriod,
"Duration between leader election retries",
)
fs.BoolVar(
&cfg.LeaderElection.ReleaseOnCancel, "leader-election-release-on-cancel", cfg.LeaderElection.ReleaseOnCancel,
"Release the leader lock when the manager is stopped",
)
fs.Bool("enable-ha", cfg.EnableHA,
"Enable high-availability mode with leader election")
fs.String("leader-election-id", cfg.LeaderElection.LockName,
"Name of the lease resource for leader election")
fs.String("leader-election-namespace", cfg.LeaderElection.Namespace,
"Namespace for the leader election lease (defaults to pod namespace)")
fs.Duration("leader-election-lease-duration", cfg.LeaderElection.LeaseDuration,
"Duration that non-leader candidates will wait before attempting to acquire leadership")
fs.Duration("leader-election-renew-deadline", cfg.LeaderElection.RenewDeadline,
"Duration that the acting leader will retry refreshing leadership before giving up")
fs.Duration("leader-election-retry-period", cfg.LeaderElection.RetryPeriod,
"Duration between leader election retries")
fs.Bool("leader-election-release-on-cancel", cfg.LeaderElection.ReleaseOnCancel,
"Release the leader lock when the manager is stopped")
// Webhook
fs.StringVar(
&cfg.WebhookURL, "webhook-url", cfg.WebhookURL,
"URL to send notification instead of triggering reload",
)
fs.String("webhook-url", cfg.WebhookURL,
"URL to send notification instead of triggering reload")
// Filtering - resources (use StringVar not StringSliceVar for simpler parsing)
fs.StringVar(
&fv.ignoredResources, "resources-to-ignore", "",
"Comma-separated list of resources to ignore (valid options: 'configMaps' or 'secrets')",
)
fs.StringVar(
&fv.ignoredWorkloads, "ignored-workload-types", "",
"Comma-separated list of workload types to ignore (valid options: 'jobs', 'cronjobs', or both)",
)
fs.StringVar(
&fv.ignoredNamespaces, "namespaces-to-ignore", "",
"Comma-separated list of namespaces to ignore",
)
// Filtering - resources
fs.String("resources-to-ignore", "",
"Comma-separated list of resources to ignore (valid options: 'configMaps' or 'secrets')")
fs.String("ignored-workload-types", "",
"Comma-separated list of workload types to ignore (valid options: 'jobs', 'cronjobs', or both)")
fs.String("namespaces-to-ignore", "",
"Comma-separated list of namespaces to ignore")
// Filtering - selectors
fs.StringVar(
&fv.namespaceSelectors, "namespace-selector", "",
"Comma-separated list of namespace label selectors",
)
fs.StringVar(
&fv.resourceSelectors, "resource-label-selector", "",
"Comma-separated list of resource label selectors",
)
fs.String("namespace-selector", "",
"Comma-separated list of namespace label selectors")
fs.String("resource-label-selector", "",
"Comma-separated list of resource label selectors")
// Logging
fs.StringVar(
&cfg.LogFormat, "log-format", cfg.LogFormat,
"Log format: 'json' or empty for default",
)
fs.StringVar(
&cfg.LogLevel, "log-level", cfg.LogLevel,
"Log level: trace, debug, info, warning, error, fatal, panic",
)
fs.String("log-format", cfg.LogFormat,
"Log format: 'json' or empty for default")
fs.String("log-level", cfg.LogLevel,
"Log level: trace, debug, info, warning, error, fatal, panic")
// Metrics
fs.StringVar(
&cfg.MetricsAddr, "metrics-addr", cfg.MetricsAddr,
"Address to serve metrics on",
)
fs.String("metrics-addr", cfg.MetricsAddr,
"Address to serve metrics on")
// Health probes
fs.StringVar(
&cfg.HealthAddr, "health-addr", cfg.HealthAddr,
"Address to serve health probes on",
)
fs.String("health-addr", cfg.HealthAddr,
"Address to serve health probes on")
// Profiling
fs.BoolVar(
&cfg.EnablePProf, "enable-pprof", cfg.EnablePProf,
"Enable pprof profiling server",
)
fs.StringVar(
&cfg.PProfAddr, "pprof-addr", cfg.PProfAddr,
"Address for pprof server",
)
fs.Bool("enable-pprof", cfg.EnablePProf,
"Enable pprof profiling server")
fs.String("pprof-addr", cfg.PProfAddr,
"Address for pprof server")
// Annotation customization (flag names match v1 for backward compatibility)
fs.StringVar(
&cfg.Annotations.Auto, "auto-annotation", cfg.Annotations.Auto,
"Annotation to detect changes in secrets/configmaps",
)
fs.StringVar(
&cfg.Annotations.ConfigmapAuto, "configmap-auto-annotation", cfg.Annotations.ConfigmapAuto,
"Annotation to detect changes in configmaps",
)
fs.StringVar(
&cfg.Annotations.SecretAuto, "secret-auto-annotation", cfg.Annotations.SecretAuto,
"Annotation to detect changes in secrets",
)
fs.StringVar(
&cfg.Annotations.ConfigmapReload, "configmap-annotation", cfg.Annotations.ConfigmapReload,
"Annotation to detect changes in configmaps, specified by name",
)
fs.StringVar(
&cfg.Annotations.SecretReload, "secret-annotation", cfg.Annotations.SecretReload,
"Annotation to detect changes in secrets, specified by name",
)
fs.StringVar(
&cfg.Annotations.Search, "auto-search-annotation", cfg.Annotations.Search,
"Annotation to detect changes in configmaps or secrets tagged with special match annotation",
)
fs.StringVar(
&cfg.Annotations.Match, "search-match-annotation", cfg.Annotations.Match,
"Annotation to mark secrets or configmaps to match the search",
)
fs.StringVar(
&cfg.Annotations.PausePeriod, "pause-deployment-annotation", cfg.Annotations.PausePeriod,
"Annotation to define the time period to pause a deployment after a configmap/secret change",
)
fs.StringVar(
&cfg.Annotations.PausedAt, "pause-deployment-time-annotation", cfg.Annotations.PausedAt,
"Annotation to indicate when a deployment was paused by Reloader",
)
fs.String("auto-annotation", cfg.Annotations.Auto,
"Annotation to detect changes in secrets/configmaps")
fs.String("configmap-auto-annotation", cfg.Annotations.ConfigmapAuto,
"Annotation to detect changes in configmaps")
fs.String("secret-auto-annotation", cfg.Annotations.SecretAuto,
"Annotation to detect changes in secrets")
fs.String("configmap-annotation", cfg.Annotations.ConfigmapReload,
"Annotation to detect changes in configmaps, specified by name")
fs.String("secret-annotation", cfg.Annotations.SecretReload,
"Annotation to detect changes in secrets, specified by name")
fs.String("auto-search-annotation", cfg.Annotations.Search,
"Annotation to detect changes in configmaps or secrets tagged with special match annotation")
fs.String("search-match-annotation", cfg.Annotations.Match,
"Annotation to mark secrets or configmaps to match the search")
fs.String("pause-deployment-annotation", cfg.Annotations.PausePeriod,
"Annotation to define the time period to pause a deployment after a configmap/secret change")
fs.String("pause-deployment-time-annotation", cfg.Annotations.PausedAt,
"Annotation to indicate when a deployment was paused by Reloader")
// Watched namespace (for single-namespace mode)
fs.StringVar(
&cfg.WatchedNamespace, "watch-namespace", cfg.WatchedNamespace,
"Namespace to watch (empty for all namespaces)",
)
fs.String("watch-namespace", cfg.WatchedNamespace,
"Namespace to watch (empty for all namespaces)")
// Alerting
fs.Bool("alert-on-reload", cfg.Alerting.Enabled,
"Enable sending alerts when resources are reloaded")
fs.String("alert-webhook-url", cfg.Alerting.WebhookURL,
"Webhook URL to send alerts to")
fs.String("alert-sink", cfg.Alerting.Sink,
"Alert sink type: 'slack', 'teams', 'gchat', or 'raw' (default)")
fs.String("alert-proxy", cfg.Alerting.Proxy,
"Proxy URL for alert webhook requests")
fs.String("alert-additional-info", cfg.Alerting.Additional,
"Additional info to include in alerts (e.g., cluster name)")
fs.Bool("alert-structured", cfg.Alerting.Structured,
"For raw sink: send structured JSON instead of plain text")
// Bind pflags to viper
_ = v.BindPFlags(fs)
// Bind legacy env var names that don't match the automatic conversion
// (flag "alert-proxy" -> env "ALERT_PROXY", but legacy is "ALERT_WEBHOOK_PROXY")
_ = v.BindEnv("alert-proxy", "ALERT_PROXY", "ALERT_WEBHOOK_PROXY")
}
// ApplyFlags applies flag values that need post-processing.
// ApplyFlags applies flag values from viper to the config struct.
// Call this after parsing flags.
func ApplyFlags(cfg *Config) error {
// Parse boolean string flags
cfg.ArgoRolloutsEnabled = parseBoolString(fv.isArgoRollouts)
cfg.ReloadOnCreate = parseBoolString(fv.reloadOnCreate)
cfg.ReloadOnDelete = parseBoolString(fv.reloadOnDelete)
// Boolean flags
cfg.AutoReloadAll = v.GetBool("auto-reload-all")
cfg.SyncAfterRestart = v.GetBool("sync-after-restart")
cfg.EnableHA = v.GetBool("enable-ha")
cfg.EnablePProf = v.GetBool("enable-pprof")
// Boolean string flags (legacy format: "true"/"false" strings)
cfg.ArgoRolloutsEnabled = parseBoolString(v.GetString("is-Argo-Rollouts"))
cfg.ReloadOnCreate = parseBoolString(v.GetString("reload-on-create"))
cfg.ReloadOnDelete = parseBoolString(v.GetString("reload-on-delete"))
// String flags
cfg.ReloadStrategy = ReloadStrategy(v.GetString("reload-strategy"))
cfg.WebhookURL = v.GetString("webhook-url")
cfg.LogFormat = v.GetString("log-format")
cfg.LogLevel = v.GetString("log-level")
cfg.MetricsAddr = v.GetString("metrics-addr")
cfg.HealthAddr = v.GetString("health-addr")
cfg.PProfAddr = v.GetString("pprof-addr")
cfg.WatchedNamespace = v.GetString("watch-namespace")
// Leader election
cfg.LeaderElection.LockName = v.GetString("leader-election-id")
cfg.LeaderElection.Namespace = v.GetString("leader-election-namespace")
cfg.LeaderElection.LeaseDuration = v.GetDuration("leader-election-lease-duration")
cfg.LeaderElection.RenewDeadline = v.GetDuration("leader-election-renew-deadline")
cfg.LeaderElection.RetryPeriod = v.GetDuration("leader-election-retry-period")
cfg.LeaderElection.ReleaseOnCancel = v.GetBool("leader-election-release-on-cancel")
// Annotations
cfg.Annotations.Auto = v.GetString("auto-annotation")
cfg.Annotations.ConfigmapAuto = v.GetString("configmap-auto-annotation")
cfg.Annotations.SecretAuto = v.GetString("secret-auto-annotation")
cfg.Annotations.ConfigmapReload = v.GetString("configmap-annotation")
cfg.Annotations.SecretReload = v.GetString("secret-annotation")
cfg.Annotations.Search = v.GetString("auto-search-annotation")
cfg.Annotations.Match = v.GetString("search-match-annotation")
cfg.Annotations.PausePeriod = v.GetString("pause-deployment-annotation")
cfg.Annotations.PausedAt = v.GetString("pause-deployment-time-annotation")
// Alerting
cfg.Alerting.Enabled = v.GetBool("alert-on-reload")
cfg.Alerting.WebhookURL = v.GetString("alert-webhook-url")
cfg.Alerting.Sink = strings.ToLower(v.GetString("alert-sink"))
cfg.Alerting.Proxy = v.GetString("alert-proxy")
cfg.Alerting.Additional = v.GetString("alert-additional-info")
cfg.Alerting.Structured = v.GetBool("alert-structured")
// Special case: if webhook URL is set, auto-enable alerting
if cfg.Alerting.WebhookURL != "" {
cfg.Alerting.Enabled = true
}
// Parse comma-separated lists
cfg.IgnoredResources = splitAndTrim(fv.ignoredResources)
cfg.IgnoredWorkloads = splitAndTrim(fv.ignoredWorkloads)
cfg.IgnoredNamespaces = splitAndTrim(fv.ignoredNamespaces)
cfg.IgnoredResources = splitAndTrim(v.GetString("resources-to-ignore"))
cfg.IgnoredWorkloads = splitAndTrim(v.GetString("ignored-workload-types"))
cfg.IgnoredNamespaces = splitAndTrim(v.GetString("namespaces-to-ignore"))
// Store raw selector strings
cfg.NamespaceSelectorStrings = splitAndTrim(fv.namespaceSelectors)
cfg.ResourceSelectorStrings = splitAndTrim(fv.resourceSelectors)
cfg.NamespaceSelectorStrings = splitAndTrim(v.GetString("namespace-selector"))
cfg.ResourceSelectorStrings = splitAndTrim(v.GetString("resource-label-selector"))
// Parse selectors into labels.Selector
var err error
@@ -223,9 +219,25 @@ func ApplyFlags(cfg *Config) error {
return err
}
// Ensure duration defaults are preserved if not set
if cfg.LeaderElection.LeaseDuration == 0 {
cfg.LeaderElection.LeaseDuration = 15 * time.Second
}
if cfg.LeaderElection.RenewDeadline == 0 {
cfg.LeaderElection.RenewDeadline = 10 * time.Second
}
if cfg.LeaderElection.RetryPeriod == 0 {
cfg.LeaderElection.RetryPeriod = 2 * time.Second
}
return nil
}
// GetViper returns the viper instance for testing or advanced configuration.
func GetViper() *viper.Viper {
return v
}
// parseBoolString parses a string as a boolean, defaulting to false.
func parseBoolString(s string) bool {
s = strings.ToLower(strings.TrimSpace(s))
+158 -48
View File
@@ -1,12 +1,23 @@
package config
import (
"os"
"strings"
"testing"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
// resetViper resets the viper instance for testing.
func resetViper() {
v = viper.New()
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
v.AutomaticEnv()
}
func TestBindFlags(t *testing.T) {
resetViper()
cfg := NewDefault()
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
@@ -48,6 +59,12 @@ func TestBindFlags(t *testing.T) {
"pause-deployment-annotation",
"pause-deployment-time-annotation",
"watch-namespace",
"alert-on-reload",
"alert-webhook-url",
"alert-sink",
"alert-proxy",
"alert-additional-info",
"alert-structured",
}
for _, flagName := range expectedFlags {
@@ -58,6 +75,7 @@ func TestBindFlags(t *testing.T) {
}
func TestBindFlags_DefaultValues(t *testing.T) {
resetViper()
cfg := NewDefault()
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
@@ -67,6 +85,10 @@ func TestBindFlags_DefaultValues(t *testing.T) {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
if cfg.ReloadStrategy != ReloadStrategyEnvVars {
t.Errorf("ReloadStrategy = %v, want %v", cfg.ReloadStrategy, ReloadStrategyEnvVars)
}
@@ -77,6 +99,7 @@ func TestBindFlags_DefaultValues(t *testing.T) {
}
func TestBindFlags_CustomValues(t *testing.T) {
resetViper()
cfg := NewDefault()
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
@@ -96,6 +119,10 @@ func TestBindFlags_CustomValues(t *testing.T) {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
if !cfg.AutoReloadAll {
t.Error("AutoReloadAll should be true")
}
@@ -143,35 +170,31 @@ func TestApplyFlags_BooleanStrings(t *testing.T) {
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
fv = flagValues{}
t.Run(tt.name, func(t *testing.T) {
resetViper()
cfg := NewDefault()
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
BindFlags(fs, cfg)
cfg := NewDefault()
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
BindFlags(fs, cfg)
if err := fs.Parse(tt.args); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if err := fs.Parse(tt.args); err != nil {
t.Fatalf("Parse() error = %v", err)
}
err := ApplyFlags(cfg)
if (err != nil) != tt.wantErr {
t.Errorf("ApplyFlags() error = %v, wantErr %v", err, tt.wantErr)
return
}
err := ApplyFlags(cfg)
if (err != nil) != tt.wantErr {
t.Errorf("ApplyFlags() error = %v, wantErr %v", err, tt.wantErr)
return
}
if cfg.ArgoRolloutsEnabled != tt.want {
t.Errorf("ArgoRolloutsEnabled = %v, want %v", cfg.ArgoRolloutsEnabled, tt.want)
}
},
)
if cfg.ArgoRolloutsEnabled != tt.want {
t.Errorf("ArgoRolloutsEnabled = %v, want %v", cfg.ArgoRolloutsEnabled, tt.want)
}
})
}
}
func TestApplyFlags_CommaSeparatedLists(t *testing.T) {
fv = flagValues{}
resetViper()
cfg := NewDefault()
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
BindFlags(fs, cfg)
@@ -201,15 +224,13 @@ func TestApplyFlags_CommaSeparatedLists(t *testing.T) {
t.Errorf("IgnoredWorkloads length = %d, want 2", len(cfg.IgnoredWorkloads))
}
// Check ignored namespaces
if len(cfg.IgnoredNamespaces) != 2 {
t.Errorf("IgnoredNamespaces length = %d, want 2", len(cfg.IgnoredNamespaces))
}
}
func TestApplyFlags_Selectors(t *testing.T) {
fv = flagValues{}
resetViper()
cfg := NewDefault()
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
BindFlags(fs, cfg)
@@ -241,8 +262,7 @@ func TestApplyFlags_Selectors(t *testing.T) {
}
func TestApplyFlags_InvalidSelector(t *testing.T) {
fv = flagValues{}
resetViper()
cfg := NewDefault()
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
BindFlags(fs, cfg)
@@ -261,6 +281,100 @@ func TestApplyFlags_InvalidSelector(t *testing.T) {
}
}
func TestApplyFlags_AlertingEnvVars(t *testing.T) {
tests := []struct {
name string
envVars map[string]string
wantURL string
wantSink string
wantEnable bool
}{
{
name: "ALERT_WEBHOOK_URL enables alerting",
envVars: map[string]string{
"ALERT_WEBHOOK_URL": "https://hooks.example.com",
},
wantURL: "https://hooks.example.com",
wantEnable: true,
},
{
name: "all alert env vars",
envVars: map[string]string{
"ALERT_WEBHOOK_URL": "https://hooks.example.com",
"ALERT_SINK": "slack",
"ALERT_WEBHOOK_PROXY": "http://proxy:8080",
},
wantURL: "https://hooks.example.com",
wantSink: "slack",
wantEnable: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Clear env and reset viper
for k := range tt.envVars {
os.Unsetenv(k)
}
resetViper()
// Set env vars
for k, val := range tt.envVars {
os.Setenv(k, val)
defer os.Unsetenv(k)
}
cfg := NewDefault()
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
BindFlags(fs, cfg)
if err := fs.Parse([]string{}); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
if cfg.Alerting.WebhookURL != tt.wantURL {
t.Errorf("Alerting.WebhookURL = %q, want %q", cfg.Alerting.WebhookURL, tt.wantURL)
}
if tt.wantSink != "" && cfg.Alerting.Sink != tt.wantSink {
t.Errorf("Alerting.Sink = %q, want %q", cfg.Alerting.Sink, tt.wantSink)
}
if cfg.Alerting.Enabled != tt.wantEnable {
t.Errorf("Alerting.Enabled = %v, want %v", cfg.Alerting.Enabled, tt.wantEnable)
}
})
}
}
func TestApplyFlags_LegacyProxyEnvVar(t *testing.T) {
resetViper()
// Set legacy env var
os.Setenv("ALERT_WEBHOOK_PROXY", "http://legacy-proxy:8080")
defer os.Unsetenv("ALERT_WEBHOOK_PROXY")
cfg := NewDefault()
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
BindFlags(fs, cfg)
if err := fs.Parse([]string{}); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
if cfg.Alerting.Proxy != "http://legacy-proxy:8080" {
t.Errorf("Alerting.Proxy = %q, want %q", cfg.Alerting.Proxy, "http://legacy-proxy:8080")
}
}
func TestParseBoolString(t *testing.T) {
tests := []struct {
input string
@@ -282,14 +396,12 @@ func TestParseBoolString(t *testing.T) {
}
for _, tt := range tests {
t.Run(
tt.input, func(t *testing.T) {
got := parseBoolString(tt.input)
if got != tt.want {
t.Errorf("parseBoolString(%q) = %v, want %v", tt.input, got, tt.want)
}
},
)
t.Run(tt.input, func(t *testing.T) {
got := parseBoolString(tt.input)
if got != tt.want {
t.Errorf("parseBoolString(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}
@@ -308,19 +420,17 @@ func TestSplitAndTrim(t *testing.T) {
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
got := splitAndTrim(tt.input)
if len(got) != len(tt.want) {
t.Errorf("splitAndTrim(%q) length = %d, want %d", tt.input, len(got), len(tt.want))
return
t.Run(tt.name, func(t *testing.T) {
got := splitAndTrim(tt.input)
if len(got) != len(tt.want) {
t.Errorf("splitAndTrim(%q) length = %d, want %d", tt.input, len(got), len(tt.want))
return
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("splitAndTrim(%q)[%d] = %q, want %q", tt.input, i, got[i], tt.want[i])
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("splitAndTrim(%q)[%d] = %q, want %q", tt.input, i, got[i], tt.want[i])
}
}
},
)
}
})
}
}