mirror of
https://github.com/stakater/Reloader.git
synced 2026-08-20 12:36:26 +00:00
feat: Add reconciler test cases and config test cases
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewDefault(t *testing.T) {
|
||||
cfg := NewDefault()
|
||||
|
||||
if cfg == nil {
|
||||
t.Fatal("NewDefault() returned nil")
|
||||
}
|
||||
|
||||
// Test default values
|
||||
if cfg.ReloadStrategy != ReloadStrategyEnvVars {
|
||||
t.Errorf("ReloadStrategy = %v, want %v", cfg.ReloadStrategy, ReloadStrategyEnvVars)
|
||||
}
|
||||
|
||||
if cfg.ArgoRolloutStrategy != ArgoRolloutStrategyRollout {
|
||||
t.Errorf("ArgoRolloutStrategy = %v, want %v", cfg.ArgoRolloutStrategy, ArgoRolloutStrategyRollout)
|
||||
}
|
||||
|
||||
if cfg.AutoReloadAll {
|
||||
t.Error("AutoReloadAll should be false by default")
|
||||
}
|
||||
|
||||
if cfg.ArgoRolloutsEnabled {
|
||||
t.Error("ArgoRolloutsEnabled should be false by default")
|
||||
}
|
||||
|
||||
if cfg.ReloadOnCreate {
|
||||
t.Error("ReloadOnCreate should be false by default")
|
||||
}
|
||||
|
||||
if cfg.ReloadOnDelete {
|
||||
t.Error("ReloadOnDelete should be false by default")
|
||||
}
|
||||
|
||||
if cfg.EnableHA {
|
||||
t.Error("EnableHA should be false by default")
|
||||
}
|
||||
|
||||
if cfg.LogLevel != "info" {
|
||||
t.Errorf("LogLevel = %q, want %q", cfg.LogLevel, "info")
|
||||
}
|
||||
|
||||
if cfg.MetricsAddr != ":9090" {
|
||||
t.Errorf("MetricsAddr = %q, want %q", cfg.MetricsAddr, ":9090")
|
||||
}
|
||||
|
||||
if cfg.HealthAddr != ":8081" {
|
||||
t.Errorf("HealthAddr = %q, want %q", cfg.HealthAddr, ":8081")
|
||||
}
|
||||
|
||||
if cfg.PProfAddr != ":6060" {
|
||||
t.Errorf("PProfAddr = %q, want %q", cfg.PProfAddr, ":6060")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAnnotations(t *testing.T) {
|
||||
ann := DefaultAnnotations()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
got string
|
||||
want string
|
||||
}{
|
||||
{"Prefix", ann.Prefix, "reloader.stakater.com"},
|
||||
{"Auto", ann.Auto, "reloader.stakater.com/auto"},
|
||||
{"ConfigmapAuto", ann.ConfigmapAuto, "configmap.reloader.stakater.com/auto"},
|
||||
{"SecretAuto", ann.SecretAuto, "secret.reloader.stakater.com/auto"},
|
||||
{"ConfigmapReload", ann.ConfigmapReload, "configmap.reloader.stakater.com/reload"},
|
||||
{"SecretReload", ann.SecretReload, "secret.reloader.stakater.com/reload"},
|
||||
{"ConfigmapExclude", ann.ConfigmapExclude, "configmaps.exclude.reloader.stakater.com/reload"},
|
||||
{"SecretExclude", ann.SecretExclude, "secrets.exclude.reloader.stakater.com/reload"},
|
||||
{"Ignore", ann.Ignore, "reloader.stakater.com/ignore"},
|
||||
{"Search", ann.Search, "reloader.stakater.com/search"},
|
||||
{"Match", ann.Match, "reloader.stakater.com/match"},
|
||||
{"RolloutStrategy", ann.RolloutStrategy, "reloader.stakater.com/rollout-strategy"},
|
||||
{"PausePeriod", ann.PausePeriod, "deployment.reloader.stakater.com/pause-period"},
|
||||
{"PausedAt", ann.PausedAt, "deployment.reloader.stakater.com/paused-at"},
|
||||
{"LastReloadedFrom", ann.LastReloadedFrom, "reloader.stakater.com/last-reloaded-from"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.got != tt.want {
|
||||
t.Errorf("%s = %q, want %q", tt.name, tt.got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultLeaderElection(t *testing.T) {
|
||||
cfg := NewDefault()
|
||||
|
||||
if cfg.LeaderElection.LockName != "reloader-leader-election" {
|
||||
t.Errorf("LockName = %q, want %q", cfg.LeaderElection.LockName, "reloader-leader-election")
|
||||
}
|
||||
|
||||
if cfg.LeaderElection.LeaseDuration != 15*time.Second {
|
||||
t.Errorf("LeaseDuration = %v, want %v", cfg.LeaderElection.LeaseDuration, 15*time.Second)
|
||||
}
|
||||
|
||||
if cfg.LeaderElection.RenewDeadline != 10*time.Second {
|
||||
t.Errorf("RenewDeadline = %v, want %v", cfg.LeaderElection.RenewDeadline, 10*time.Second)
|
||||
}
|
||||
|
||||
if cfg.LeaderElection.RetryPeriod != 2*time.Second {
|
||||
t.Errorf("RetryPeriod = %v, want %v", cfg.LeaderElection.RetryPeriod, 2*time.Second)
|
||||
}
|
||||
|
||||
if !cfg.LeaderElection.ReleaseOnCancel {
|
||||
t.Error("ReleaseOnCancel should be true by default")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_IsResourceIgnored(t *testing.T) {
|
||||
cfg := NewDefault()
|
||||
cfg.IgnoredResources = []string{"configmaps", "secrets"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
resource string
|
||||
want bool
|
||||
}{
|
||||
{"exact match lowercase", "configmaps", true},
|
||||
{"exact match uppercase", "CONFIGMAPS", true},
|
||||
{"exact match mixed case", "ConfigMaps", true},
|
||||
{"not ignored", "deployments", false},
|
||||
{"partial match (not ignored)", "config", false},
|
||||
{"empty string", "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := cfg.IsResourceIgnored(tt.resource)
|
||||
if got != tt.want {
|
||||
t.Errorf("IsResourceIgnored(%q) = %v, want %v", tt.resource, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_IsWorkloadIgnored(t *testing.T) {
|
||||
cfg := NewDefault()
|
||||
cfg.IgnoredWorkloads = []string{"jobs", "cronjobs"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
workload string
|
||||
want bool
|
||||
}{
|
||||
{"exact match", "jobs", true},
|
||||
{"case insensitive", "JOBS", true},
|
||||
{"not ignored", "deployments", false},
|
||||
{"empty string", "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := cfg.IsWorkloadIgnored(tt.workload)
|
||||
if got != tt.want {
|
||||
t.Errorf("IsWorkloadIgnored(%q) = %v, want %v", tt.workload, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_IsNamespaceIgnored(t *testing.T) {
|
||||
cfg := NewDefault()
|
||||
cfg.IgnoredNamespaces = []string{"kube-system", "kube-public"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
want bool
|
||||
}{
|
||||
{"exact match", "kube-system", true},
|
||||
{"case sensitive no match", "Kube-System", false},
|
||||
{"not ignored", "default", false},
|
||||
{"empty string", "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := cfg.IsNamespaceIgnored(tt.namespace)
|
||||
if got != tt.want {
|
||||
t.Errorf("IsNamespaceIgnored(%q) = %v, want %v", tt.namespace, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEqualFold(t *testing.T) {
|
||||
tests := []struct {
|
||||
s, t string
|
||||
want bool
|
||||
}{
|
||||
{"abc", "abc", true},
|
||||
{"ABC", "abc", true},
|
||||
{"abc", "ABC", true},
|
||||
{"aBc", "AbC", true},
|
||||
{"abc", "abcd", false},
|
||||
{"", "", true},
|
||||
{"a", "", false},
|
||||
{"", "a", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.s+"_"+tt.t, func(t *testing.T) {
|
||||
got := equalFold(tt.s, tt.t)
|
||||
if got != tt.want {
|
||||
t.Errorf("equalFold(%q, %q) = %v, want %v", tt.s, tt.t, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReloadStrategy_String(t *testing.T) {
|
||||
if string(ReloadStrategyEnvVars) != "env-vars" {
|
||||
t.Errorf("ReloadStrategyEnvVars = %q, want %q", ReloadStrategyEnvVars, "env-vars")
|
||||
}
|
||||
if string(ReloadStrategyAnnotations) != "annotations" {
|
||||
t.Errorf("ReloadStrategyAnnotations = %q, want %q", ReloadStrategyAnnotations, "annotations")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgoRolloutStrategy_String(t *testing.T) {
|
||||
if string(ArgoRolloutStrategyRestart) != "restart" {
|
||||
t.Errorf("ArgoRolloutStrategyRestart = %q, want %q", ArgoRolloutStrategyRestart, "restart")
|
||||
}
|
||||
if string(ArgoRolloutStrategyRollout) != "rollout" {
|
||||
t.Errorf("ArgoRolloutStrategyRollout = %q, want %q", ArgoRolloutStrategyRollout, "rollout")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func TestBindFlags(t *testing.T) {
|
||||
cfg := NewDefault()
|
||||
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
||||
|
||||
BindFlags(fs, cfg)
|
||||
|
||||
// Verify flags are registered
|
||||
expectedFlags := []string{
|
||||
"auto-reload-all",
|
||||
"reload-strategy",
|
||||
"is-Argo-Rollouts",
|
||||
"reload-on-create",
|
||||
"reload-on-delete",
|
||||
"sync-after-restart",
|
||||
"enable-ha",
|
||||
"leader-election-id",
|
||||
"leader-election-namespace",
|
||||
"leader-election-lease-duration",
|
||||
"leader-election-renew-deadline",
|
||||
"leader-election-retry-period",
|
||||
"leader-election-release-on-cancel",
|
||||
"webhook-url",
|
||||
"resources-to-ignore",
|
||||
"ignored-workload-types",
|
||||
"namespaces-to-ignore",
|
||||
"namespace-selector",
|
||||
"resource-label-selector",
|
||||
"log-format",
|
||||
"log-level",
|
||||
"metrics-addr",
|
||||
"health-addr",
|
||||
"enable-pprof",
|
||||
"pprof-addr",
|
||||
"auto-annotation",
|
||||
"configmap-auto-annotation",
|
||||
"secret-auto-annotation",
|
||||
"configmap-annotation",
|
||||
"secret-annotation",
|
||||
"auto-search-annotation",
|
||||
"search-match-annotation",
|
||||
"pause-deployment-annotation",
|
||||
"pause-deployment-time-annotation",
|
||||
"watch-namespace",
|
||||
}
|
||||
|
||||
for _, flagName := range expectedFlags {
|
||||
if fs.Lookup(flagName) == nil {
|
||||
t.Errorf("Expected flag %q to be registered", flagName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindFlags_DefaultValues(t *testing.T) {
|
||||
cfg := NewDefault()
|
||||
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
||||
|
||||
BindFlags(fs, cfg)
|
||||
|
||||
// Parse empty args to use defaults
|
||||
if err := fs.Parse([]string{}); err != nil {
|
||||
t.Fatalf("Parse() error = %v", err)
|
||||
}
|
||||
|
||||
// Check default values are preserved
|
||||
if cfg.ReloadStrategy != ReloadStrategyEnvVars {
|
||||
t.Errorf("ReloadStrategy = %v, want %v", cfg.ReloadStrategy, ReloadStrategyEnvVars)
|
||||
}
|
||||
|
||||
if cfg.LogLevel != "info" {
|
||||
t.Errorf("LogLevel = %q, want %q", cfg.LogLevel, "info")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindFlags_CustomValues(t *testing.T) {
|
||||
cfg := NewDefault()
|
||||
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
||||
|
||||
BindFlags(fs, cfg)
|
||||
|
||||
args := []string{
|
||||
"--auto-reload-all=true",
|
||||
"--reload-strategy=annotations",
|
||||
"--log-level=debug",
|
||||
"--log-format=json",
|
||||
"--webhook-url=https://example.com/hook",
|
||||
"--enable-ha=true",
|
||||
"--enable-pprof=true",
|
||||
}
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
t.Fatalf("Parse() error = %v", err)
|
||||
}
|
||||
|
||||
if !cfg.AutoReloadAll {
|
||||
t.Error("AutoReloadAll should be true")
|
||||
}
|
||||
|
||||
if cfg.ReloadStrategy != ReloadStrategyAnnotations {
|
||||
t.Errorf("ReloadStrategy = %v, want %v", cfg.ReloadStrategy, ReloadStrategyAnnotations)
|
||||
}
|
||||
|
||||
if cfg.LogLevel != "debug" {
|
||||
t.Errorf("LogLevel = %q, want %q", cfg.LogLevel, "debug")
|
||||
}
|
||||
|
||||
if cfg.LogFormat != "json" {
|
||||
t.Errorf("LogFormat = %q, want %q", cfg.LogFormat, "json")
|
||||
}
|
||||
|
||||
if cfg.WebhookURL != "https://example.com/hook" {
|
||||
t.Errorf("WebhookURL = %q, want %q", cfg.WebhookURL, "https://example.com/hook")
|
||||
}
|
||||
|
||||
if !cfg.EnableHA {
|
||||
t.Error("EnableHA should be true")
|
||||
}
|
||||
|
||||
if !cfg.EnablePProf {
|
||||
t.Error("EnablePProf should be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyFlags_BooleanStrings(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{"true lowercase", []string{"--is-Argo-Rollouts=true"}, true, false},
|
||||
{"TRUE uppercase", []string{"--is-Argo-Rollouts=TRUE"}, true, false},
|
||||
{"1", []string{"--is-Argo-Rollouts=1"}, true, false},
|
||||
{"yes", []string{"--is-Argo-Rollouts=yes"}, true, false},
|
||||
{"false", []string{"--is-Argo-Rollouts=false"}, false, false},
|
||||
{"no", []string{"--is-Argo-Rollouts=no"}, false, false},
|
||||
{"0", []string{"--is-Argo-Rollouts=0"}, false, false},
|
||||
{"empty", []string{}, false, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Reset flag values
|
||||
fv = flagValues{}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyFlags_CommaSeparatedLists(t *testing.T) {
|
||||
// Reset flag values
|
||||
fv = flagValues{}
|
||||
|
||||
cfg := NewDefault()
|
||||
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
||||
BindFlags(fs, cfg)
|
||||
|
||||
args := []string{
|
||||
"--resources-to-ignore=configMaps,secrets",
|
||||
"--ignored-workload-types=jobs,cronjobs",
|
||||
"--namespaces-to-ignore=kube-system,kube-public",
|
||||
}
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
t.Fatalf("Parse() error = %v", err)
|
||||
}
|
||||
|
||||
if err := ApplyFlags(cfg); err != nil {
|
||||
t.Fatalf("ApplyFlags() error = %v", err)
|
||||
}
|
||||
|
||||
// Check ignored resources
|
||||
if len(cfg.IgnoredResources) != 2 {
|
||||
t.Errorf("IgnoredResources length = %d, want 2", len(cfg.IgnoredResources))
|
||||
}
|
||||
if cfg.IgnoredResources[0] != "configMaps" || cfg.IgnoredResources[1] != "secrets" {
|
||||
t.Errorf("IgnoredResources = %v", cfg.IgnoredResources)
|
||||
}
|
||||
|
||||
// Check ignored workloads
|
||||
if len(cfg.IgnoredWorkloads) != 2 {
|
||||
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) {
|
||||
// Reset flag values
|
||||
fv = flagValues{}
|
||||
|
||||
cfg := NewDefault()
|
||||
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
||||
BindFlags(fs, cfg)
|
||||
|
||||
args := []string{
|
||||
"--namespace-selector=env=production,team=platform",
|
||||
"--resource-label-selector=app=myapp",
|
||||
}
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
t.Fatalf("Parse() error = %v", err)
|
||||
}
|
||||
|
||||
if err := ApplyFlags(cfg); err != nil {
|
||||
t.Fatalf("ApplyFlags() error = %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.NamespaceSelectors) != 2 {
|
||||
t.Errorf("NamespaceSelectors length = %d, want 2", len(cfg.NamespaceSelectors))
|
||||
}
|
||||
|
||||
if len(cfg.ResourceSelectors) != 1 {
|
||||
t.Errorf("ResourceSelectors length = %d, want 1", len(cfg.ResourceSelectors))
|
||||
}
|
||||
|
||||
// Check string versions are preserved
|
||||
if len(cfg.NamespaceSelectorStrings) != 2 {
|
||||
t.Errorf("NamespaceSelectorStrings length = %d, want 2", len(cfg.NamespaceSelectorStrings))
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyFlags_InvalidSelector(t *testing.T) {
|
||||
// Reset flag values
|
||||
fv = flagValues{}
|
||||
|
||||
cfg := NewDefault()
|
||||
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
||||
BindFlags(fs, cfg)
|
||||
|
||||
args := []string{
|
||||
"--namespace-selector=env in (prod,staging", // missing closing paren
|
||||
}
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
t.Fatalf("Parse() error = %v", err)
|
||||
}
|
||||
|
||||
err := ApplyFlags(cfg)
|
||||
if err == nil {
|
||||
t.Error("ApplyFlags() should return error for invalid selector")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBoolString(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want bool
|
||||
}{
|
||||
{"true", true},
|
||||
{"TRUE", true},
|
||||
{"True", true},
|
||||
{" true ", true},
|
||||
{"1", true},
|
||||
{"yes", true},
|
||||
{"YES", true},
|
||||
{"false", false},
|
||||
{"FALSE", false},
|
||||
{"0", false},
|
||||
{"no", false},
|
||||
{"", false},
|
||||
{"invalid", false},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitAndTrim(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want []string
|
||||
}{
|
||||
{"empty string", "", nil},
|
||||
{"single value", "abc", []string{"abc"}},
|
||||
{"multiple values", "a,b,c", []string{"a", "b", "c"}},
|
||||
{"with spaces", " a , b , c ", []string{"a", "b", "c"}},
|
||||
{"empty elements", "a,,b", []string{"a", "b"}},
|
||||
{"only commas", ",,,", []string{}},
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfig_Validate_ReloadStrategy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
strategy ReloadStrategy
|
||||
wantErr bool
|
||||
wantVal ReloadStrategy
|
||||
}{
|
||||
{"valid env-vars", ReloadStrategyEnvVars, false, ReloadStrategyEnvVars},
|
||||
{"valid annotations", ReloadStrategyAnnotations, false, ReloadStrategyAnnotations},
|
||||
{"empty defaults to env-vars", "", false, ReloadStrategyEnvVars},
|
||||
{"invalid strategy", "invalid", true, ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := NewDefault()
|
||||
cfg.ReloadStrategy = tt.strategy
|
||||
|
||||
err := cfg.Validate()
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("Validate() should return error for invalid strategy")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Validate() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if cfg.ReloadStrategy != tt.wantVal {
|
||||
t.Errorf("ReloadStrategy = %v, want %v", cfg.ReloadStrategy, tt.wantVal)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_Validate_ArgoRolloutStrategy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
strategy ArgoRolloutStrategy
|
||||
wantErr bool
|
||||
wantVal ArgoRolloutStrategy
|
||||
}{
|
||||
{"valid restart", ArgoRolloutStrategyRestart, false, ArgoRolloutStrategyRestart},
|
||||
{"valid rollout", ArgoRolloutStrategyRollout, false, ArgoRolloutStrategyRollout},
|
||||
{"empty defaults to rollout", "", false, ArgoRolloutStrategyRollout},
|
||||
{"invalid strategy", "invalid", true, ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := NewDefault()
|
||||
cfg.ArgoRolloutStrategy = tt.strategy
|
||||
|
||||
err := cfg.Validate()
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("Validate() should return error for invalid strategy")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Validate() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if cfg.ArgoRolloutStrategy != tt.wantVal {
|
||||
t.Errorf("ArgoRolloutStrategy = %v, want %v", cfg.ArgoRolloutStrategy, tt.wantVal)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_Validate_LogLevel(t *testing.T) {
|
||||
validLevels := []string{"trace", "debug", "info", "warn", "warning", "error", "fatal", "panic", ""}
|
||||
for _, level := range validLevels {
|
||||
t.Run("valid_"+level, func(t *testing.T) {
|
||||
cfg := NewDefault()
|
||||
cfg.LogLevel = level
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Errorf("Validate() error for level %q: %v", level, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("invalid level", func(t *testing.T) {
|
||||
cfg := NewDefault()
|
||||
cfg.LogLevel = "invalid"
|
||||
err := cfg.Validate()
|
||||
if err == nil {
|
||||
t.Error("Validate() should return error for invalid log level")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfig_Validate_LogFormat(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
format string
|
||||
wantErr bool
|
||||
}{
|
||||
{"json format", "json", false},
|
||||
{"empty format", "", false},
|
||||
{"invalid format", "xml", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := NewDefault()
|
||||
cfg.LogFormat = tt.format
|
||||
err := cfg.Validate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_Validate_NormalizesIgnoredResources(t *testing.T) {
|
||||
cfg := NewDefault()
|
||||
cfg.IgnoredResources = []string{"ConfigMaps", "SECRETS", " spaces "}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("Validate() error = %v", err)
|
||||
}
|
||||
|
||||
expected := []string{"configmaps", "secrets", "spaces"}
|
||||
if len(cfg.IgnoredResources) != len(expected) {
|
||||
t.Fatalf("IgnoredResources length = %d, want %d", len(cfg.IgnoredResources), len(expected))
|
||||
}
|
||||
|
||||
for i, got := range cfg.IgnoredResources {
|
||||
if got != expected[i] {
|
||||
t.Errorf("IgnoredResources[%d] = %q, want %q", i, got, expected[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_Validate_NormalizesIgnoredWorkloads(t *testing.T) {
|
||||
cfg := NewDefault()
|
||||
cfg.IgnoredWorkloads = []string{"Jobs", "CRONJOBS", ""}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("Validate() error = %v", err)
|
||||
}
|
||||
|
||||
expected := []string{"jobs", "cronjobs"}
|
||||
if len(cfg.IgnoredWorkloads) != len(expected) {
|
||||
t.Fatalf("IgnoredWorkloads length = %d, want %d", len(cfg.IgnoredWorkloads), len(expected))
|
||||
}
|
||||
|
||||
for i, got := range cfg.IgnoredWorkloads {
|
||||
if got != expected[i] {
|
||||
t.Errorf("IgnoredWorkloads[%d] = %q, want %q", i, got, expected[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_Validate_MultipleErrors(t *testing.T) {
|
||||
cfg := NewDefault()
|
||||
cfg.ReloadStrategy = "invalid"
|
||||
cfg.ArgoRolloutStrategy = "invalid"
|
||||
cfg.LogLevel = "invalid"
|
||||
cfg.LogFormat = "invalid"
|
||||
|
||||
err := cfg.Validate()
|
||||
if err == nil {
|
||||
t.Fatal("Validate() should return error for multiple invalid values")
|
||||
}
|
||||
|
||||
errs, ok := err.(ValidationErrors)
|
||||
if !ok {
|
||||
t.Fatalf("Expected ValidationErrors, got %T", err)
|
||||
}
|
||||
|
||||
if len(errs) != 4 {
|
||||
t.Errorf("Expected 4 errors, got %d: %v", len(errs), errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidationError_Error(t *testing.T) {
|
||||
err := ValidationError{
|
||||
Field: "TestField",
|
||||
Message: "test message",
|
||||
}
|
||||
|
||||
expected := "config.TestField: test message"
|
||||
if err.Error() != expected {
|
||||
t.Errorf("Error() = %q, want %q", err.Error(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidationErrors_Error(t *testing.T) {
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
var errs ValidationErrors
|
||||
if errs.Error() != "" {
|
||||
t.Errorf("Empty errors should return empty string, got %q", errs.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single error", func(t *testing.T) {
|
||||
errs := ValidationErrors{
|
||||
{Field: "Field1", Message: "error1"},
|
||||
}
|
||||
if !strings.Contains(errs.Error(), "Field1") {
|
||||
t.Errorf("Error() should contain field name, got %q", errs.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple errors", func(t *testing.T) {
|
||||
errs := ValidationErrors{
|
||||
{Field: "Field1", Message: "error1"},
|
||||
{Field: "Field2", Message: "error2"},
|
||||
}
|
||||
errStr := errs.Error()
|
||||
if !strings.Contains(errStr, "multiple configuration errors") {
|
||||
t.Errorf("Error() should mention multiple errors, got %q", errStr)
|
||||
}
|
||||
if !strings.Contains(errStr, "Field1") || !strings.Contains(errStr, "Field2") {
|
||||
t.Errorf("Error() should contain all field names, got %q", errStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseSelectors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
selectors []string
|
||||
wantLen int
|
||||
wantErr bool
|
||||
}{
|
||||
{"nil input", nil, 0, false},
|
||||
{"empty input", []string{}, 0, false},
|
||||
{"single valid selector", []string{"env=production"}, 1, false},
|
||||
{"multiple valid selectors", []string{"env=production", "team=platform"}, 2, false},
|
||||
{"selector with whitespace", []string{" env=production "}, 1, false},
|
||||
{"empty string in list", []string{"env=production", "", "team=platform"}, 2, false},
|
||||
{"invalid selector syntax", []string{"env in (prod,staging"}, 0, true}, // missing closing paren
|
||||
{"set-based selector", []string{"env in (prod,staging)"}, 1, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
selectors, err := ParseSelectors(tt.selectors)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseSelectors() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr && len(selectors) != tt.wantLen {
|
||||
t.Errorf("ParseSelectors() returned %d selectors, want %d", len(selectors), tt.wantLen)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustParseSelectors(t *testing.T) {
|
||||
t.Run("valid selectors", func(t *testing.T) {
|
||||
selectors := MustParseSelectors([]string{"env=production"})
|
||||
if len(selectors) != 1 {
|
||||
t.Errorf("MustParseSelectors() returned %d selectors, want 1", len(selectors))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("panics on invalid", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("MustParseSelectors() should panic on invalid selector")
|
||||
}
|
||||
}()
|
||||
MustParseSelectors([]string{"env in (prod,staging"}) // missing closing paren
|
||||
})
|
||||
}
|
||||
|
||||
func TestNormalizeToLower(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string
|
||||
want []string
|
||||
}{
|
||||
{"nil input", nil, nil},
|
||||
{"empty input", []string{}, []string{}},
|
||||
{"lowercase", []string{"abc"}, []string{"abc"}},
|
||||
{"uppercase", []string{"ABC"}, []string{"abc"}},
|
||||
{"mixed case", []string{"AbC"}, []string{"abc"}},
|
||||
{"with whitespace", []string{" abc "}, []string{"abc"}},
|
||||
{"removes empty", []string{"abc", "", "def"}, []string{"abc", "def"}},
|
||||
{"only whitespace", []string{" "}, []string{}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := normalizeToLower(tt.input)
|
||||
if tt.want == nil && got != nil {
|
||||
t.Errorf("normalizeToLower() = %v, want nil", got)
|
||||
return
|
||||
}
|
||||
if len(got) != len(tt.want) {
|
||||
t.Errorf("normalizeToLower() length = %d, want %d", len(got), len(tt.want))
|
||||
return
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.want[i] {
|
||||
t.Errorf("normalizeToLower()[%d] = %q, want %q", i, got[i], tt.want[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,844 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/go-logr/logr/testr"
|
||||
"github.com/stakater/Reloader/internal/pkg/alerting"
|
||||
"github.com/stakater/Reloader/internal/pkg/config"
|
||||
"github.com/stakater/Reloader/internal/pkg/controller"
|
||||
"github.com/stakater/Reloader/internal/pkg/events"
|
||||
"github.com/stakater/Reloader/internal/pkg/metrics"
|
||||
"github.com/stakater/Reloader/internal/pkg/reload"
|
||||
"github.com/stakater/Reloader/internal/pkg/webhook"
|
||||
"github.com/stakater/Reloader/internal/pkg/workload"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
func newTestScheme() *runtime.Scheme {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
_ = appsv1.AddToScheme(scheme)
|
||||
_ = batchv1.AddToScheme(scheme)
|
||||
return scheme
|
||||
}
|
||||
|
||||
func newTestConfigMapReconciler(t *testing.T, cfg *config.Config, objects ...runtime.Object) *controller.ConfigMapReconciler {
|
||||
scheme := newTestScheme()
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithRuntimeObjects(objects...).
|
||||
Build()
|
||||
|
||||
collectors := metrics.NewCollectors()
|
||||
|
||||
return &controller.ConfigMapReconciler{
|
||||
Client: fakeClient,
|
||||
Log: testr.New(t),
|
||||
Config: cfg,
|
||||
ReloadService: reload.NewService(cfg),
|
||||
Registry: workload.NewRegistry(cfg.ArgoRolloutsEnabled),
|
||||
Collectors: &collectors,
|
||||
EventRecorder: events.NewRecorder(nil),
|
||||
WebhookClient: webhook.NewClient("", testr.New(t)),
|
||||
Alerter: &alerting.NoOpAlerter{},
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_NotFound(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
reconciler := newTestConfigMapReconciler(t, cfg)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "nonexistent-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue for NotFound")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_NotFound_ReloadOnDelete(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.ReloadOnDelete = true
|
||||
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-deployment",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "deleted-cm",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, deployment)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "deleted-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_IgnoredNamespace(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.IgnoredNamespaces = []string{"kube-system"}
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-cm",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue for ignored namespace")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_NoMatchingWorkloads(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-deployment",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, deployment)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_MatchingDeployment_AutoAnnotation(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.AutoReloadAll = true
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-deployment",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
EnvFrom: []corev1.EnvFromSource{{
|
||||
ConfigMapRef: &corev1.ConfigMapEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: "test-cm",
|
||||
},
|
||||
},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, deployment)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_MatchingDeployment_ExplicitAnnotation(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-deployment",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, deployment)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_WorkloadInDifferentNamespace(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "namespace-a",
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-deployment",
|
||||
Namespace: "namespace-b",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, deployment)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-cm",
|
||||
Namespace: "namespace-a",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_IgnoredWorkloadType(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.IgnoredWorkloads = []string{"deployment"}
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-deployment",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, deployment)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_DaemonSet(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
|
||||
daemonset := &appsv1.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-daemonset",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DaemonSetSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, daemonset)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_StatefulSet(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
|
||||
statefulset := &appsv1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-statefulset",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, statefulset)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_MultipleWorkloads(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "shared-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
|
||||
deployment1 := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "deployment-1",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "shared-cm",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test1"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test1"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
deployment2 := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "deployment-2",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "shared-cm",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test2"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test2"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
daemonset := &appsv1.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "daemonset-1",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "shared-cm",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DaemonSetSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "daemon"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "daemon"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, deployment1, deployment2, daemonset)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "shared-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_VolumeMount(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.AutoReloadAll = true
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "volume-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{"config.yaml": "key: value"},
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-deployment",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
VolumeMounts: []corev1.VolumeMount{{
|
||||
Name: "config",
|
||||
MountPath: "/etc/config",
|
||||
}},
|
||||
}},
|
||||
Volumes: []corev1.Volume{{
|
||||
Name: "config",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
ConfigMap: &corev1.ConfigMapVolumeSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: "volume-cm",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, deployment)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "volume-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_ProjectedVolume(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.AutoReloadAll = true
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "projected-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{"config.yaml": "key: value"},
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-deployment",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
VolumeMounts: []corev1.VolumeMount{{
|
||||
Name: "config",
|
||||
MountPath: "/etc/config",
|
||||
}},
|
||||
}},
|
||||
Volumes: []corev1.Volume{{
|
||||
Name: "config",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Projected: &corev1.ProjectedVolumeSource{
|
||||
Sources: []corev1.VolumeProjection{{
|
||||
ConfigMap: &corev1.ConfigMapProjection{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: "projected-cm",
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, deployment)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "projected-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_SearchAnnotation(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.Match: "true",
|
||||
},
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-deployment",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.Search: "true",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, deployment)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user