mirror of
https://github.com/stakater/Reloader.git
synced 2026-08-27 14:37:17 +00:00
feat: e2e tests and a lot of refactoring for existing tests
This commit is contained in:
@@ -106,9 +106,12 @@ jobs:
|
||||
kubectl cluster-info
|
||||
|
||||
|
||||
- name: Test
|
||||
- name: Unit Tests
|
||||
run: make test
|
||||
|
||||
- name: E2E Tests
|
||||
run: make e2e
|
||||
|
||||
- name: Generate Tags
|
||||
id: generate_tag
|
||||
run: |
|
||||
|
||||
@@ -147,7 +147,10 @@ manifest:
|
||||
docker manifest annotate --arch $(ARCH) $(REPOSITORY_GENERIC) $(REPOSITORY_ARCH)
|
||||
|
||||
test:
|
||||
"$(GOCMD)" test -timeout 1800s -v ./cmd/... ./internal/...
|
||||
"$(GOCMD)" test -timeout 1800s -v -short ./cmd/... ./internal/...
|
||||
|
||||
e2e:
|
||||
"$(GOCMD)" test -timeout 1800s -v ./test/...
|
||||
|
||||
stop:
|
||||
@docker stop "${BINARY}"
|
||||
|
||||
@@ -7,6 +7,7 @@ require (
|
||||
github.com/go-logr/logr v1.4.2
|
||||
github.com/go-logr/zerologr v1.2.3
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/prometheus/client_model v0.6.2
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/pflag v1.0.9
|
||||
@@ -43,7 +44,6 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.63.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
|
||||
@@ -45,9 +45,9 @@ func testAlertMessage() AlertMessage {
|
||||
|
||||
func TestNewAlerter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(*config.Config)
|
||||
wantType string
|
||||
name string
|
||||
setup func(*config.Config)
|
||||
wantType string
|
||||
}{
|
||||
{
|
||||
name: "disabled",
|
||||
|
||||
@@ -26,75 +26,75 @@ const (
|
||||
|
||||
// Config holds all configuration for Reloader.
|
||||
type Config struct {
|
||||
Annotations AnnotationConfig
|
||||
AutoReloadAll bool
|
||||
ReloadStrategy ReloadStrategy
|
||||
ArgoRolloutsEnabled bool
|
||||
ArgoRolloutStrategy ArgoRolloutStrategy
|
||||
ReloadOnCreate bool
|
||||
ReloadOnDelete bool
|
||||
SyncAfterRestart bool
|
||||
EnableHA bool
|
||||
WebhookURL string
|
||||
Annotations AnnotationConfig `json:"annotations"`
|
||||
AutoReloadAll bool `json:"autoReloadAll"`
|
||||
ReloadStrategy ReloadStrategy `json:"reloadStrategy"`
|
||||
ArgoRolloutsEnabled bool `json:"argoRolloutsEnabled"`
|
||||
ArgoRolloutStrategy ArgoRolloutStrategy `json:"argoRolloutStrategy"`
|
||||
ReloadOnCreate bool `json:"reloadOnCreate"`
|
||||
ReloadOnDelete bool `json:"reloadOnDelete"`
|
||||
SyncAfterRestart bool `json:"syncAfterRestart"`
|
||||
EnableHA bool `json:"enableHA"`
|
||||
WebhookURL string `json:"webhookUrl,omitempty"`
|
||||
|
||||
IgnoredResources []string
|
||||
IgnoredWorkloads []string
|
||||
IgnoredNamespaces []string
|
||||
NamespaceSelectors []labels.Selector
|
||||
ResourceSelectors []labels.Selector
|
||||
NamespaceSelectorStrings []string
|
||||
ResourceSelectorStrings []string
|
||||
IgnoredResources []string `json:"ignoredResources,omitempty"`
|
||||
IgnoredWorkloads []string `json:"ignoredWorkloads,omitempty"`
|
||||
IgnoredNamespaces []string `json:"ignoredNamespaces,omitempty"`
|
||||
NamespaceSelectors []labels.Selector `json:"-"`
|
||||
ResourceSelectors []labels.Selector `json:"-"`
|
||||
NamespaceSelectorStrings []string `json:"namespaceSelectors,omitempty"`
|
||||
ResourceSelectorStrings []string `json:"resourceSelectors,omitempty"`
|
||||
|
||||
LogFormat string
|
||||
LogLevel string
|
||||
MetricsAddr string
|
||||
HealthAddr string
|
||||
EnablePProf bool
|
||||
PProfAddr string
|
||||
LogFormat string `json:"logFormat,omitempty"`
|
||||
LogLevel string `json:"logLevel"`
|
||||
MetricsAddr string `json:"metricsAddr"`
|
||||
HealthAddr string `json:"healthAddr"`
|
||||
EnablePProf bool `json:"enablePProf"`
|
||||
PProfAddr string `json:"pprofAddr,omitempty"`
|
||||
|
||||
Alerting AlertingConfig
|
||||
LeaderElection LeaderElectionConfig
|
||||
WatchedNamespace string
|
||||
SyncPeriod time.Duration
|
||||
Alerting AlertingConfig `json:"alerting"`
|
||||
LeaderElection LeaderElectionConfig `json:"leaderElection"`
|
||||
WatchedNamespace string `json:"watchedNamespace,omitempty"`
|
||||
SyncPeriod time.Duration `json:"syncPeriod"`
|
||||
}
|
||||
|
||||
// AnnotationConfig holds customizable annotation keys.
|
||||
type AnnotationConfig struct {
|
||||
Prefix string
|
||||
Auto string
|
||||
ConfigmapAuto string
|
||||
SecretAuto string
|
||||
ConfigmapReload string
|
||||
SecretReload string
|
||||
ConfigmapExclude string
|
||||
SecretExclude string
|
||||
Ignore string
|
||||
Search string
|
||||
Match string
|
||||
RolloutStrategy string
|
||||
PausePeriod string
|
||||
PausedAt string
|
||||
LastReloadedFrom string
|
||||
Prefix string `json:"prefix"`
|
||||
Auto string `json:"auto"`
|
||||
ConfigmapAuto string `json:"configmapAuto"`
|
||||
SecretAuto string `json:"secretAuto"`
|
||||
ConfigmapReload string `json:"configmapReload"`
|
||||
SecretReload string `json:"secretReload"`
|
||||
ConfigmapExclude string `json:"configmapExclude"`
|
||||
SecretExclude string `json:"secretExclude"`
|
||||
Ignore string `json:"ignore"`
|
||||
Search string `json:"search"`
|
||||
Match string `json:"match"`
|
||||
RolloutStrategy string `json:"rolloutStrategy"`
|
||||
PausePeriod string `json:"pausePeriod"`
|
||||
PausedAt string `json:"pausedAt"`
|
||||
LastReloadedFrom string `json:"lastReloadedFrom"`
|
||||
}
|
||||
|
||||
// AlertingConfig holds configuration for alerting integrations.
|
||||
type AlertingConfig struct {
|
||||
Enabled bool
|
||||
WebhookURL string
|
||||
Sink string
|
||||
Proxy string
|
||||
Additional string
|
||||
Enabled bool `json:"enabled"`
|
||||
WebhookURL string `json:"webhookUrl,omitempty"`
|
||||
Sink string `json:"sink,omitempty"`
|
||||
Proxy string `json:"proxy,omitempty"`
|
||||
Additional string `json:"additional,omitempty"`
|
||||
}
|
||||
|
||||
// LeaderElectionConfig holds configuration for leader election.
|
||||
type LeaderElectionConfig struct {
|
||||
LockName string
|
||||
Namespace string
|
||||
Identity string
|
||||
LeaseDuration time.Duration
|
||||
RenewDeadline time.Duration
|
||||
RetryPeriod time.Duration
|
||||
ReleaseOnCancel bool
|
||||
LockName string `json:"lockName"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
Identity string `json:"identity,omitempty"`
|
||||
LeaseDuration time.Duration `json:"leaseDuration"`
|
||||
RenewDeadline time.Duration `json:"renewDeadline"`
|
||||
RetryPeriod time.Duration `json:"retryPeriod"`
|
||||
ReleaseOnCancel bool `json:"releaseOnCancel"`
|
||||
}
|
||||
|
||||
// NewDefault creates a Config with default values.
|
||||
@@ -184,4 +184,3 @@ func (c *Config) IsNamespaceIgnored(namespace string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ func TestNewDefault(t *testing.T) {
|
||||
t.Fatal("NewDefault() returned nil")
|
||||
}
|
||||
|
||||
// Test default values
|
||||
if cfg.ReloadStrategy != ReloadStrategyEnvVars {
|
||||
t.Errorf("ReloadStrategy = %v, want %v", cfg.ReloadStrategy, ReloadStrategyEnvVars)
|
||||
}
|
||||
@@ -200,4 +199,3 @@ func TestConfig_IsNamespaceIgnored(t *testing.T) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ func ApplyFlags(cfg *Config) error {
|
||||
cfg.IgnoredWorkloads = splitAndTrim(fv.ignoredWorkloads)
|
||||
cfg.IgnoredNamespaces = splitAndTrim(fv.ignoredNamespaces)
|
||||
|
||||
// Store raw selector strings (for backward compatibility)
|
||||
// Store raw selector strings
|
||||
cfg.NamespaceSelectorStrings = splitAndTrim(fv.namespaceSelectors)
|
||||
cfg.ResourceSelectorStrings = splitAndTrim(fv.resourceSelectors)
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ func TestBindFlags(t *testing.T) {
|
||||
|
||||
BindFlags(fs, cfg)
|
||||
|
||||
// Verify flags are registered
|
||||
expectedFlags := []string{
|
||||
"auto-reload-all",
|
||||
"reload-strategy",
|
||||
@@ -64,12 +63,10 @@ func TestBindFlags_DefaultValues(t *testing.T) {
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -146,33 +143,33 @@ func TestApplyFlags_BooleanStrings(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Reset flag values
|
||||
fv = flagValues{}
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
fv = flagValues{}
|
||||
|
||||
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) {
|
||||
// Reset flag values
|
||||
fv = flagValues{}
|
||||
|
||||
cfg := NewDefault()
|
||||
@@ -193,7 +190,6 @@ func TestApplyFlags_CommaSeparatedLists(t *testing.T) {
|
||||
t.Fatalf("ApplyFlags() error = %v", err)
|
||||
}
|
||||
|
||||
// Check ignored resources
|
||||
if len(cfg.IgnoredResources) != 2 {
|
||||
t.Errorf("IgnoredResources length = %d, want 2", len(cfg.IgnoredResources))
|
||||
}
|
||||
@@ -201,7 +197,6 @@ func TestApplyFlags_CommaSeparatedLists(t *testing.T) {
|
||||
t.Errorf("IgnoredResources = %v", cfg.IgnoredResources)
|
||||
}
|
||||
|
||||
// Check ignored workloads
|
||||
if len(cfg.IgnoredWorkloads) != 2 {
|
||||
t.Errorf("IgnoredWorkloads length = %d, want 2", len(cfg.IgnoredWorkloads))
|
||||
}
|
||||
@@ -213,7 +208,6 @@ func TestApplyFlags_CommaSeparatedLists(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApplyFlags_Selectors(t *testing.T) {
|
||||
// Reset flag values
|
||||
fv = flagValues{}
|
||||
|
||||
cfg := NewDefault()
|
||||
@@ -241,14 +235,12 @@ func TestApplyFlags_Selectors(t *testing.T) {
|
||||
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()
|
||||
@@ -290,12 +282,14 @@ 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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,17 +308,19 @@ 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
|
||||
}
|
||||
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])
|
||||
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])
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func (c *Config) Validate() error {
|
||||
default:
|
||||
errs = append(
|
||||
errs, ValidationError{
|
||||
Field: "ArgoRolloutStrategy",
|
||||
Field: "ArgoRolloutStrategy",
|
||||
Message: fmt.Sprintf(
|
||||
"invalid value %q, must be %q or %q", c.ArgoRolloutStrategy, ArgoRolloutStrategyRestart, ArgoRolloutStrategyRollout,
|
||||
),
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package constants
|
||||
|
||||
// Environment variable names for pod identity in HA mode.
|
||||
const (
|
||||
PodNameEnv string = "POD_NAME"
|
||||
PodNamespaceEnv string = "POD_NAMESPACE"
|
||||
)
|
||||
@@ -1,844 +1,159 @@
|
||||
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")
|
||||
}
|
||||
reconciler := newConfigMapReconciler(t, cfg)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("nonexistent-cm", "default"))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
deployment := testDeployment("test-deployment", "default", map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "deleted-cm",
|
||||
})
|
||||
reconciler := newConfigMapReconciler(t, cfg, deployment)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("deleted-cm", "default"))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
cm := testConfigMap("test-cm", "kube-system")
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("test-cm", "kube-system"))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
cm := testConfigMap("test-cm", "default")
|
||||
deployment := testDeployment("test-deployment", "default", nil)
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, deployment)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("test-cm", "default"))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
cm := testConfigMap("test-cm", "default")
|
||||
deployment := testDeploymentWithEnvFrom("test-deployment", "default", "test-cm", "")
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, deployment)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("test-cm", "default"))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
cm := testConfigMap("test-cm", "default")
|
||||
deployment := testDeployment("test-deployment", "default", map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
})
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, deployment)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("test-cm", "default"))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
cm := testConfigMap("test-cm", "namespace-a")
|
||||
deployment := testDeployment("test-deployment", "namespace-b", map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
})
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, deployment)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("test-cm", "namespace-a"))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
cm := testConfigMap("test-cm", "default")
|
||||
deployment := testDeployment("test-deployment", "default", map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
})
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, deployment)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("test-cm", "default"))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
cm := testConfigMap("test-cm", "default")
|
||||
daemonset := testDaemonSet("test-daemonset", "default", map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
})
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, daemonset)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("test-cm", "default"))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
cm := testConfigMap("test-cm", "default")
|
||||
statefulset := testStatefulSet("test-statefulset", "default", map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
})
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, statefulset)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("test-cm", "default"))
|
||||
}
|
||||
|
||||
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"},
|
||||
}
|
||||
cm := testConfigMap("shared-cm", "default")
|
||||
deployment1 := testDeployment("deployment-1", "default", map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "shared-cm",
|
||||
})
|
||||
deployment2 := testDeployment("deployment-2", "default", map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "shared-cm",
|
||||
})
|
||||
daemonset := testDaemonSet("daemonset-1", "default", map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "shared-cm",
|
||||
})
|
||||
|
||||
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")
|
||||
}
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, deployment1, deployment2, daemonset)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("shared-cm", "default"))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
cm := testConfigMap("volume-cm", "default")
|
||||
deployment := testDeploymentWithVolume("test-deployment", "default", "volume-cm", "")
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, deployment)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("volume-cm", "default"))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
cm := testConfigMap("projected-cm", "default")
|
||||
deployment := testDeploymentWithProjectedVolume("test-deployment", "default", "projected-cm", "")
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, deployment)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("projected-cm", "default"))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
cm := testConfigMapWithAnnotations("test-cm", "default", map[string]string{
|
||||
cfg.Annotations.Match: "true",
|
||||
})
|
||||
deployment := testDeployment("test-deployment", "default", map[string]string{
|
||||
cfg.Annotations.Search: "true",
|
||||
})
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, deployment)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("test-cm", "default"))
|
||||
}
|
||||
|
||||
@@ -11,70 +11,72 @@ import (
|
||||
|
||||
func TestCreateEventPredicate_CreateEvent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
reloadOnCreate bool
|
||||
syncAfterRestart bool
|
||||
initialized bool
|
||||
expectedResult bool
|
||||
name string
|
||||
reloadOnCreate bool
|
||||
syncAfterRestart bool
|
||||
initialized bool
|
||||
expectedResult bool
|
||||
}{
|
||||
{
|
||||
name: "reload on create enabled, initialized",
|
||||
reloadOnCreate: true,
|
||||
syncAfterRestart: false,
|
||||
initialized: true,
|
||||
expectedResult: true,
|
||||
name: "reload on create enabled, initialized",
|
||||
reloadOnCreate: true,
|
||||
syncAfterRestart: false,
|
||||
initialized: true,
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "reload on create disabled, initialized",
|
||||
reloadOnCreate: false,
|
||||
syncAfterRestart: false,
|
||||
initialized: true,
|
||||
expectedResult: false,
|
||||
name: "reload on create disabled, initialized",
|
||||
reloadOnCreate: false,
|
||||
syncAfterRestart: false,
|
||||
initialized: true,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "not initialized, sync after restart enabled",
|
||||
reloadOnCreate: true,
|
||||
syncAfterRestart: true,
|
||||
initialized: false,
|
||||
expectedResult: true,
|
||||
name: "not initialized, sync after restart enabled",
|
||||
reloadOnCreate: true,
|
||||
syncAfterRestart: true,
|
||||
initialized: false,
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "not initialized, sync after restart disabled",
|
||||
reloadOnCreate: true,
|
||||
syncAfterRestart: false,
|
||||
initialized: false,
|
||||
expectedResult: false,
|
||||
name: "not initialized, sync after restart disabled",
|
||||
reloadOnCreate: true,
|
||||
syncAfterRestart: false,
|
||||
initialized: false,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "not initialized, sync after restart disabled, reload on create disabled",
|
||||
reloadOnCreate: false,
|
||||
syncAfterRestart: false,
|
||||
initialized: false,
|
||||
expectedResult: false,
|
||||
name: "not initialized, sync after restart disabled, reload on create disabled",
|
||||
reloadOnCreate: false,
|
||||
syncAfterRestart: false,
|
||||
initialized: false,
|
||||
expectedResult: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
ReloadOnCreate: tt.reloadOnCreate,
|
||||
SyncAfterRestart: tt.syncAfterRestart,
|
||||
}
|
||||
initialized := tt.initialized
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
ReloadOnCreate: tt.reloadOnCreate,
|
||||
SyncAfterRestart: tt.syncAfterRestart,
|
||||
}
|
||||
initialized := tt.initialized
|
||||
|
||||
pred := createEventPredicate(cfg, &initialized)
|
||||
pred := createEventPredicate(cfg, &initialized)
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}
|
||||
|
||||
e := event.CreateEvent{Object: cm}
|
||||
result := pred.Create(e)
|
||||
e := event.CreateEvent{Object: cm}
|
||||
result := pred.Create(e)
|
||||
|
||||
if result != tt.expectedResult {
|
||||
t.Errorf("CreateFunc() = %v, want %v", result, tt.expectedResult)
|
||||
}
|
||||
})
|
||||
if result != tt.expectedResult {
|
||||
t.Errorf("CreateFunc() = %v, want %v", result, tt.expectedResult)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +93,6 @@ func TestCreateEventPredicate_UpdateEvent(t *testing.T) {
|
||||
e := event.UpdateEvent{ObjectOld: cm, ObjectNew: cm}
|
||||
result := pred.Update(e)
|
||||
|
||||
// Update events should always return true
|
||||
if !result {
|
||||
t.Error("UpdateFunc() should always return true")
|
||||
}
|
||||
@@ -116,25 +117,27 @@ func TestCreateEventPredicate_DeleteEvent(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
ReloadOnDelete: tt.reloadOnDelete,
|
||||
}
|
||||
initialized := true
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
ReloadOnDelete: tt.reloadOnDelete,
|
||||
}
|
||||
initialized := true
|
||||
|
||||
pred := createEventPredicate(cfg, &initialized)
|
||||
pred := createEventPredicate(cfg, &initialized)
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}
|
||||
|
||||
e := event.DeleteEvent{Object: cm}
|
||||
result := pred.Delete(e)
|
||||
e := event.DeleteEvent{Object: cm}
|
||||
result := pred.Delete(e)
|
||||
|
||||
if result != tt.expectedResult {
|
||||
t.Errorf("DeleteFunc() = %v, want %v", result, tt.expectedResult)
|
||||
}
|
||||
})
|
||||
if result != tt.expectedResult {
|
||||
t.Errorf("DeleteFunc() = %v, want %v", result, tt.expectedResult)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +154,6 @@ func TestCreateEventPredicate_GenericEvent(t *testing.T) {
|
||||
e := event.GenericEvent{Object: cm}
|
||||
result := pred.Generic(e)
|
||||
|
||||
// Generic events should always return false
|
||||
if result {
|
||||
t.Error("GenericFunc() should always return false")
|
||||
}
|
||||
@@ -164,17 +166,14 @@ func TestBuildEventFilter(t *testing.T) {
|
||||
}
|
||||
initialized := true
|
||||
|
||||
// Create a simple always-true predicate as the resource predicate
|
||||
resourcePred := &alwaysTruePredicate{}
|
||||
|
||||
filter := BuildEventFilter(resourcePred, cfg, &initialized)
|
||||
|
||||
// The filter should be created without error
|
||||
if filter == nil {
|
||||
t.Fatal("BuildEventFilter() should return a non-nil predicate")
|
||||
}
|
||||
|
||||
// Test update event passes (since resourcePred returns true and update always returns true)
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}
|
||||
@@ -182,7 +181,6 @@ func TestBuildEventFilter(t *testing.T) {
|
||||
e := event.UpdateEvent{ObjectOld: cm, ObjectNew: cm}
|
||||
result := filter.Update(e)
|
||||
|
||||
// Since namespace filter is empty (all namespaces allowed), this should pass
|
||||
if !result {
|
||||
t.Error("UpdateFunc() should return true when all predicates pass")
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
@@ -81,6 +82,37 @@ func NewManager(opts ManagerOptions) (ctrl.Manager, error) {
|
||||
return mgr, nil
|
||||
}
|
||||
|
||||
// NewManagerWithRestConfig creates a new controller-runtime manager with the given rest.Config.
|
||||
// This is useful for testing where you have a pre-existing cluster configuration.
|
||||
func NewManagerWithRestConfig(opts ManagerOptions, restConfig *rest.Config) (ctrl.Manager, error) {
|
||||
cfg := opts.Config
|
||||
le := cfg.LeaderElection
|
||||
|
||||
mgrOpts := ctrl.Options{
|
||||
Scheme: runtimeScheme,
|
||||
Metrics: ctrlmetrics.Options{
|
||||
BindAddress: "0", // Disable metrics server in tests
|
||||
},
|
||||
HealthProbeBindAddress: "0", // Disable health probes in tests
|
||||
|
||||
// Leader election configuration
|
||||
LeaderElection: cfg.EnableHA,
|
||||
LeaderElectionID: le.LockName,
|
||||
LeaderElectionNamespace: le.Namespace,
|
||||
LeaderElectionReleaseOnCancel: le.ReleaseOnCancel,
|
||||
LeaseDuration: &le.LeaseDuration,
|
||||
RenewDeadline: &le.RenewDeadline,
|
||||
RetryPeriod: &le.RetryPeriod,
|
||||
}
|
||||
|
||||
mgr, err := ctrl.NewManager(restConfig, mgrOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating manager: %w", err)
|
||||
}
|
||||
|
||||
return mgr, nil
|
||||
}
|
||||
|
||||
// SetupReconcilers sets up all reconcilers with the manager.
|
||||
func SetupReconcilers(mgr ctrl.Manager, cfg *config.Config, log logr.Logger, collectors *metrics.Collectors) error {
|
||||
registry := workload.NewRegistry(cfg.ArgoRolloutsEnabled)
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/go-logr/logr/testr"
|
||||
"github.com/stakater/Reloader/internal/pkg/config"
|
||||
"github.com/stakater/Reloader/internal/pkg/controller"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"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 TestNamespaceCache_Basic(t *testing.T) {
|
||||
cache := controller.NewNamespaceCache(true)
|
||||
|
||||
// Test Add and Contains
|
||||
cache.Add("namespace-1")
|
||||
if !cache.Contains("namespace-1") {
|
||||
t.Error("Cache should contain namespace-1")
|
||||
@@ -28,7 +19,6 @@ func TestNamespaceCache_Basic(t *testing.T) {
|
||||
t.Error("Cache should not contain namespace-2")
|
||||
}
|
||||
|
||||
// Test Remove
|
||||
cache.Remove("namespace-1")
|
||||
if cache.Contains("namespace-1") {
|
||||
t.Error("Cache should not contain namespace-1 after removal")
|
||||
@@ -38,13 +28,9 @@ func TestNamespaceCache_Basic(t *testing.T) {
|
||||
func TestNamespaceCache_Disabled(t *testing.T) {
|
||||
cache := controller.NewNamespaceCache(false)
|
||||
|
||||
// When disabled, Contains should always return true
|
||||
if !cache.Contains("any-namespace") {
|
||||
t.Error("Disabled cache should return true for any namespace")
|
||||
}
|
||||
if !cache.Contains("other-namespace") {
|
||||
t.Error("Disabled cache should return true for any namespace")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceCache_List(t *testing.T) {
|
||||
@@ -58,7 +44,6 @@ func TestNamespaceCache_List(t *testing.T) {
|
||||
t.Errorf("Expected 3 namespaces, got %d", len(list))
|
||||
}
|
||||
|
||||
// Check all namespaces are in the list
|
||||
found := make(map[string]bool)
|
||||
for _, ns := range list {
|
||||
found[ns] = true
|
||||
@@ -71,53 +56,24 @@ func TestNamespaceCache_List(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNamespaceCache_IsEnabled(t *testing.T) {
|
||||
enabledCache := controller.NewNamespaceCache(true)
|
||||
disabledCache := controller.NewNamespaceCache(false)
|
||||
|
||||
if !enabledCache.IsEnabled() {
|
||||
if !controller.NewNamespaceCache(true).IsEnabled() {
|
||||
t.Error("EnabledCache.IsEnabled() should return true")
|
||||
}
|
||||
if disabledCache.IsEnabled() {
|
||||
if controller.NewNamespaceCache(false).IsEnabled() {
|
||||
t.Error("DisabledCache.IsEnabled() should return false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceReconciler_Add(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
|
||||
ns := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ns",
|
||||
Labels: map[string]string{"env": "production"},
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithObjects(ns).
|
||||
Build()
|
||||
|
||||
cfg := config.NewDefault()
|
||||
selector, _ := labels.Parse("env=production")
|
||||
cfg.NamespaceSelectors = []labels.Selector{selector}
|
||||
|
||||
cache := controller.NewNamespaceCache(true)
|
||||
reconciler := &controller.NamespaceReconciler{
|
||||
Client: fakeClient,
|
||||
Log: testr.New(t),
|
||||
Config: cfg,
|
||||
Cache: cache,
|
||||
}
|
||||
ns := testNamespace("test-ns", map[string]string{"env": "production"})
|
||||
reconciler := newNamespaceReconciler(t, cfg, cache, ns)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "test-ns"},
|
||||
}
|
||||
|
||||
_, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
assertReconcileSuccess(t, reconciler, namespaceRequest("test-ns"))
|
||||
|
||||
if !cache.Contains("test-ns") {
|
||||
t.Error("Cache should contain test-ns after reconcile")
|
||||
@@ -125,45 +81,17 @@ func TestNamespaceReconciler_Add(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNamespaceReconciler_Remove_LabelChange(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
|
||||
// Namespace with non-matching labels
|
||||
ns := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ns",
|
||||
Labels: map[string]string{"env": "staging"},
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithObjects(ns).
|
||||
Build()
|
||||
|
||||
cfg := config.NewDefault()
|
||||
selector, _ := labels.Parse("env=production")
|
||||
cfg.NamespaceSelectors = []labels.Selector{selector}
|
||||
|
||||
cache := controller.NewNamespaceCache(true)
|
||||
// Pre-populate cache
|
||||
cache.Add("test-ns")
|
||||
cache.Add("test-ns") // Pre-populate
|
||||
|
||||
reconciler := &controller.NamespaceReconciler{
|
||||
Client: fakeClient,
|
||||
Log: testr.New(t),
|
||||
Config: cfg,
|
||||
Cache: cache,
|
||||
}
|
||||
ns := testNamespace("test-ns", map[string]string{"env": "staging"}) // Non-matching
|
||||
reconciler := newNamespaceReconciler(t, cfg, cache, ns)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "test-ns"},
|
||||
}
|
||||
|
||||
_, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
assertReconcileSuccess(t, reconciler, namespaceRequest("test-ns"))
|
||||
|
||||
if cache.Contains("test-ns") {
|
||||
t.Error("Cache should not contain test-ns after reconcile (labels no longer match)")
|
||||
@@ -171,37 +99,16 @@ func TestNamespaceReconciler_Remove_LabelChange(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNamespaceReconciler_Remove_Delete(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
|
||||
// No namespace in cluster (simulates delete)
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
Build()
|
||||
|
||||
cfg := config.NewDefault()
|
||||
selector, _ := labels.Parse("env=production")
|
||||
cfg.NamespaceSelectors = []labels.Selector{selector}
|
||||
|
||||
cache := controller.NewNamespaceCache(true)
|
||||
// Pre-populate cache
|
||||
cache.Add("deleted-ns")
|
||||
cache.Add("deleted-ns") // Pre-populate
|
||||
|
||||
reconciler := &controller.NamespaceReconciler{
|
||||
Client: fakeClient,
|
||||
Log: testr.New(t),
|
||||
Config: cfg,
|
||||
Cache: cache,
|
||||
}
|
||||
reconciler := newNamespaceReconciler(t, cfg, cache) // No namespace in cluster
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "deleted-ns"},
|
||||
}
|
||||
|
||||
_, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
assertReconcileSuccess(t, reconciler, namespaceRequest("deleted-ns"))
|
||||
|
||||
if cache.Contains("deleted-ns") {
|
||||
t.Error("Cache should not contain deleted-ns after reconcile")
|
||||
@@ -209,85 +116,32 @@ func TestNamespaceReconciler_Remove_Delete(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNamespaceReconciler_MultipleSelectors(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
|
||||
ns := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ns",
|
||||
Labels: map[string]string{"team": "platform"},
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithObjects(ns).
|
||||
Build()
|
||||
|
||||
cfg := config.NewDefault()
|
||||
selector1, _ := labels.Parse("env=production")
|
||||
selector2, _ := labels.Parse("team=platform")
|
||||
cfg.NamespaceSelectors = []labels.Selector{selector1, selector2}
|
||||
|
||||
cache := controller.NewNamespaceCache(true)
|
||||
reconciler := &controller.NamespaceReconciler{
|
||||
Client: fakeClient,
|
||||
Log: testr.New(t),
|
||||
Config: cfg,
|
||||
Cache: cache,
|
||||
}
|
||||
ns := testNamespace("test-ns", map[string]string{"team": "platform"})
|
||||
reconciler := newNamespaceReconciler(t, cfg, cache, ns)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "test-ns"},
|
||||
}
|
||||
assertReconcileSuccess(t, reconciler, namespaceRequest("test-ns"))
|
||||
|
||||
_, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
|
||||
// Should be added because it matches second selector (team=platform)
|
||||
if !cache.Contains("test-ns") {
|
||||
t.Error("Cache should contain test-ns (matches second selector)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceReconciler_NoLabels(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
|
||||
// Namespace with no labels
|
||||
ns := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ns",
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithObjects(ns).
|
||||
Build()
|
||||
|
||||
cfg := config.NewDefault()
|
||||
selector, _ := labels.Parse("env=production")
|
||||
cfg.NamespaceSelectors = []labels.Selector{selector}
|
||||
|
||||
cache := controller.NewNamespaceCache(true)
|
||||
reconciler := &controller.NamespaceReconciler{
|
||||
Client: fakeClient,
|
||||
Log: testr.New(t),
|
||||
Config: cfg,
|
||||
Cache: cache,
|
||||
}
|
||||
ns := testNamespace("test-ns", nil) // No labels
|
||||
reconciler := newNamespaceReconciler(t, cfg, cache, ns)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "test-ns"},
|
||||
}
|
||||
|
||||
_, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
assertReconcileSuccess(t, reconciler, namespaceRequest("test-ns"))
|
||||
|
||||
if cache.Contains("test-ns") {
|
||||
t.Error("Cache should not contain test-ns (no labels)")
|
||||
|
||||
@@ -1,118 +1,286 @@
|
||||
package controller
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stakater/Reloader/internal/pkg/config"
|
||||
"github.com/stakater/Reloader/internal/pkg/controller"
|
||||
"github.com/stakater/Reloader/internal/pkg/reload"
|
||||
"github.com/stakater/Reloader/internal/pkg/workload"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
func TestUpdateWorkloadWithRetry_SwitchCases(t *testing.T) {
|
||||
// Test that the switch statement correctly identifies workload types
|
||||
// Note: Full integration tests require a fake k8s client, so we just test type detection
|
||||
|
||||
func TestUpdateWorkloadWithRetry_WorkloadTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
workload workload.WorkloadAccessor
|
||||
expectedKind workload.Kind
|
||||
object runtime.Object
|
||||
workload func(runtime.Object) workload.WorkloadAccessor
|
||||
resourceType reload.ResourceType
|
||||
verify func(t *testing.T, c client.Client)
|
||||
}{
|
||||
{
|
||||
name: "deployment workload",
|
||||
workload: workload.NewDeploymentWorkload(&appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}),
|
||||
expectedKind: workload.KindDeployment,
|
||||
name: "Deployment",
|
||||
object: testDeployment("test-deployment", "default", nil),
|
||||
workload: func(o runtime.Object) workload.WorkloadAccessor {
|
||||
return workload.NewDeploymentWorkload(o.(*appsv1.Deployment))
|
||||
},
|
||||
resourceType: reload.ResourceTypeConfigMap,
|
||||
verify: func(t *testing.T, c client.Client) {
|
||||
var result appsv1.Deployment
|
||||
if err := c.Get(context.Background(), types.NamespacedName{Name: "test-deployment", Namespace: "default"}, &result); err != nil {
|
||||
t.Fatalf("Failed to get deployment: %v", err)
|
||||
}
|
||||
if result.Spec.Template.Annotations == nil {
|
||||
t.Fatal("Expected pod template annotations to be set")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "daemonset workload",
|
||||
workload: workload.NewDaemonSetWorkload(&appsv1.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}),
|
||||
expectedKind: workload.KindDaemonSet,
|
||||
name: "DaemonSet",
|
||||
object: testDaemonSet("test-daemonset", "default", nil),
|
||||
workload: func(o runtime.Object) workload.WorkloadAccessor {
|
||||
return workload.NewDaemonSetWorkload(o.(*appsv1.DaemonSet))
|
||||
},
|
||||
resourceType: reload.ResourceTypeSecret,
|
||||
verify: func(t *testing.T, c client.Client) {
|
||||
var result appsv1.DaemonSet
|
||||
if err := c.Get(context.Background(), types.NamespacedName{Name: "test-daemonset", Namespace: "default"}, &result); err != nil {
|
||||
t.Fatalf("Failed to get daemonset: %v", err)
|
||||
}
|
||||
if result.Spec.Template.Annotations == nil {
|
||||
t.Fatal("Expected pod template annotations to be set")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "statefulset workload",
|
||||
workload: workload.NewStatefulSetWorkload(&appsv1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}),
|
||||
expectedKind: workload.KindStatefulSet,
|
||||
name: "StatefulSet",
|
||||
object: testStatefulSet("test-statefulset", "default", nil),
|
||||
workload: func(o runtime.Object) workload.WorkloadAccessor {
|
||||
return workload.NewStatefulSetWorkload(o.(*appsv1.StatefulSet))
|
||||
},
|
||||
resourceType: reload.ResourceTypeConfigMap,
|
||||
verify: func(t *testing.T, c client.Client) {
|
||||
var result appsv1.StatefulSet
|
||||
if err := c.Get(context.Background(), types.NamespacedName{Name: "test-statefulset", Namespace: "default"}, &result); err != nil {
|
||||
t.Fatalf("Failed to get statefulset: %v", err)
|
||||
}
|
||||
if result.Spec.Template.Annotations == nil {
|
||||
t.Fatal("Expected pod template annotations to be set")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "job workload",
|
||||
workload: workload.NewJobWorkload(&batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}),
|
||||
expectedKind: workload.KindJob,
|
||||
name: "Job",
|
||||
object: testJob("test-job", "default"),
|
||||
workload: func(o runtime.Object) workload.WorkloadAccessor {
|
||||
return workload.NewJobWorkload(o.(*batchv1.Job))
|
||||
},
|
||||
resourceType: reload.ResourceTypeConfigMap,
|
||||
verify: func(t *testing.T, c client.Client) {
|
||||
var jobs batchv1.JobList
|
||||
if err := c.List(context.Background(), &jobs, client.InNamespace("default")); err != nil {
|
||||
t.Fatalf("Failed to list jobs: %v", err)
|
||||
}
|
||||
if len(jobs.Items) != 1 {
|
||||
t.Errorf("Expected 1 job (recreated), got %d", len(jobs.Items))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cronjob workload",
|
||||
workload: workload.NewCronJobWorkload(&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}),
|
||||
expectedKind: workload.KindCronJob,
|
||||
name: "CronJob",
|
||||
object: testCronJob("test-cronjob", "default"),
|
||||
workload: func(o runtime.Object) workload.WorkloadAccessor {
|
||||
return workload.NewCronJobWorkload(o.(*batchv1.CronJob))
|
||||
},
|
||||
resourceType: reload.ResourceTypeSecret,
|
||||
verify: func(t *testing.T, c client.Client) {
|
||||
var jobs batchv1.JobList
|
||||
if err := c.List(context.Background(), &jobs, client.InNamespace("default")); err != nil {
|
||||
t.Fatalf("Failed to list jobs: %v", err)
|
||||
}
|
||||
if len(jobs.Items) != 1 {
|
||||
t.Errorf("Expected 1 job from cronjob, got %d", len(jobs.Items))
|
||||
}
|
||||
if len(jobs.Items) > 0 && jobs.Items[0].Annotations["cronjob.kubernetes.io/instantiate"] != "manual" {
|
||||
t.Error("Expected job to have manual instantiate annotation")
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Verify the workload kind is correctly identified
|
||||
if tt.workload.Kind() != tt.expectedKind {
|
||||
t.Errorf("workload.Kind() = %v, want %v", tt.workload.Kind(), tt.expectedKind)
|
||||
}
|
||||
})
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
reloadService := reload.NewService(cfg)
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(testScheme()).
|
||||
WithRuntimeObjects(tt.object).
|
||||
Build()
|
||||
|
||||
wl := tt.workload(tt.object)
|
||||
|
||||
updated, err := controller.UpdateWorkloadWithRetry(
|
||||
context.Background(),
|
||||
fakeClient,
|
||||
reloadService,
|
||||
wl,
|
||||
"test-resource",
|
||||
tt.resourceType,
|
||||
"default",
|
||||
"abc123",
|
||||
false,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateWorkloadWithRetry failed: %v", err)
|
||||
}
|
||||
if !updated {
|
||||
t.Error("Expected workload to be updated")
|
||||
}
|
||||
|
||||
tt.verify(t, fakeClient)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJobWorkloadTypeCast(t *testing.T) {
|
||||
// Test that JobWorkload type cast works correctly
|
||||
job := &batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-job", Namespace: "default"},
|
||||
}
|
||||
jobWl := workload.NewJobWorkload(job)
|
||||
|
||||
if jobWl.GetName() != "test-job" {
|
||||
t.Errorf("JobWorkload.GetName() = %v, want test-job", jobWl.GetName())
|
||||
}
|
||||
|
||||
// Test GetJob method
|
||||
gotJob := jobWl.GetJob()
|
||||
if gotJob.Name != "test-job" {
|
||||
t.Errorf("JobWorkload.GetJob().Name = %v, want test-job", gotJob.Name)
|
||||
}
|
||||
|
||||
// Verify it satisfies WorkloadAccessor interface
|
||||
var _ workload.WorkloadAccessor = jobWl
|
||||
}
|
||||
|
||||
func TestCronJobWorkloadTypeCast(t *testing.T) {
|
||||
// Test that CronJobWorkload type cast works correctly
|
||||
cronJob := &batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-cronjob", Namespace: "default"},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "*/5 * * * *",
|
||||
func TestUpdateWorkloadWithRetry_Strategies(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
strategy config.ReloadStrategy
|
||||
verify func(t *testing.T, cfg *config.Config, result *appsv1.Deployment)
|
||||
}{
|
||||
{
|
||||
name: "EnvVarStrategy",
|
||||
strategy: config.ReloadStrategyEnvVars,
|
||||
verify: func(t *testing.T, cfg *config.Config, result *appsv1.Deployment) {
|
||||
found := false
|
||||
for _, env := range result.Spec.Template.Spec.Containers[0].Env {
|
||||
if env.Name == "STAKATER_TEST_CM_CONFIGMAP" && env.Value == "abc123" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Expected STAKATER_TEST_CM_CONFIGMAP env var to be set")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AnnotationStrategy",
|
||||
strategy: config.ReloadStrategyAnnotations,
|
||||
verify: func(t *testing.T, cfg *config.Config, result *appsv1.Deployment) {
|
||||
if result.Spec.Template.Annotations == nil {
|
||||
t.Fatal("Expected pod template annotations to be set")
|
||||
}
|
||||
if _, ok := result.Spec.Template.Annotations[cfg.Annotations.LastReloadedFrom]; !ok {
|
||||
t.Errorf("Expected %s annotation to be set", cfg.Annotations.LastReloadedFrom)
|
||||
}
|
||||
for _, env := range result.Spec.Template.Spec.Containers[0].Env {
|
||||
if env.Name == "STAKATER_TEST_CM_CONFIGMAP" {
|
||||
t.Error("Annotation strategy should not add env vars")
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
cronJobWl := workload.NewCronJobWorkload(cronJob)
|
||||
|
||||
if cronJobWl.GetName() != "test-cronjob" {
|
||||
t.Errorf("CronJobWorkload.GetName() = %v, want test-cronjob", cronJobWl.GetName())
|
||||
for _, tt := range tests {
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.ReloadStrategy = tt.strategy
|
||||
reloadService := reload.NewService(cfg)
|
||||
|
||||
deployment := testDeployment("test-deployment", "default", nil)
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(testScheme()).
|
||||
WithObjects(deployment).
|
||||
Build()
|
||||
|
||||
wl := workload.NewDeploymentWorkload(deployment)
|
||||
|
||||
updated, err := controller.UpdateWorkloadWithRetry(
|
||||
context.Background(),
|
||||
fakeClient,
|
||||
reloadService,
|
||||
wl,
|
||||
"test-cm",
|
||||
reload.ResourceTypeConfigMap,
|
||||
"default",
|
||||
"abc123",
|
||||
false,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateWorkloadWithRetry failed: %v", err)
|
||||
}
|
||||
if !updated {
|
||||
t.Error("Expected workload to be updated")
|
||||
}
|
||||
|
||||
var result appsv1.Deployment
|
||||
if err := fakeClient.Get(
|
||||
context.Background(), types.NamespacedName{Name: "test-deployment", Namespace: "default"}, &result,
|
||||
); err != nil {
|
||||
t.Fatalf("Failed to get deployment: %v", err)
|
||||
}
|
||||
|
||||
tt.verify(t, cfg, &result)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateWorkloadWithRetry_NoUpdate(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
reloadService := reload.NewService(cfg)
|
||||
|
||||
deployment := testDeployment("test-deployment", "default", nil)
|
||||
deployment.Spec.Template.Spec.Containers[0].Env = []corev1.EnvVar{
|
||||
{
|
||||
Name: "STAKATER_TEST_CM_CONFIGMAP",
|
||||
Value: "abc123",
|
||||
},
|
||||
}
|
||||
|
||||
// Test GetCronJob method
|
||||
gotCronJob := cronJobWl.GetCronJob()
|
||||
if gotCronJob.Name != "test-cronjob" {
|
||||
t.Errorf("CronJobWorkload.GetCronJob().Name = %v, want test-cronjob", gotCronJob.Name)
|
||||
}
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(testScheme()).
|
||||
WithObjects(deployment).
|
||||
Build()
|
||||
|
||||
// Verify it satisfies WorkloadAccessor interface
|
||||
var _ workload.WorkloadAccessor = cronJobWl
|
||||
wl := workload.NewDeploymentWorkload(deployment)
|
||||
|
||||
updated, err := controller.UpdateWorkloadWithRetry(
|
||||
context.Background(),
|
||||
fakeClient,
|
||||
reloadService,
|
||||
wl,
|
||||
"test-cm",
|
||||
reload.ResourceTypeConfigMap,
|
||||
"default",
|
||||
"abc123", // Same hash as already set
|
||||
false,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateWorkloadWithRetry failed: %v", err)
|
||||
}
|
||||
if updated {
|
||||
t.Error("Expected workload NOT to be updated (same hash)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceTypeKind(t *testing.T) {
|
||||
// Test that ResourceType.Kind() returns correct values
|
||||
tests := []struct {
|
||||
resourceType reload.ResourceType
|
||||
expectedKind string
|
||||
@@ -122,10 +290,12 @@ func TestResourceTypeKind(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.resourceType), func(t *testing.T) {
|
||||
if got := tt.resourceType.Kind(); got != tt.expectedKind {
|
||||
t.Errorf("ResourceType.Kind() = %v, want %v", got, tt.expectedKind)
|
||||
}
|
||||
})
|
||||
t.Run(
|
||||
string(tt.resourceType), func(t *testing.T) {
|
||||
if got := tt.resourceType.Kind(); got != tt.expectedKind {
|
||||
t.Errorf("ResourceType.Kind() = %v, want %v", got, tt.expectedKind)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,418 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// testScheme is a shared scheme for all controller tests.
|
||||
func testScheme() *runtime.Scheme {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
_ = appsv1.AddToScheme(scheme)
|
||||
_ = batchv1.AddToScheme(scheme)
|
||||
return scheme
|
||||
}
|
||||
|
||||
// newConfigMapReconciler creates a ConfigMapReconciler for testing.
|
||||
func newConfigMapReconciler(t *testing.T, cfg *config.Config, objects ...runtime.Object) *controller.ConfigMapReconciler {
|
||||
t.Helper()
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(testScheme()).
|
||||
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{},
|
||||
}
|
||||
}
|
||||
|
||||
// newSecretReconciler creates a SecretReconciler for testing.
|
||||
func newSecretReconciler(t *testing.T, cfg *config.Config, objects ...runtime.Object) *controller.SecretReconciler {
|
||||
t.Helper()
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(testScheme()).
|
||||
WithRuntimeObjects(objects...).
|
||||
Build()
|
||||
|
||||
collectors := metrics.NewCollectors()
|
||||
|
||||
return &controller.SecretReconciler{
|
||||
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{},
|
||||
}
|
||||
}
|
||||
|
||||
// testConfigMap creates a ConfigMap for testing.
|
||||
func testConfigMap(name, namespace string) *corev1.ConfigMap {
|
||||
return &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
}
|
||||
|
||||
// testConfigMapWithAnnotations creates a ConfigMap with annotations.
|
||||
func testConfigMapWithAnnotations(name, namespace string, annotations map[string]string) *corev1.ConfigMap {
|
||||
cm := testConfigMap(name, namespace)
|
||||
cm.Annotations = annotations
|
||||
return cm
|
||||
}
|
||||
|
||||
// testSecret creates a Secret for testing.
|
||||
func testSecret(name, namespace string) *corev1.Secret {
|
||||
return &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Data: map[string][]byte{"key": []byte("value")},
|
||||
}
|
||||
}
|
||||
|
||||
// testSecretWithAnnotations creates a Secret with annotations.
|
||||
func testSecretWithAnnotations(name, namespace string, annotations map[string]string) *corev1.Secret {
|
||||
secret := testSecret(name, namespace)
|
||||
secret.Annotations = annotations
|
||||
return secret
|
||||
}
|
||||
|
||||
// testDeployment creates a minimal Deployment for testing.
|
||||
func testDeployment(name, namespace string, annotations map[string]string) *appsv1.Deployment {
|
||||
return &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: annotations,
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": name},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": name},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// testDeploymentWithEnvFrom creates a Deployment with EnvFrom referencing a ConfigMap or Secret.
|
||||
func testDeploymentWithEnvFrom(name, namespace string, configMapName, secretName string) *appsv1.Deployment {
|
||||
d := testDeployment(name, namespace, nil)
|
||||
if configMapName != "" {
|
||||
d.Spec.Template.Spec.Containers[0].EnvFrom = append(
|
||||
d.Spec.Template.Spec.Containers[0].EnvFrom,
|
||||
corev1.EnvFromSource{
|
||||
ConfigMapRef: &corev1.ConfigMapEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{Name: configMapName},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
if secretName != "" {
|
||||
d.Spec.Template.Spec.Containers[0].EnvFrom = append(
|
||||
d.Spec.Template.Spec.Containers[0].EnvFrom,
|
||||
corev1.EnvFromSource{
|
||||
SecretRef: &corev1.SecretEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{Name: secretName},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// testDeploymentWithVolume creates a Deployment with a volume from ConfigMap or Secret.
|
||||
func testDeploymentWithVolume(name, namespace string, configMapName, secretName string) *appsv1.Deployment {
|
||||
d := testDeployment(name, namespace, nil)
|
||||
d.Spec.Template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{
|
||||
{
|
||||
Name: "config",
|
||||
MountPath: "/etc/config",
|
||||
},
|
||||
}
|
||||
|
||||
if configMapName != "" {
|
||||
d.Spec.Template.Spec.Volumes = []corev1.Volume{
|
||||
{
|
||||
Name: "config",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
ConfigMap: &corev1.ConfigMapVolumeSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{Name: configMapName},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
if secretName != "" {
|
||||
d.Spec.Template.Spec.Volumes = []corev1.Volume{
|
||||
{
|
||||
Name: "config",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
SecretName: secretName,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// testDeploymentWithProjectedVolume creates a Deployment with a projected volume.
|
||||
func testDeploymentWithProjectedVolume(name, namespace string, configMapName, secretName string) *appsv1.Deployment {
|
||||
d := testDeployment(name, namespace, nil)
|
||||
d.Spec.Template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{
|
||||
{
|
||||
Name: "config",
|
||||
MountPath: "/etc/config",
|
||||
},
|
||||
}
|
||||
|
||||
var sources []corev1.VolumeProjection
|
||||
if configMapName != "" {
|
||||
sources = append(
|
||||
sources, corev1.VolumeProjection{
|
||||
ConfigMap: &corev1.ConfigMapProjection{
|
||||
LocalObjectReference: corev1.LocalObjectReference{Name: configMapName},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
if secretName != "" {
|
||||
sources = append(
|
||||
sources, corev1.VolumeProjection{
|
||||
Secret: &corev1.SecretProjection{
|
||||
LocalObjectReference: corev1.LocalObjectReference{Name: secretName},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
d.Spec.Template.Spec.Volumes = []corev1.Volume{
|
||||
{
|
||||
Name: "config",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Projected: &corev1.ProjectedVolumeSource{Sources: sources},
|
||||
},
|
||||
},
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// testDaemonSet creates a minimal DaemonSet for testing.
|
||||
func testDaemonSet(name, namespace string, annotations map[string]string) *appsv1.DaemonSet {
|
||||
return &appsv1.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: annotations,
|
||||
},
|
||||
Spec: appsv1.DaemonSetSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": name},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": name},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// testStatefulSet creates a minimal StatefulSet for testing.
|
||||
func testStatefulSet(name, namespace string, annotations map[string]string) *appsv1.StatefulSet {
|
||||
return &appsv1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: annotations,
|
||||
},
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": name},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": name},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// reconcileRequest creates a ctrl.Request for the given name and namespace.
|
||||
func reconcileRequest(name, namespace string) ctrl.Request {
|
||||
return ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// namespaceRequest creates a ctrl.Request for a namespace (no namespace field needed).
|
||||
func namespaceRequest(name string) ctrl.Request {
|
||||
return ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{Name: name},
|
||||
}
|
||||
}
|
||||
|
||||
// testNamespace creates a Namespace with optional labels.
|
||||
func testNamespace(name string, labels map[string]string) *corev1.Namespace {
|
||||
return &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Labels: labels,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// newNamespaceReconciler creates a NamespaceReconciler for testing.
|
||||
func newNamespaceReconciler(t *testing.T, cfg *config.Config, cache *controller.NamespaceCache, objects ...runtime.Object) *controller.NamespaceReconciler {
|
||||
t.Helper()
|
||||
scheme := runtime.NewScheme()
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithRuntimeObjects(objects...).
|
||||
Build()
|
||||
|
||||
return &controller.NamespaceReconciler{
|
||||
Client: fakeClient,
|
||||
Log: testr.New(t),
|
||||
Config: cfg,
|
||||
Cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
// assertReconcileSuccess runs reconcile and asserts no error and no requeue.
|
||||
func assertReconcileSuccess(t *testing.T, reconciler interface {
|
||||
Reconcile(context.Context, ctrl.Request) (ctrl.Result, error)
|
||||
}, req ctrl.Request) {
|
||||
t.Helper()
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
}
|
||||
|
||||
// testJob creates a minimal Job for testing.
|
||||
func testJob(name, namespace string) *batchv1.Job {
|
||||
return &batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "main",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// testCronJob creates a minimal CronJob for testing.
|
||||
func testCronJob(name, namespace string) *batchv1.CronJob {
|
||||
return &batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
UID: "test-uid",
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "*/5 * * * *",
|
||||
JobTemplate: batchv1.JobTemplateSpec{
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "main",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,6 @@ func TestNilRecorder_NoPanic(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRecorder_NilInternalRecorder(t *testing.T) {
|
||||
// Create a Recorder with nil internal recorder (edge case)
|
||||
r := &Recorder{recorder: nil}
|
||||
|
||||
pod := &corev1.Pod{
|
||||
@@ -112,26 +111,10 @@ func TestRecorder_NilInternalRecorder(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
// These should not panic
|
||||
r.ReloadSuccess(pod, "ConfigMap", "my-config")
|
||||
r.ReloadFailed(pod, "Secret", "my-secret", errors.New("test error"))
|
||||
}
|
||||
|
||||
func TestEventConstants(t *testing.T) {
|
||||
if EventTypeNormal != corev1.EventTypeNormal {
|
||||
t.Errorf("EventTypeNormal = %q, want %q", EventTypeNormal, corev1.EventTypeNormal)
|
||||
}
|
||||
if EventTypeWarning != corev1.EventTypeWarning {
|
||||
t.Errorf("EventTypeWarning = %q, want %q", EventTypeWarning, corev1.EventTypeWarning)
|
||||
}
|
||||
if ReasonReloaded != "Reloaded" {
|
||||
t.Errorf("ReasonReloaded = %q, want %q", ReasonReloaded, "Reloaded")
|
||||
}
|
||||
if ReasonReloadFailed != "ReloadFailed" {
|
||||
t.Errorf("ReasonReloadFailed = %q, want %q", ReasonReloadFailed, "ReloadFailed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReloadSuccess_DifferentObjectTypes(t *testing.T) {
|
||||
fakeRecorder := record.NewFakeRecorder(10)
|
||||
r := NewRecorder(fakeRecorder)
|
||||
@@ -155,18 +138,20 @@ func TestReloadSuccess_DifferentObjectTypes(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r.ReloadSuccess(tt.object, "ConfigMap", "my-config")
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
r.ReloadSuccess(tt.object, "ConfigMap", "my-config")
|
||||
|
||||
select {
|
||||
case event := <-fakeRecorder.Events:
|
||||
if event == "" {
|
||||
select {
|
||||
case event := <-fakeRecorder.Events:
|
||||
if event == "" {
|
||||
t.Error("Expected event to be recorded")
|
||||
}
|
||||
default:
|
||||
t.Error("Expected event to be recorded")
|
||||
}
|
||||
default:
|
||||
t.Error("Expected event to be recorded")
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@ var (
|
||||
type MetaInfo struct {
|
||||
// BuildInfo contains information about the build version, commit, and compilation details.
|
||||
BuildInfo BuildInfo `json:"buildInfo"`
|
||||
// ReloaderOptions contains all the configuration options used by this Reloader instance.
|
||||
ReloaderOptions ReloaderOptions `json:"reloaderOptions"`
|
||||
// Config contains all the configuration options used by this Reloader instance.
|
||||
Config *config.Config `json:"config"`
|
||||
// DeploymentInfo contains metadata about the Kubernetes deployment of this instance.
|
||||
DeploymentInfo DeploymentInfo `json:"deploymentInfo"`
|
||||
}
|
||||
@@ -66,56 +66,6 @@ type DeploymentInfo struct {
|
||||
Namespace string `json:"namespace"`
|
||||
}
|
||||
|
||||
// ReloaderOptions contains the configuration options for Reloader.
|
||||
// This is a subset of config.Config that's relevant for the metadata ConfigMap.
|
||||
type ReloaderOptions struct {
|
||||
// AutoReloadAll enables automatic reloading of all resources.
|
||||
AutoReloadAll bool `json:"autoReloadAll"`
|
||||
// ReloadStrategy specifies the strategy used to trigger resource reloads.
|
||||
ReloadStrategy string `json:"reloadStrategy"`
|
||||
// IsArgoRollouts indicates whether support for Argo Rollouts is enabled.
|
||||
IsArgoRollouts bool `json:"isArgoRollouts"`
|
||||
// ReloadOnCreate indicates whether to trigger reloads when resources are created.
|
||||
ReloadOnCreate bool `json:"reloadOnCreate"`
|
||||
// ReloadOnDelete indicates whether to trigger reloads when resources are deleted.
|
||||
ReloadOnDelete bool `json:"reloadOnDelete"`
|
||||
// SyncAfterRestart indicates whether to sync add events after Reloader restarts.
|
||||
SyncAfterRestart bool `json:"syncAfterRestart"`
|
||||
// EnableHA indicates whether High Availability mode is enabled.
|
||||
EnableHA bool `json:"enableHA"`
|
||||
// WebhookURL is the URL to send webhook notifications to.
|
||||
WebhookURL string `json:"webhookUrl"`
|
||||
// LogFormat specifies the log format to use.
|
||||
LogFormat string `json:"logFormat"`
|
||||
// LogLevel specifies the log level to use.
|
||||
LogLevel string `json:"logLevel"`
|
||||
// ResourcesToIgnore is a list of resource types to ignore.
|
||||
ResourcesToIgnore []string `json:"resourcesToIgnore"`
|
||||
// WorkloadTypesToIgnore is a list of workload types to ignore.
|
||||
WorkloadTypesToIgnore []string `json:"workloadTypesToIgnore"`
|
||||
// NamespacesToIgnore is a list of namespaces to ignore.
|
||||
NamespacesToIgnore []string `json:"namespacesToIgnore"`
|
||||
// NamespaceSelectors is a list of namespace label selectors.
|
||||
NamespaceSelectors []string `json:"namespaceSelectors"`
|
||||
// ResourceSelectors is a list of resource label selectors.
|
||||
ResourceSelectors []string `json:"resourceSelectors"`
|
||||
|
||||
// Annotations
|
||||
ConfigmapUpdateOnChangeAnnotation string `json:"configmapUpdateOnChangeAnnotation"`
|
||||
SecretUpdateOnChangeAnnotation string `json:"secretUpdateOnChangeAnnotation"`
|
||||
ReloaderAutoAnnotation string `json:"reloaderAutoAnnotation"`
|
||||
ConfigmapReloaderAutoAnnotation string `json:"configmapReloaderAutoAnnotation"`
|
||||
SecretReloaderAutoAnnotation string `json:"secretReloaderAutoAnnotation"`
|
||||
IgnoreResourceAnnotation string `json:"ignoreResourceAnnotation"`
|
||||
ConfigmapExcludeReloaderAnnotation string `json:"configmapExcludeReloaderAnnotation"`
|
||||
SecretExcludeReloaderAnnotation string `json:"secretExcludeReloaderAnnotation"`
|
||||
AutoSearchAnnotation string `json:"autoSearchAnnotation"`
|
||||
SearchMatchAnnotation string `json:"searchMatchAnnotation"`
|
||||
RolloutStrategyAnnotation string `json:"rolloutStrategyAnnotation"`
|
||||
PauseDeploymentAnnotation string `json:"pauseDeploymentAnnotation"`
|
||||
PauseDeploymentTimeAnnotation string `json:"pauseDeploymentTimeAnnotation"`
|
||||
}
|
||||
|
||||
// NewBuildInfo creates a new BuildInfo with current build information.
|
||||
func NewBuildInfo() BuildInfo {
|
||||
return BuildInfo{
|
||||
@@ -126,45 +76,11 @@ func NewBuildInfo() BuildInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// NewReloaderOptions creates ReloaderOptions from a Config.
|
||||
func NewReloaderOptions(cfg *config.Config) ReloaderOptions {
|
||||
return ReloaderOptions{
|
||||
AutoReloadAll: cfg.AutoReloadAll,
|
||||
ReloadStrategy: string(cfg.ReloadStrategy),
|
||||
IsArgoRollouts: cfg.ArgoRolloutsEnabled,
|
||||
ReloadOnCreate: cfg.ReloadOnCreate,
|
||||
ReloadOnDelete: cfg.ReloadOnDelete,
|
||||
SyncAfterRestart: cfg.SyncAfterRestart,
|
||||
EnableHA: cfg.EnableHA,
|
||||
WebhookURL: cfg.WebhookURL,
|
||||
LogFormat: cfg.LogFormat,
|
||||
LogLevel: cfg.LogLevel,
|
||||
ResourcesToIgnore: cfg.IgnoredResources,
|
||||
WorkloadTypesToIgnore: cfg.IgnoredWorkloads,
|
||||
NamespacesToIgnore: cfg.IgnoredNamespaces,
|
||||
NamespaceSelectors: cfg.NamespaceSelectorStrings,
|
||||
ResourceSelectors: cfg.ResourceSelectorStrings,
|
||||
ConfigmapUpdateOnChangeAnnotation: cfg.Annotations.ConfigmapReload,
|
||||
SecretUpdateOnChangeAnnotation: cfg.Annotations.SecretReload,
|
||||
ReloaderAutoAnnotation: cfg.Annotations.Auto,
|
||||
ConfigmapReloaderAutoAnnotation: cfg.Annotations.ConfigmapAuto,
|
||||
SecretReloaderAutoAnnotation: cfg.Annotations.SecretAuto,
|
||||
IgnoreResourceAnnotation: cfg.Annotations.Ignore,
|
||||
ConfigmapExcludeReloaderAnnotation: cfg.Annotations.ConfigmapExclude,
|
||||
SecretExcludeReloaderAnnotation: cfg.Annotations.SecretExclude,
|
||||
AutoSearchAnnotation: cfg.Annotations.Search,
|
||||
SearchMatchAnnotation: cfg.Annotations.Match,
|
||||
RolloutStrategyAnnotation: cfg.Annotations.RolloutStrategy,
|
||||
PauseDeploymentAnnotation: cfg.Annotations.PausePeriod,
|
||||
PauseDeploymentTimeAnnotation: cfg.Annotations.PausedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// NewMetaInfo creates a new MetaInfo from configuration.
|
||||
func NewMetaInfo(cfg *config.Config) *MetaInfo {
|
||||
return &MetaInfo{
|
||||
BuildInfo: NewBuildInfo(),
|
||||
ReloaderOptions: NewReloaderOptions(cfg),
|
||||
BuildInfo: NewBuildInfo(),
|
||||
Config: cfg,
|
||||
DeploymentInfo: DeploymentInfo{
|
||||
Name: os.Getenv(EnvReloaderDeploymentName),
|
||||
Namespace: os.Getenv(EnvReloaderNamespace),
|
||||
@@ -183,9 +99,9 @@ func (m *MetaInfo) ToConfigMap() *corev1.ConfigMap {
|
||||
},
|
||||
},
|
||||
Data: map[string]string{
|
||||
"buildInfo": toJSON(m.BuildInfo),
|
||||
"reloaderOptions": toJSON(m.ReloaderOptions),
|
||||
"deploymentInfo": toJSON(m.DeploymentInfo),
|
||||
"buildInfo": toJSON(m.BuildInfo),
|
||||
"config": toJSON(m.Config),
|
||||
"deploymentInfo": toJSON(m.DeploymentInfo),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ func testLogger() logr.Logger {
|
||||
}
|
||||
|
||||
func TestNewBuildInfo(t *testing.T) {
|
||||
// Set build variables for testing
|
||||
oldVersion := Version
|
||||
oldCommit := Commit
|
||||
oldBuildDate := BuildDate
|
||||
@@ -49,7 +48,10 @@ func TestNewBuildInfo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewReloaderOptions(t *testing.T) {
|
||||
func TestNewMetaInfo(t *testing.T) {
|
||||
t.Setenv(EnvReloaderNamespace, "test-ns")
|
||||
t.Setenv(EnvReloaderDeploymentName, "test-deploy")
|
||||
|
||||
cfg := config.NewDefault()
|
||||
cfg.AutoReloadAll = true
|
||||
cfg.ReloadStrategy = config.ReloadStrategyAnnotations
|
||||
@@ -64,53 +66,39 @@ func TestNewReloaderOptions(t *testing.T) {
|
||||
cfg.IgnoredWorkloads = []string{"jobs"}
|
||||
cfg.IgnoredNamespaces = []string{"kube-system"}
|
||||
|
||||
opts := NewReloaderOptions(cfg)
|
||||
metaInfo := NewMetaInfo(cfg)
|
||||
|
||||
if !opts.AutoReloadAll {
|
||||
if !metaInfo.Config.AutoReloadAll {
|
||||
t.Error("AutoReloadAll should be true")
|
||||
}
|
||||
if opts.ReloadStrategy != "annotations" {
|
||||
t.Errorf("ReloadStrategy = %s, want annotations", opts.ReloadStrategy)
|
||||
if metaInfo.Config.ReloadStrategy != config.ReloadStrategyAnnotations {
|
||||
t.Errorf("ReloadStrategy = %s, want annotations", metaInfo.Config.ReloadStrategy)
|
||||
}
|
||||
if !opts.IsArgoRollouts {
|
||||
t.Error("IsArgoRollouts should be true")
|
||||
if !metaInfo.Config.ArgoRolloutsEnabled {
|
||||
t.Error("ArgoRolloutsEnabled should be true")
|
||||
}
|
||||
if !opts.ReloadOnCreate {
|
||||
if !metaInfo.Config.ReloadOnCreate {
|
||||
t.Error("ReloadOnCreate should be true")
|
||||
}
|
||||
if !opts.ReloadOnDelete {
|
||||
if !metaInfo.Config.ReloadOnDelete {
|
||||
t.Error("ReloadOnDelete should be true")
|
||||
}
|
||||
if !opts.EnableHA {
|
||||
if !metaInfo.Config.EnableHA {
|
||||
t.Error("EnableHA should be true")
|
||||
}
|
||||
if opts.WebhookURL != "https://example.com/webhook" {
|
||||
t.Errorf("WebhookURL = %s, want https://example.com/webhook", opts.WebhookURL)
|
||||
}
|
||||
if opts.LogFormat != "json" {
|
||||
t.Errorf("LogFormat = %s, want json", opts.LogFormat)
|
||||
}
|
||||
if opts.LogLevel != "debug" {
|
||||
t.Errorf("LogLevel = %s, want debug", opts.LogLevel)
|
||||
}
|
||||
if len(opts.ResourcesToIgnore) != 1 || opts.ResourcesToIgnore[0] != "configmaps" {
|
||||
t.Errorf("ResourcesToIgnore = %v, want [configmaps]", opts.ResourcesToIgnore)
|
||||
}
|
||||
if len(opts.WorkloadTypesToIgnore) != 1 || opts.WorkloadTypesToIgnore[0] != "jobs" {
|
||||
t.Errorf("WorkloadTypesToIgnore = %v, want [jobs]", opts.WorkloadTypesToIgnore)
|
||||
}
|
||||
if len(opts.NamespacesToIgnore) != 1 || opts.NamespacesToIgnore[0] != "kube-system" {
|
||||
t.Errorf("NamespacesToIgnore = %v, want [kube-system]", opts.NamespacesToIgnore)
|
||||
if metaInfo.Config.WebhookURL != "https://example.com/webhook" {
|
||||
t.Errorf("WebhookURL = %s, want https://example.com/webhook", metaInfo.Config.WebhookURL)
|
||||
}
|
||||
|
||||
// Check annotations
|
||||
if opts.ReloaderAutoAnnotation != "reloader.stakater.com/auto" {
|
||||
t.Errorf("ReloaderAutoAnnotation = %s, want reloader.stakater.com/auto", opts.ReloaderAutoAnnotation)
|
||||
if metaInfo.DeploymentInfo.Namespace != "test-ns" {
|
||||
t.Errorf("DeploymentInfo.Namespace = %s, want test-ns", metaInfo.DeploymentInfo.Namespace)
|
||||
}
|
||||
if metaInfo.DeploymentInfo.Name != "test-deploy" {
|
||||
t.Errorf("DeploymentInfo.Name = %s, want test-deploy", metaInfo.DeploymentInfo.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaInfo_ToConfigMap(t *testing.T) {
|
||||
// Set environment variables
|
||||
t.Setenv(EnvReloaderNamespace, "reloader-ns")
|
||||
t.Setenv(EnvReloaderDeploymentName, "reloader-deploy")
|
||||
|
||||
@@ -128,12 +116,11 @@ func TestMetaInfo_ToConfigMap(t *testing.T) {
|
||||
t.Errorf("Label = %s, want %s", cm.Labels[ConfigMapLabelKey], ConfigMapLabelValue)
|
||||
}
|
||||
|
||||
// Check data fields exist
|
||||
if _, ok := cm.Data["buildInfo"]; !ok {
|
||||
t.Error("buildInfo data key missing")
|
||||
}
|
||||
if _, ok := cm.Data["reloaderOptions"]; !ok {
|
||||
t.Error("reloaderOptions data key missing")
|
||||
if _, ok := cm.Data["config"]; !ok {
|
||||
t.Error("config data key missing")
|
||||
}
|
||||
if _, ok := cm.Data["deploymentInfo"]; !ok {
|
||||
t.Error("deploymentInfo data key missing")
|
||||
@@ -145,6 +132,11 @@ func TestMetaInfo_ToConfigMap(t *testing.T) {
|
||||
t.Errorf("buildInfo is not valid JSON: %v", err)
|
||||
}
|
||||
|
||||
var parsedConfig config.Config
|
||||
if err := json.Unmarshal([]byte(cm.Data["config"]), &parsedConfig); err != nil {
|
||||
t.Errorf("config is not valid JSON: %v", err)
|
||||
}
|
||||
|
||||
// Verify deploymentInfo contains expected values
|
||||
var deployInfo DeploymentInfo
|
||||
if err := json.Unmarshal([]byte(cm.Data["deploymentInfo"]), &deployInfo); err != nil {
|
||||
@@ -159,7 +151,6 @@ func TestMetaInfo_ToConfigMap(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPublisher_Publish_NoNamespace(t *testing.T) {
|
||||
// Ensure RELOADER_NAMESPACE is not set (empty value)
|
||||
t.Setenv(EnvReloaderNamespace, "")
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
@@ -176,7 +167,6 @@ func TestPublisher_Publish_NoNamespace(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPublisher_Publish_CreateNew(t *testing.T) {
|
||||
// Set environment variables
|
||||
t.Setenv(EnvReloaderNamespace, "test-ns")
|
||||
t.Setenv(EnvReloaderDeploymentName, "test-deploy")
|
||||
|
||||
@@ -193,7 +183,6 @@ func TestPublisher_Publish_CreateNew(t *testing.T) {
|
||||
t.Errorf("Publish() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify ConfigMap was created
|
||||
cm := &corev1.ConfigMap{}
|
||||
err = fakeClient.Get(ctx, client.ObjectKey{Name: ConfigMapName, Namespace: "test-ns"}, cm)
|
||||
if err != nil {
|
||||
@@ -205,14 +194,12 @@ func TestPublisher_Publish_CreateNew(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPublisher_Publish_UpdateExisting(t *testing.T) {
|
||||
// Set environment variables
|
||||
t.Setenv(EnvReloaderNamespace, "test-ns")
|
||||
t.Setenv(EnvReloaderDeploymentName, "test-deploy")
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
|
||||
// Create existing ConfigMap with old data
|
||||
existingCM := &corev1.ConfigMap{}
|
||||
existingCM.Name = ConfigMapName
|
||||
existingCM.Namespace = "test-ns"
|
||||
@@ -234,32 +221,28 @@ func TestPublisher_Publish_UpdateExisting(t *testing.T) {
|
||||
t.Errorf("Publish() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify ConfigMap was updated
|
||||
cm := &corev1.ConfigMap{}
|
||||
err = fakeClient.Get(ctx, client.ObjectKey{Name: ConfigMapName, Namespace: "test-ns"}, cm)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to get updated ConfigMap: %v", err)
|
||||
}
|
||||
|
||||
// Check that all data keys are present
|
||||
if _, ok := cm.Data["buildInfo"]; !ok {
|
||||
t.Error("buildInfo data key missing after update")
|
||||
}
|
||||
if _, ok := cm.Data["reloaderOptions"]; !ok {
|
||||
t.Error("reloaderOptions data key missing after update")
|
||||
if _, ok := cm.Data["config"]; !ok {
|
||||
t.Error("config data key missing after update")
|
||||
}
|
||||
if _, ok := cm.Data["deploymentInfo"]; !ok {
|
||||
t.Error("deploymentInfo data key missing after update")
|
||||
}
|
||||
|
||||
// Verify labels were added
|
||||
if cm.Labels[ConfigMapLabelKey] != ConfigMapLabelValue {
|
||||
t.Errorf("Label not updated: %s", cm.Labels[ConfigMapLabelKey])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishMetaInfoConfigMap(t *testing.T) {
|
||||
// Set environment variables
|
||||
t.Setenv(EnvReloaderNamespace, "test-ns")
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
@@ -274,7 +257,6 @@ func TestPublishMetaInfoConfigMap(t *testing.T) {
|
||||
t.Errorf("PublishMetaInfoConfigMap() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify ConfigMap was created
|
||||
cm := &corev1.ConfigMap{}
|
||||
err = fakeClient.Get(ctx, client.ObjectKey{Name: ConfigMapName, Namespace: "test-ns"}, cm)
|
||||
if err != nil {
|
||||
@@ -306,17 +288,19 @@ func TestParseUTCTime(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := parseUTCTime(tt.input)
|
||||
if tt.wantErr {
|
||||
if !result.IsZero() {
|
||||
t.Errorf("parseUTCTime(%s) should return zero time", tt.input)
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
result := parseUTCTime(tt.input)
|
||||
if tt.wantErr {
|
||||
if !result.IsZero() {
|
||||
t.Errorf("parseUTCTime(%s) should return zero time", tt.input)
|
||||
}
|
||||
} else {
|
||||
if result.IsZero() {
|
||||
t.Errorf("parseUTCTime(%s) should not return zero time", tt.input)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if result.IsZero() {
|
||||
t.Errorf("parseUTCTime(%s) should not return zero time", tt.input)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ func TestNewCollectors_CreatesCounters(t *testing.T) {
|
||||
func TestNewCollectors_InitializesWithZero(t *testing.T) {
|
||||
collectors := NewCollectors()
|
||||
|
||||
// Check that success=true counter is initialized to 0
|
||||
metric := &dto.Metric{}
|
||||
err := collectors.Reloaded.With(prometheus.Labels{"success": "true"}).Write(metric)
|
||||
if err != nil {
|
||||
@@ -31,7 +30,6 @@ func TestNewCollectors_InitializesWithZero(t *testing.T) {
|
||||
t.Errorf("Initial success=true counter = %v, want 0", metric.Counter.GetValue())
|
||||
}
|
||||
|
||||
// Check that success=false counter is initialized to 0
|
||||
err = collectors.Reloaded.With(prometheus.Labels{"success": "false"}).Write(metric)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get metric: %v", err)
|
||||
@@ -95,17 +93,18 @@ func TestRecordReload_MultipleIncrements(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRecordReload_WithNamespaceTracking(t *testing.T) {
|
||||
// Enable namespace tracking
|
||||
t.Setenv("METRICS_COUNT_BY_NAMESPACE", "enabled")
|
||||
|
||||
collectors := NewCollectors()
|
||||
collectors.RecordReload(true, "kube-system")
|
||||
|
||||
metric := &dto.Metric{}
|
||||
err := collectors.ReloadedByNamespace.With(prometheus.Labels{
|
||||
"success": "true",
|
||||
"namespace": "kube-system",
|
||||
}).Write(metric)
|
||||
err := collectors.ReloadedByNamespace.With(
|
||||
prometheus.Labels{
|
||||
"success": "true",
|
||||
"namespace": "kube-system",
|
||||
},
|
||||
).Write(metric)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get metric: %v", err)
|
||||
}
|
||||
@@ -115,14 +114,11 @@ func TestRecordReload_WithNamespaceTracking(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRecordReload_WithoutNamespaceTracking(t *testing.T) {
|
||||
// Ensure namespace tracking is disabled (t.Setenv to empty resets it)
|
||||
t.Setenv("METRICS_COUNT_BY_NAMESPACE", "")
|
||||
|
||||
collectors := NewCollectors()
|
||||
collectors.RecordReload(true, "kube-system")
|
||||
|
||||
// The ReloadedByNamespace counter should not be incremented
|
||||
// We can verify by checking countByNamespace is false
|
||||
if collectors.countByNamespace {
|
||||
t.Error("countByNamespace should be false when env var is not set")
|
||||
}
|
||||
@@ -131,7 +127,6 @@ func TestRecordReload_WithoutNamespaceTracking(t *testing.T) {
|
||||
func TestNilCollectors_NoPanic(t *testing.T) {
|
||||
var c *Collectors = nil
|
||||
|
||||
// This should not panic
|
||||
c.RecordReload(true, "default")
|
||||
c.RecordReload(false, "default")
|
||||
}
|
||||
@@ -146,11 +141,12 @@ func TestRecordReload_DifferentNamespaces(t *testing.T) {
|
||||
|
||||
metric := &dto.Metric{}
|
||||
|
||||
// Check namespace-a has 2 reloads
|
||||
err := collectors.ReloadedByNamespace.With(prometheus.Labels{
|
||||
"success": "true",
|
||||
"namespace": "namespace-a",
|
||||
}).Write(metric)
|
||||
err := collectors.ReloadedByNamespace.With(
|
||||
prometheus.Labels{
|
||||
"success": "true",
|
||||
"namespace": "namespace-a",
|
||||
},
|
||||
).Write(metric)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get metric: %v", err)
|
||||
}
|
||||
@@ -158,11 +154,12 @@ func TestRecordReload_DifferentNamespaces(t *testing.T) {
|
||||
t.Errorf("namespace-a counter = %v, want 2", metric.Counter.GetValue())
|
||||
}
|
||||
|
||||
// Check namespace-b has 1 reload
|
||||
err = collectors.ReloadedByNamespace.With(prometheus.Labels{
|
||||
"success": "true",
|
||||
"namespace": "namespace-b",
|
||||
}).Write(metric)
|
||||
err = collectors.ReloadedByNamespace.With(
|
||||
prometheus.Labels{
|
||||
"success": "true",
|
||||
"namespace": "namespace-b",
|
||||
},
|
||||
).Write(metric)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get metric: %v", err)
|
||||
}
|
||||
@@ -174,7 +171,6 @@ func TestRecordReload_DifferentNamespaces(t *testing.T) {
|
||||
func TestCollectors_MetricNames(t *testing.T) {
|
||||
collectors := NewCollectors()
|
||||
|
||||
// Verify the Reloaded metric has correct description
|
||||
ch := make(chan *prometheus.Desc, 10)
|
||||
collectors.Reloaded.Describe(ch)
|
||||
close(ch)
|
||||
|
||||
@@ -9,28 +9,33 @@ import (
|
||||
)
|
||||
|
||||
func TestFilterDecisions(t *testing.T) {
|
||||
// Create some mock workloads for testing
|
||||
wl1 := workload.NewDeploymentWorkload(&appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "deploy1", Namespace: "default"},
|
||||
})
|
||||
wl2 := workload.NewDeploymentWorkload(&appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "deploy2", Namespace: "default"},
|
||||
})
|
||||
wl3 := workload.NewDeploymentWorkload(&appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "deploy3", Namespace: "default"},
|
||||
})
|
||||
wl1 := workload.NewDeploymentWorkload(
|
||||
&appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "deploy1", Namespace: "default"},
|
||||
},
|
||||
)
|
||||
wl2 := workload.NewDeploymentWorkload(
|
||||
&appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "deploy2", Namespace: "default"},
|
||||
},
|
||||
)
|
||||
wl3 := workload.NewDeploymentWorkload(
|
||||
&appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "deploy3", Namespace: "default"},
|
||||
},
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
decisions []ReloadDecision
|
||||
wantCount int
|
||||
wantNames []string
|
||||
name string
|
||||
decisions []ReloadDecision
|
||||
wantCount int
|
||||
wantNames []string
|
||||
}{
|
||||
{
|
||||
name: "empty list",
|
||||
decisions: []ReloadDecision{},
|
||||
wantCount: 0,
|
||||
wantNames: nil,
|
||||
name: "empty list",
|
||||
decisions: []ReloadDecision{},
|
||||
wantCount: 0,
|
||||
wantNames: nil,
|
||||
},
|
||||
{
|
||||
name: "all should reload",
|
||||
@@ -63,29 +68,35 @@ func TestFilterDecisions(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := FilterDecisions(tt.decisions)
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
result := FilterDecisions(tt.decisions)
|
||||
|
||||
if len(result) != tt.wantCount {
|
||||
t.Errorf("FilterDecisions() returned %d decisions, want %d", len(result), tt.wantCount)
|
||||
}
|
||||
if len(result) != tt.wantCount {
|
||||
t.Errorf("FilterDecisions() returned %d decisions, want %d", len(result), tt.wantCount)
|
||||
}
|
||||
|
||||
if tt.wantNames != nil {
|
||||
for i, d := range result {
|
||||
if d.Workload.GetName() != tt.wantNames[i] {
|
||||
t.Errorf("FilterDecisions()[%d].Workload.GetName() = %s, want %s",
|
||||
i, d.Workload.GetName(), tt.wantNames[i])
|
||||
if tt.wantNames != nil {
|
||||
for i, d := range result {
|
||||
if d.Workload.GetName() != tt.wantNames[i] {
|
||||
t.Errorf(
|
||||
"FilterDecisions()[%d].Workload.GetName() = %s, want %s",
|
||||
i, d.Workload.GetName(), tt.wantNames[i],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReloadDecision_Fields(t *testing.T) {
|
||||
wl := workload.NewDeploymentWorkload(&appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
})
|
||||
wl := workload.NewDeploymentWorkload(
|
||||
&appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
},
|
||||
)
|
||||
|
||||
decision := ReloadDecision{
|
||||
Workload: wl,
|
||||
|
||||
@@ -20,7 +20,6 @@ func TestHasher_HashConfigMap(t *testing.T) {
|
||||
Data: nil,
|
||||
BinaryData: nil,
|
||||
},
|
||||
// Empty configmap gets a valid hash (hash of empty data)
|
||||
wantHash: hasher.HashConfigMap(&corev1.ConfigMap{}),
|
||||
},
|
||||
{
|
||||
@@ -31,13 +30,14 @@ func TestHasher_HashConfigMap(t *testing.T) {
|
||||
"key2": "value2",
|
||||
},
|
||||
},
|
||||
// Hash should be deterministic
|
||||
wantHash: hasher.HashConfigMap(&corev1.ConfigMap{
|
||||
Data: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
wantHash: hasher.HashConfigMap(
|
||||
&corev1.ConfigMap{
|
||||
Data: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "configmap with binary data",
|
||||
@@ -46,21 +46,25 @@ func TestHasher_HashConfigMap(t *testing.T) {
|
||||
"binary1": []byte("binaryvalue1"),
|
||||
},
|
||||
},
|
||||
wantHash: hasher.HashConfigMap(&corev1.ConfigMap{
|
||||
BinaryData: map[string][]byte{
|
||||
"binary1": []byte("binaryvalue1"),
|
||||
wantHash: hasher.HashConfigMap(
|
||||
&corev1.ConfigMap{
|
||||
BinaryData: map[string][]byte{
|
||||
"binary1": []byte("binaryvalue1"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := hasher.HashConfigMap(tt.cm)
|
||||
if got != tt.wantHash {
|
||||
t.Errorf("HashConfigMap() = %v, want %v", got, tt.wantHash)
|
||||
}
|
||||
})
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
got := hasher.HashConfigMap(tt.cm)
|
||||
if got != tt.wantHash {
|
||||
t.Errorf("HashConfigMap() = %v, want %v", got, tt.wantHash)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +79,6 @@ func TestHasher_HashConfigMap_Deterministic(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
// Hash should be the same regardless of iteration order
|
||||
hash1 := hasher.HashConfigMap(cm)
|
||||
hash2 := hasher.HashConfigMap(cm)
|
||||
hash3 := hasher.HashConfigMap(cm)
|
||||
@@ -121,7 +124,6 @@ func TestHasher_HashSecret(t *testing.T) {
|
||||
secret: &corev1.Secret{
|
||||
Data: nil,
|
||||
},
|
||||
// Empty secret gets a valid hash (hash of empty data)
|
||||
wantHash: hasher.HashSecret(&corev1.Secret{}),
|
||||
},
|
||||
{
|
||||
@@ -132,22 +134,26 @@ func TestHasher_HashSecret(t *testing.T) {
|
||||
"key2": []byte("value2"),
|
||||
},
|
||||
},
|
||||
wantHash: hasher.HashSecret(&corev1.Secret{
|
||||
Data: map[string][]byte{
|
||||
"key1": []byte("value1"),
|
||||
"key2": []byte("value2"),
|
||||
wantHash: hasher.HashSecret(
|
||||
&corev1.Secret{
|
||||
Data: map[string][]byte{
|
||||
"key1": []byte("value1"),
|
||||
"key2": []byte("value2"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := hasher.HashSecret(tt.secret)
|
||||
if got != tt.wantHash {
|
||||
t.Errorf("HashSecret() = %v, want %v", got, tt.wantHash)
|
||||
}
|
||||
})
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
got := hasher.HashSecret(tt.secret)
|
||||
if got != tt.wantHash {
|
||||
t.Errorf("HashSecret() = %v, want %v", got, tt.wantHash)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +168,6 @@ func TestHasher_HashSecret_Deterministic(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
// Hash should be the same regardless of iteration order
|
||||
hash1 := hasher.HashSecret(secret)
|
||||
hash2 := hasher.HashSecret(secret)
|
||||
hash3 := hasher.HashSecret(secret)
|
||||
@@ -198,20 +203,17 @@ func TestHasher_HashSecret_DifferentValues(t *testing.T) {
|
||||
func TestHasher_EmptyHash(t *testing.T) {
|
||||
hasher := NewHasher()
|
||||
|
||||
// EmptyHash returns empty string to signal deletion
|
||||
emptyHash := hasher.EmptyHash()
|
||||
if emptyHash != "" {
|
||||
t.Errorf("EmptyHash should be empty string, got %s", emptyHash)
|
||||
}
|
||||
|
||||
// Empty ConfigMap should have a valid hash (not empty)
|
||||
cm := &corev1.ConfigMap{}
|
||||
cmHash := hasher.HashConfigMap(cm)
|
||||
if cmHash == "" {
|
||||
t.Error("Empty ConfigMap should have a non-empty hash")
|
||||
}
|
||||
|
||||
// Empty Secret should have a valid hash (not empty)
|
||||
secret := &corev1.Secret{}
|
||||
secretHash := hasher.HashSecret(secret)
|
||||
if secretHash == "" {
|
||||
@@ -222,13 +224,11 @@ func TestHasher_EmptyHash(t *testing.T) {
|
||||
func TestHasher_NilInput(t *testing.T) {
|
||||
hasher := NewHasher()
|
||||
|
||||
// Test nil ConfigMap - returns hash of empty content (not EmptyHash)
|
||||
cmHash := hasher.HashConfigMap(nil)
|
||||
if cmHash == "" {
|
||||
t.Error("nil ConfigMap should return a valid hash")
|
||||
}
|
||||
|
||||
// Test nil Secret - returns hash of empty content (not EmptyHash)
|
||||
secretHash := hasher.HashSecret(nil)
|
||||
if secretHash == "" {
|
||||
t.Error("nil Secret should return a valid hash")
|
||||
|
||||
@@ -17,7 +17,6 @@ func TestMatcher_ShouldReload(t *testing.T) {
|
||||
wantAutoReload bool
|
||||
description string
|
||||
}{
|
||||
// Ignore annotation tests
|
||||
{
|
||||
name: "ignore annotation on resource skips reload",
|
||||
input: MatchInput{
|
||||
@@ -46,8 +45,6 @@ func TestMatcher_ShouldReload(t *testing.T) {
|
||||
wantAutoReload: true,
|
||||
description: "Resources with ignore=false should allow reload",
|
||||
},
|
||||
|
||||
// Exclude annotation tests
|
||||
{
|
||||
name: "exclude annotation skips reload",
|
||||
input: MatchInput{
|
||||
@@ -82,8 +79,6 @@ func TestMatcher_ShouldReload(t *testing.T) {
|
||||
wantAutoReload: false,
|
||||
description: "ConfigMaps in comma-separated exclude list should not trigger reload",
|
||||
},
|
||||
|
||||
// BUG FIX: Explicit annotation checked BEFORE auto
|
||||
{
|
||||
name: "explicit reload annotation with auto enabled - should reload",
|
||||
input: MatchInput{
|
||||
@@ -133,8 +128,6 @@ func TestMatcher_ShouldReload(t *testing.T) {
|
||||
wantAutoReload: false,
|
||||
description: "ConfigMaps not in reload list should not trigger reload",
|
||||
},
|
||||
|
||||
// Auto annotation tests
|
||||
{
|
||||
name: "auto annotation on workload triggers reload",
|
||||
input: MatchInput{
|
||||
@@ -205,8 +198,6 @@ func TestMatcher_ShouldReload(t *testing.T) {
|
||||
wantAutoReload: false,
|
||||
description: "ConfigMap-specific auto annotation should not match secrets",
|
||||
},
|
||||
|
||||
// Search/Match annotation tests
|
||||
{
|
||||
name: "search annotation with matching resource",
|
||||
input: MatchInput{
|
||||
@@ -235,8 +226,6 @@ func TestMatcher_ShouldReload(t *testing.T) {
|
||||
wantAutoReload: false,
|
||||
description: "Search annotation without matching resource should not trigger reload",
|
||||
},
|
||||
|
||||
// No annotations - should not reload
|
||||
{
|
||||
name: "no annotations does not trigger reload",
|
||||
input: MatchInput{
|
||||
@@ -251,8 +240,6 @@ func TestMatcher_ShouldReload(t *testing.T) {
|
||||
wantAutoReload: false,
|
||||
description: "Without any annotations, should not trigger reload",
|
||||
},
|
||||
|
||||
// Secret tests
|
||||
{
|
||||
name: "secret reload annotation",
|
||||
input: MatchInput{
|
||||
@@ -289,19 +276,21 @@ func TestMatcher_ShouldReload(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := matcher.ShouldReload(tt.input)
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
result := matcher.ShouldReload(tt.input)
|
||||
|
||||
if result.ShouldReload != tt.wantReload {
|
||||
t.Errorf("ShouldReload = %v, want %v (%s)", result.ShouldReload, tt.wantReload, tt.description)
|
||||
}
|
||||
if result.ShouldReload != tt.wantReload {
|
||||
t.Errorf("ShouldReload = %v, want %v (%s)", result.ShouldReload, tt.wantReload, tt.description)
|
||||
}
|
||||
|
||||
if result.AutoReload != tt.wantAutoReload {
|
||||
t.Errorf("AutoReload = %v, want %v (%s)", result.AutoReload, tt.wantAutoReload, tt.description)
|
||||
}
|
||||
if result.AutoReload != tt.wantAutoReload {
|
||||
t.Errorf("AutoReload = %v, want %v (%s)", result.AutoReload, tt.wantAutoReload, tt.description)
|
||||
}
|
||||
|
||||
t.Logf("✓ %s", tt.description)
|
||||
})
|
||||
t.Logf("✓ %s", tt.description)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,39 +353,31 @@ func TestMatcher_ShouldReload_AutoReloadAll(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := matcher.ShouldReload(tt.input)
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
result := matcher.ShouldReload(tt.input)
|
||||
|
||||
if result.ShouldReload != tt.wantReload {
|
||||
t.Errorf("ShouldReload = %v, want %v (%s)", result.ShouldReload, tt.wantReload, tt.description)
|
||||
}
|
||||
if result.ShouldReload != tt.wantReload {
|
||||
t.Errorf("ShouldReload = %v, want %v (%s)", result.ShouldReload, tt.wantReload, tt.description)
|
||||
}
|
||||
|
||||
if result.AutoReload != tt.wantAutoReload {
|
||||
t.Errorf("AutoReload = %v, want %v (%s)", result.AutoReload, tt.wantAutoReload, tt.description)
|
||||
}
|
||||
if result.AutoReload != tt.wantAutoReload {
|
||||
t.Errorf("AutoReload = %v, want %v (%s)", result.AutoReload, tt.wantAutoReload, tt.description)
|
||||
}
|
||||
|
||||
t.Logf("✓ %s", tt.description)
|
||||
})
|
||||
t.Logf("✓ %s", tt.description)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMatcher_BugFix_AutoDoesNotIgnoreExplicit tests the fix for the bug where
|
||||
// TestMatcher_AutoDoesNotIgnoreExplicit tests the fix for the bug where
|
||||
// having reloader.stakater.com/auto: "true" would cause explicit reload annotations
|
||||
// to be ignored due to an early return.
|
||||
func TestMatcher_BugFix_AutoDoesNotIgnoreExplicit(t *testing.T) {
|
||||
func TestMatcher_AutoDoesNotIgnoreExplicit(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
matcher := NewMatcher(cfg)
|
||||
|
||||
// This is the exact scenario from the bug report:
|
||||
// Workload has:
|
||||
// reloader.stakater.com/auto: "true" (watches all referenced CMs)
|
||||
// configmap.reloader.stakater.com/reload: "external-config" (ALSO watches this one)
|
||||
// Container references: app-config
|
||||
//
|
||||
// When "external-config" changes:
|
||||
// - Expected: Reload (explicitly listed)
|
||||
// - Bug behavior: No reload (auto annotation causes early return)
|
||||
|
||||
input := MatchInput{
|
||||
ResourceName: "external-config", // Not referenced by workload
|
||||
ResourceNamespace: "default",
|
||||
@@ -416,12 +397,11 @@ func TestMatcher_BugFix_AutoDoesNotIgnoreExplicit(t *testing.T) {
|
||||
t.Errorf("Expected ShouldReload=true for explicitly listed ConfigMap, got false")
|
||||
}
|
||||
|
||||
// Should be marked as non-auto since it matched the explicit list
|
||||
if result.AutoReload {
|
||||
t.Errorf("Expected AutoReload=false for explicit match, got true")
|
||||
}
|
||||
|
||||
t.Log("✓ Bug fixed: Explicit reload annotation works even when auto is enabled")
|
||||
t.Log("✓ Explicit reload annotation works even when auto is enabled")
|
||||
}
|
||||
|
||||
// TestMatcher_PrecedenceOrder verifies the correct order of precedence:
|
||||
@@ -435,54 +415,60 @@ func TestMatcher_PrecedenceOrder(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
matcher := NewMatcher(cfg)
|
||||
|
||||
t.Run("explicit takes precedence over auto", func(t *testing.T) {
|
||||
input := MatchInput{
|
||||
ResourceName: "my-config",
|
||||
ResourceNamespace: "default",
|
||||
ResourceType: ResourceTypeConfigMap,
|
||||
WorkloadAnnotations: map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
"configmap.reloader.stakater.com/reload": "my-config",
|
||||
},
|
||||
}
|
||||
result := matcher.ShouldReload(input)
|
||||
if result.AutoReload {
|
||||
t.Error("Expected explicit match (AutoReload=false), got auto match")
|
||||
}
|
||||
if !result.ShouldReload {
|
||||
t.Error("Expected ShouldReload=true")
|
||||
}
|
||||
})
|
||||
t.Run(
|
||||
"explicit takes precedence over auto", func(t *testing.T) {
|
||||
input := MatchInput{
|
||||
ResourceName: "my-config",
|
||||
ResourceNamespace: "default",
|
||||
ResourceType: ResourceTypeConfigMap,
|
||||
WorkloadAnnotations: map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
"configmap.reloader.stakater.com/reload": "my-config",
|
||||
},
|
||||
}
|
||||
result := matcher.ShouldReload(input)
|
||||
if result.AutoReload {
|
||||
t.Error("Expected explicit match (AutoReload=false), got auto match")
|
||||
}
|
||||
if !result.ShouldReload {
|
||||
t.Error("Expected ShouldReload=true")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
t.Run("ignore takes precedence over explicit", func(t *testing.T) {
|
||||
input := MatchInput{
|
||||
ResourceName: "my-config",
|
||||
ResourceNamespace: "default",
|
||||
ResourceType: ResourceTypeConfigMap,
|
||||
ResourceAnnotations: map[string]string{"reloader.stakater.com/ignore": "true"},
|
||||
WorkloadAnnotations: map[string]string{
|
||||
"configmap.reloader.stakater.com/reload": "my-config",
|
||||
},
|
||||
}
|
||||
result := matcher.ShouldReload(input)
|
||||
if result.ShouldReload {
|
||||
t.Error("Expected ignore to take precedence, but got ShouldReload=true")
|
||||
}
|
||||
})
|
||||
t.Run(
|
||||
"ignore takes precedence over explicit", func(t *testing.T) {
|
||||
input := MatchInput{
|
||||
ResourceName: "my-config",
|
||||
ResourceNamespace: "default",
|
||||
ResourceType: ResourceTypeConfigMap,
|
||||
ResourceAnnotations: map[string]string{"reloader.stakater.com/ignore": "true"},
|
||||
WorkloadAnnotations: map[string]string{
|
||||
"configmap.reloader.stakater.com/reload": "my-config",
|
||||
},
|
||||
}
|
||||
result := matcher.ShouldReload(input)
|
||||
if result.ShouldReload {
|
||||
t.Error("Expected ignore to take precedence, but got ShouldReload=true")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
t.Run("exclude takes precedence over explicit", func(t *testing.T) {
|
||||
input := MatchInput{
|
||||
ResourceName: "my-config",
|
||||
ResourceNamespace: "default",
|
||||
ResourceType: ResourceTypeConfigMap,
|
||||
WorkloadAnnotations: map[string]string{
|
||||
"configmap.reloader.stakater.com/reload": "my-config",
|
||||
"configmaps.exclude.reloader.stakater.com/reload": "my-config",
|
||||
},
|
||||
}
|
||||
result := matcher.ShouldReload(input)
|
||||
if result.ShouldReload {
|
||||
t.Error("Expected exclude to take precedence, but got ShouldReload=true")
|
||||
}
|
||||
})
|
||||
t.Run(
|
||||
"exclude takes precedence over explicit", func(t *testing.T) {
|
||||
input := MatchInput{
|
||||
ResourceName: "my-config",
|
||||
ResourceNamespace: "default",
|
||||
ResourceType: ResourceTypeConfigMap,
|
||||
WorkloadAnnotations: map[string]string{
|
||||
"configmap.reloader.stakater.com/reload": "my-config",
|
||||
"configmaps.exclude.reloader.stakater.com/reload": "my-config",
|
||||
},
|
||||
}
|
||||
result := matcher.ShouldReload(input)
|
||||
if result.ShouldReload {
|
||||
t.Error("Expected exclude to take precedence, but got ShouldReload=true")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -58,10 +58,8 @@ func (h *PauseHandler) ApplyPause(wl workload.WorkloadAccessor) error {
|
||||
|
||||
deploy := deployWl.GetDeployment()
|
||||
|
||||
// Set paused flag
|
||||
deploy.Spec.Paused = true
|
||||
|
||||
// Set paused-at annotation
|
||||
if deploy.Annotations == nil {
|
||||
deploy.Annotations = make(map[string]string)
|
||||
}
|
||||
@@ -109,7 +107,6 @@ func (h *PauseHandler) CheckPauseExpired(deploy *appsv1.Deployment) (expired boo
|
||||
func (h *PauseHandler) ClearPause(deploy *appsv1.Deployment) {
|
||||
deploy.Spec.Paused = false
|
||||
delete(deploy.Annotations, h.cfg.Annotations.PausedAt)
|
||||
// Keep pause-period annotation (user's config)
|
||||
}
|
||||
|
||||
// IsPausedByReloader checks if a deployment was paused by Reloader.
|
||||
|
||||
@@ -33,26 +33,30 @@ func resourcePredicates(cfg *config.Config, hashFn func(old, new client.Object)
|
||||
|
||||
// ConfigMapPredicates returns predicates for filtering ConfigMap events.
|
||||
func ConfigMapPredicates(cfg *config.Config, hasher *Hasher) predicate.Predicate {
|
||||
return resourcePredicates(cfg, func(old, new client.Object) (string, string, bool) {
|
||||
oldCM, okOld := old.(*corev1.ConfigMap)
|
||||
newCM, okNew := new.(*corev1.ConfigMap)
|
||||
if !okOld || !okNew {
|
||||
return "", "", false
|
||||
}
|
||||
return hasher.HashConfigMap(oldCM), hasher.HashConfigMap(newCM), true
|
||||
})
|
||||
return resourcePredicates(
|
||||
cfg, func(old, new client.Object) (string, string, bool) {
|
||||
oldCM, okOld := old.(*corev1.ConfigMap)
|
||||
newCM, okNew := new.(*corev1.ConfigMap)
|
||||
if !okOld || !okNew {
|
||||
return "", "", false
|
||||
}
|
||||
return hasher.HashConfigMap(oldCM), hasher.HashConfigMap(newCM), true
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// SecretPredicates returns predicates for filtering Secret events.
|
||||
func SecretPredicates(cfg *config.Config, hasher *Hasher) predicate.Predicate {
|
||||
return resourcePredicates(cfg, func(old, new client.Object) (string, string, bool) {
|
||||
oldSecret, okOld := old.(*corev1.Secret)
|
||||
newSecret, okNew := new.(*corev1.Secret)
|
||||
if !okOld || !okNew {
|
||||
return "", "", false
|
||||
}
|
||||
return hasher.HashSecret(oldSecret), hasher.HashSecret(newSecret), true
|
||||
})
|
||||
return resourcePredicates(
|
||||
cfg, func(old, new client.Object) (string, string, bool) {
|
||||
oldSecret, okOld := old.(*corev1.Secret)
|
||||
newSecret, okNew := new.(*corev1.Secret)
|
||||
if !okOld || !okNew {
|
||||
return "", "", false
|
||||
}
|
||||
return hasher.HashSecret(oldSecret), hasher.HashSecret(newSecret), true
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// NamespaceChecker defines the interface for checking if a namespace is allowed.
|
||||
@@ -68,47 +72,49 @@ func NamespaceFilterPredicate(cfg *config.Config) predicate.Predicate {
|
||||
// NamespaceFilterPredicateWithCache returns a predicate that filters resources by namespace,
|
||||
// using the provided NamespaceChecker for namespace selector filtering.
|
||||
func NamespaceFilterPredicateWithCache(cfg *config.Config, nsCache NamespaceChecker) predicate.Predicate {
|
||||
return predicate.NewPredicateFuncs(func(obj client.Object) bool {
|
||||
namespace := obj.GetNamespace()
|
||||
return predicate.NewPredicateFuncs(
|
||||
func(obj client.Object) bool {
|
||||
namespace := obj.GetNamespace()
|
||||
|
||||
// Check if namespace should be ignored
|
||||
if cfg.IsNamespaceIgnored(namespace) {
|
||||
return false
|
||||
}
|
||||
if cfg.IsNamespaceIgnored(namespace) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check namespace selector cache if provided
|
||||
if nsCache != nil && !nsCache.Contains(namespace) {
|
||||
return false
|
||||
}
|
||||
if nsCache != nil && !nsCache.Contains(namespace) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
return true
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// LabelSelectorPredicate returns a predicate that filters resources by labels.
|
||||
func LabelSelectorPredicate(cfg *config.Config) predicate.Predicate {
|
||||
if len(cfg.ResourceSelectors) == 0 {
|
||||
// No selectors configured, allow all
|
||||
return predicate.NewPredicateFuncs(func(obj client.Object) bool {
|
||||
return true
|
||||
})
|
||||
return predicate.NewPredicateFuncs(
|
||||
func(obj client.Object) bool {
|
||||
return true
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return predicate.NewPredicateFuncs(func(obj client.Object) bool {
|
||||
labels := obj.GetLabels()
|
||||
if labels == nil {
|
||||
labels = make(map[string]string)
|
||||
}
|
||||
|
||||
// Check if any selector matches
|
||||
for _, selector := range cfg.ResourceSelectors {
|
||||
if selector.Matches(LabelsSet(labels)) {
|
||||
return true
|
||||
return predicate.NewPredicateFuncs(
|
||||
func(obj client.Object) bool {
|
||||
labels := obj.GetLabels()
|
||||
if labels == nil {
|
||||
labels = make(map[string]string)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
for _, selector := range cfg.ResourceSelectors {
|
||||
if selector.Matches(LabelsSet(labels)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// LabelsSet implements the k8s.io/apimachinery/pkg/labels.Labels interface
|
||||
@@ -128,15 +134,16 @@ func (ls LabelsSet) Get(key string) string {
|
||||
|
||||
// IgnoreAnnotationPredicate returns a predicate that filters out resources with the ignore annotation.
|
||||
func IgnoreAnnotationPredicate(cfg *config.Config) predicate.Predicate {
|
||||
return predicate.NewPredicateFuncs(func(obj client.Object) bool {
|
||||
annotations := obj.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return true
|
||||
}
|
||||
return predicate.NewPredicateFuncs(
|
||||
func(obj client.Object) bool {
|
||||
annotations := obj.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for ignore annotation
|
||||
return annotations[cfg.Annotations.Ignore] != "true"
|
||||
})
|
||||
return annotations[cfg.Annotations.Ignore] != "true"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// CombinedPredicates combines multiple predicates with AND logic.
|
||||
|
||||
@@ -44,25 +44,27 @@ func TestNamespaceFilterPredicate_Create(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.IgnoredNamespaces = tt.ignoredNamespaces
|
||||
predicate := NamespaceFilterPredicate(cfg)
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.IgnoredNamespaces = tt.ignoredNamespaces
|
||||
predicate := NamespaceFilterPredicate(cfg)
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: tt.eventNamespace,
|
||||
},
|
||||
}
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: tt.eventNamespace,
|
||||
},
|
||||
}
|
||||
|
||||
e := event.CreateEvent{Object: cm}
|
||||
got := predicate.Create(e)
|
||||
e := event.CreateEvent{Object: cm}
|
||||
got := predicate.Create(e)
|
||||
|
||||
if got != tt.wantAllow {
|
||||
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
|
||||
}
|
||||
})
|
||||
if got != tt.wantAllow {
|
||||
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,36 +174,37 @@ func TestLabelSelectorPredicate_Create(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
selector, err := labels.Parse(tt.selector)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse selector: %v", err)
|
||||
}
|
||||
cfg.ResourceSelectors = []labels.Selector{selector}
|
||||
predicate := LabelSelectorPredicate(cfg)
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
selector, err := labels.Parse(tt.selector)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse selector: %v", err)
|
||||
}
|
||||
cfg.ResourceSelectors = []labels.Selector{selector}
|
||||
predicate := LabelSelectorPredicate(cfg)
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
Labels: tt.objectLabels,
|
||||
},
|
||||
}
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
Labels: tt.objectLabels,
|
||||
},
|
||||
}
|
||||
|
||||
e := event.CreateEvent{Object: cm}
|
||||
got := predicate.Create(e)
|
||||
e := event.CreateEvent{Object: cm}
|
||||
got := predicate.Create(e)
|
||||
|
||||
if got != tt.wantAllow {
|
||||
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
|
||||
}
|
||||
})
|
||||
if got != tt.wantAllow {
|
||||
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelSelectorPredicate_NoSelectors(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
// No selectors configured
|
||||
predicate := LabelSelectorPredicate(cfg)
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
@@ -253,22 +256,24 @@ func TestLabelSelectorPredicate_MultipleSelectors(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
Labels: tt.labels,
|
||||
},
|
||||
}
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
Labels: tt.labels,
|
||||
},
|
||||
}
|
||||
|
||||
e := event.CreateEvent{Object: cm}
|
||||
got := predicate.Create(e)
|
||||
e := event.CreateEvent{Object: cm}
|
||||
got := predicate.Create(e)
|
||||
|
||||
if got != tt.wantAllow {
|
||||
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
|
||||
}
|
||||
})
|
||||
if got != tt.wantAllow {
|
||||
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,34 +397,35 @@ func TestCombinedFiltering(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: tt.namespace,
|
||||
Labels: tt.labels,
|
||||
},
|
||||
}
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: tt.namespace,
|
||||
Labels: tt.labels,
|
||||
},
|
||||
}
|
||||
|
||||
e := event.CreateEvent{Object: cm}
|
||||
e := event.CreateEvent{Object: cm}
|
||||
|
||||
gotNS := nsPredicate.Create(e)
|
||||
if gotNS != tt.wantNSAllow {
|
||||
t.Errorf("Namespace predicate Create() = %v, want %v", gotNS, tt.wantNSAllow)
|
||||
}
|
||||
gotNS := nsPredicate.Create(e)
|
||||
if gotNS != tt.wantNSAllow {
|
||||
t.Errorf("Namespace predicate Create() = %v, want %v", gotNS, tt.wantNSAllow)
|
||||
}
|
||||
|
||||
gotLabel := labelPredicate.Create(e)
|
||||
if gotLabel != tt.wantLabelAllow {
|
||||
t.Errorf("Label predicate Create() = %v, want %v", gotLabel, tt.wantLabelAllow)
|
||||
}
|
||||
gotLabel := labelPredicate.Create(e)
|
||||
if gotLabel != tt.wantLabelAllow {
|
||||
t.Errorf("Label predicate Create() = %v, want %v", gotLabel, tt.wantLabelAllow)
|
||||
}
|
||||
|
||||
// Both must be true for the event to pass through
|
||||
combinedAllow := gotNS && gotLabel
|
||||
expectedCombined := tt.wantNSAllow && tt.wantLabelAllow
|
||||
if combinedAllow != expectedCombined {
|
||||
t.Errorf("Combined allow = %v, want %v", combinedAllow, expectedCombined)
|
||||
}
|
||||
})
|
||||
combinedAllow := gotNS && gotLabel
|
||||
expectedCombined := tt.wantNSAllow && tt.wantLabelAllow
|
||||
if combinedAllow != expectedCombined {
|
||||
t.Errorf("Combined allow = %v, want %v", combinedAllow, expectedCombined)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,7 +455,6 @@ func TestFilteringWithSecrets(t *testing.T) {
|
||||
|
||||
func TestExistsLabelSelector(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
// Selector that checks if label exists (any value)
|
||||
selector, _ := labels.Parse("managed")
|
||||
cfg.ResourceSelectors = []labels.Selector{selector}
|
||||
predicate := LabelSelectorPredicate(cfg)
|
||||
@@ -482,22 +487,24 @@ func TestExistsLabelSelector(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
Labels: tt.labels,
|
||||
},
|
||||
}
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
Labels: tt.labels,
|
||||
},
|
||||
}
|
||||
|
||||
e := event.CreateEvent{Object: cm}
|
||||
got := predicate.Create(e)
|
||||
e := event.CreateEvent{Object: cm}
|
||||
got := predicate.Create(e)
|
||||
|
||||
if got != tt.wantAllow {
|
||||
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
|
||||
}
|
||||
})
|
||||
if got != tt.wantAllow {
|
||||
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,27 +556,29 @@ func TestNamespaceFilterPredicateWithCache(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.IgnoredNamespaces = tt.ignoredNamespaces
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.IgnoredNamespaces = tt.ignoredNamespaces
|
||||
|
||||
cache := &mockNamespaceChecker{allowed: tt.cacheAllowed}
|
||||
predicate := NamespaceFilterPredicateWithCache(cfg, cache)
|
||||
cache := &mockNamespaceChecker{allowed: tt.cacheAllowed}
|
||||
predicate := NamespaceFilterPredicateWithCache(cfg, cache)
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: tt.eventNamespace,
|
||||
},
|
||||
}
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: tt.eventNamespace,
|
||||
},
|
||||
}
|
||||
|
||||
e := event.CreateEvent{Object: cm}
|
||||
got := predicate.Create(e)
|
||||
e := event.CreateEvent{Object: cm}
|
||||
got := predicate.Create(e)
|
||||
|
||||
if got != tt.wantAllow {
|
||||
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
|
||||
}
|
||||
})
|
||||
if got != tt.wantAllow {
|
||||
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,7 +586,6 @@ func TestNamespaceFilterPredicateWithCache_NilCache(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.IgnoredNamespaces = []string{"kube-system"}
|
||||
|
||||
// Nil cache should allow all namespaces (only check ignore list)
|
||||
predicate := NamespaceFilterPredicateWithCache(cfg, nil)
|
||||
|
||||
tests := []struct {
|
||||
@@ -590,21 +598,23 @@ func TestNamespaceFilterPredicateWithCache_NilCache(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.namespace, func(t *testing.T) {
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: tt.namespace,
|
||||
},
|
||||
}
|
||||
t.Run(
|
||||
tt.namespace, func(t *testing.T) {
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: tt.namespace,
|
||||
},
|
||||
}
|
||||
|
||||
e := event.CreateEvent{Object: cm}
|
||||
got := predicate.Create(e)
|
||||
e := event.CreateEvent{Object: cm}
|
||||
got := predicate.Create(e)
|
||||
|
||||
if got != tt.wantAllow {
|
||||
t.Errorf("Create() = %v, want %v for namespace %s", got, tt.wantAllow, tt.namespace)
|
||||
}
|
||||
})
|
||||
if got != tt.wantAllow {
|
||||
t.Errorf("Create() = %v, want %v for namespace %s", got, tt.wantAllow, tt.namespace)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,22 +660,24 @@ func TestIgnoreAnnotationPredicate_Create(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
Annotations: tt.annotations,
|
||||
},
|
||||
}
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
Annotations: tt.annotations,
|
||||
},
|
||||
}
|
||||
|
||||
e := event.CreateEvent{Object: cm}
|
||||
got := predicate.Create(e)
|
||||
e := event.CreateEvent{Object: cm}
|
||||
got := predicate.Create(e)
|
||||
|
||||
if got != tt.wantAllow {
|
||||
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
|
||||
}
|
||||
})
|
||||
if got != tt.wantAllow {
|
||||
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -688,7 +700,6 @@ func TestIgnoreAnnotationPredicate_AllEventTypes(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
// Test Update
|
||||
if predicate.Update(event.UpdateEvent{ObjectNew: ignoredCM}) {
|
||||
t.Error("Update() should block ignored resource")
|
||||
}
|
||||
@@ -696,7 +707,6 @@ func TestIgnoreAnnotationPredicate_AllEventTypes(t *testing.T) {
|
||||
t.Error("Update() should allow non-ignored resource")
|
||||
}
|
||||
|
||||
// Test Delete
|
||||
if predicate.Delete(event.DeleteEvent{Object: ignoredCM}) {
|
||||
t.Error("Delete() should block ignored resource")
|
||||
}
|
||||
@@ -704,7 +714,6 @@ func TestIgnoreAnnotationPredicate_AllEventTypes(t *testing.T) {
|
||||
t.Error("Delete() should allow non-ignored resource")
|
||||
}
|
||||
|
||||
// Test Generic
|
||||
if predicate.Generic(event.GenericEvent{Object: ignoredCM}) {
|
||||
t.Error("Generic() should block ignored resource")
|
||||
}
|
||||
@@ -755,22 +764,24 @@ func TestCombinedPredicates(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: tt.namespace,
|
||||
Annotations: tt.annotations,
|
||||
},
|
||||
}
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: tt.namespace,
|
||||
Annotations: tt.annotations,
|
||||
},
|
||||
}
|
||||
|
||||
e := event.CreateEvent{Object: cm}
|
||||
got := combined.Create(e)
|
||||
e := event.CreateEvent{Object: cm}
|
||||
got := combined.Create(e)
|
||||
|
||||
if got != tt.wantAllow {
|
||||
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
|
||||
}
|
||||
})
|
||||
if got != tt.wantAllow {
|
||||
t.Errorf("Create() = %v, want %v", got, tt.wantAllow)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -792,13 +803,11 @@ func TestConfigMapPredicates_Update(t *testing.T) {
|
||||
Data: map[string]string{"key": "value2"},
|
||||
}
|
||||
|
||||
// Same content should not trigger update
|
||||
e := event.UpdateEvent{ObjectOld: oldCM, ObjectNew: newCMSameContent}
|
||||
if predicate.Update(e) {
|
||||
t.Error("Update() should return false when content is the same")
|
||||
}
|
||||
|
||||
// Different content should trigger update
|
||||
e = event.UpdateEvent{ObjectOld: oldCM, ObjectNew: newCMDifferentContent}
|
||||
if !predicate.Update(e) {
|
||||
t.Error("Update() should return true when content changed")
|
||||
@@ -810,7 +819,6 @@ func TestConfigMapPredicates_InvalidTypes(t *testing.T) {
|
||||
hasher := NewHasher()
|
||||
predicate := ConfigMapPredicates(cfg, hasher)
|
||||
|
||||
// Test with non-ConfigMap types
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}
|
||||
@@ -818,13 +826,11 @@ func TestConfigMapPredicates_InvalidTypes(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}
|
||||
|
||||
// Old is secret, new is configmap - should return false
|
||||
e := event.UpdateEvent{ObjectOld: secret, ObjectNew: cm}
|
||||
if predicate.Update(e) {
|
||||
t.Error("Update() should return false for mismatched types")
|
||||
}
|
||||
|
||||
// Both are secrets - should return false
|
||||
e = event.UpdateEvent{ObjectOld: secret, ObjectNew: secret}
|
||||
if predicate.Update(e) {
|
||||
t.Error("Update() should return false for non-ConfigMap types")
|
||||
@@ -842,17 +848,14 @@ func TestConfigMapPredicates_CreateDeleteGeneric(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}
|
||||
|
||||
// Test Create
|
||||
if !predicate.Create(event.CreateEvent{Object: cm}) {
|
||||
t.Error("Create() should return true when ReloadOnCreate is true")
|
||||
}
|
||||
|
||||
// Test Delete
|
||||
if !predicate.Delete(event.DeleteEvent{Object: cm}) {
|
||||
t.Error("Delete() should return true when ReloadOnDelete is true")
|
||||
}
|
||||
|
||||
// Test Generic (should always return false)
|
||||
if predicate.Generic(event.GenericEvent{Object: cm}) {
|
||||
t.Error("Generic() should always return false")
|
||||
}
|
||||
@@ -876,13 +879,11 @@ func TestSecretPredicates_Update(t *testing.T) {
|
||||
Data: map[string][]byte{"key": []byte("value2")},
|
||||
}
|
||||
|
||||
// Same content should not trigger update
|
||||
e := event.UpdateEvent{ObjectOld: oldSecret, ObjectNew: newSecretSameContent}
|
||||
if predicate.Update(e) {
|
||||
t.Error("Update() should return false when content is the same")
|
||||
}
|
||||
|
||||
// Different content should trigger update
|
||||
e = event.UpdateEvent{ObjectOld: oldSecret, ObjectNew: newSecretDifferentContent}
|
||||
if !predicate.Update(e) {
|
||||
t.Error("Update() should return true when content changed")
|
||||
@@ -894,7 +895,6 @@ func TestSecretPredicates_InvalidTypes(t *testing.T) {
|
||||
hasher := NewHasher()
|
||||
predicate := SecretPredicates(cfg, hasher)
|
||||
|
||||
// Test with non-Secret types
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}
|
||||
@@ -902,13 +902,11 @@ func TestSecretPredicates_InvalidTypes(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}
|
||||
|
||||
// Old is configmap, new is secret - should return false
|
||||
e := event.UpdateEvent{ObjectOld: cm, ObjectNew: secret}
|
||||
if predicate.Update(e) {
|
||||
t.Error("Update() should return false for mismatched types")
|
||||
}
|
||||
|
||||
// Both are configmaps - should return false
|
||||
e = event.UpdateEvent{ObjectOld: cm, ObjectNew: cm}
|
||||
if predicate.Update(e) {
|
||||
t.Error("Update() should return false for non-Secret types")
|
||||
@@ -918,7 +916,6 @@ func TestSecretPredicates_InvalidTypes(t *testing.T) {
|
||||
func TestLabelsSet(t *testing.T) {
|
||||
ls := LabelsSet{"app": "test", "env": "prod"}
|
||||
|
||||
// Test Has
|
||||
if !ls.Has("app") {
|
||||
t.Error("Has(app) should return true")
|
||||
}
|
||||
@@ -926,7 +923,6 @@ func TestLabelsSet(t *testing.T) {
|
||||
t.Error("Has(nonexistent) should return false")
|
||||
}
|
||||
|
||||
// Test Get
|
||||
if ls.Get("app") != "test" {
|
||||
t.Errorf("Get(app) = %v, want test", ls.Get("app"))
|
||||
}
|
||||
|
||||
@@ -16,21 +16,13 @@ func TestResourceType_Kind(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.resourceType), func(t *testing.T) {
|
||||
got := tt.resourceType.Kind()
|
||||
if got != tt.want {
|
||||
t.Errorf("ResourceType(%q).Kind() = %v, want %v", tt.resourceType, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceTypeConstants(t *testing.T) {
|
||||
// Verify the constant values are as expected
|
||||
if ResourceTypeConfigMap != "configmap" {
|
||||
t.Errorf("ResourceTypeConfigMap = %v, want configmap", ResourceTypeConfigMap)
|
||||
}
|
||||
if ResourceTypeSecret != "secret" {
|
||||
t.Errorf("ResourceTypeSecret = %v, want secret", ResourceTypeSecret)
|
||||
t.Run(
|
||||
string(tt.resourceType), func(t *testing.T) {
|
||||
got := tt.resourceType.Kind()
|
||||
if got != tt.want {
|
||||
t.Errorf("ResourceType(%q).Kind() = %v, want %v", tt.resourceType, got, tt.want)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+142
-107
@@ -16,9 +16,11 @@ func TestService_ProcessConfigMap_AutoReload(t *testing.T) {
|
||||
svc := NewService(cfg)
|
||||
|
||||
// Create a deployment with auto annotation that uses the configmap
|
||||
deploy := createTestDeployment("test-deploy", "default", map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
})
|
||||
deploy := createTestDeployment(
|
||||
"test-deploy", "default", map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
},
|
||||
)
|
||||
deploy.Spec.Template.Spec.Volumes = []corev1.Volume{
|
||||
{
|
||||
Name: "config-vol",
|
||||
@@ -74,10 +76,11 @@ func TestService_ProcessConfigMap_ExplicitAnnotation(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
svc := NewService(cfg)
|
||||
|
||||
// Create a deployment with explicit configmap annotation
|
||||
deploy := createTestDeployment("test-deploy", "default", map[string]string{
|
||||
"configmap.reloader.stakater.com/reload": "test-cm",
|
||||
})
|
||||
deploy := createTestDeployment(
|
||||
"test-deploy", "default", map[string]string{
|
||||
"configmap.reloader.stakater.com/reload": "test-cm",
|
||||
},
|
||||
)
|
||||
|
||||
workloads := []workload.WorkloadAccessor{
|
||||
workload.NewDeploymentWorkload(deploy),
|
||||
@@ -118,9 +121,11 @@ func TestService_ProcessConfigMap_IgnoredResource(t *testing.T) {
|
||||
svc := NewService(cfg)
|
||||
|
||||
// Create a deployment with auto annotation
|
||||
deploy := createTestDeployment("test-deploy", "default", map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
})
|
||||
deploy := createTestDeployment(
|
||||
"test-deploy", "default", map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
},
|
||||
)
|
||||
deploy.Spec.Template.Spec.Volumes = []corev1.Volume{
|
||||
{
|
||||
Name: "config-vol",
|
||||
@@ -172,9 +177,11 @@ func TestService_ProcessSecret_AutoReload(t *testing.T) {
|
||||
svc := NewService(cfg)
|
||||
|
||||
// Create a deployment with auto annotation that uses the secret
|
||||
deploy := createTestDeployment("test-deploy", "default", map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
})
|
||||
deploy := createTestDeployment(
|
||||
"test-deploy", "default", map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
},
|
||||
)
|
||||
deploy.Spec.Template.Spec.Volumes = []corev1.Volume{
|
||||
{
|
||||
Name: "secret-vol",
|
||||
@@ -226,9 +233,11 @@ func TestService_ProcessConfigMap_DeleteEvent(t *testing.T) {
|
||||
svc := NewService(cfg)
|
||||
|
||||
// Create a deployment with explicit configmap annotation
|
||||
deploy := createTestDeployment("test-deploy", "default", map[string]string{
|
||||
"configmap.reloader.stakater.com/reload": "test-cm",
|
||||
})
|
||||
deploy := createTestDeployment(
|
||||
"test-deploy", "default", map[string]string{
|
||||
"configmap.reloader.stakater.com/reload": "test-cm",
|
||||
},
|
||||
)
|
||||
|
||||
workloads := []workload.WorkloadAccessor{
|
||||
workload.NewDeploymentWorkload(deploy),
|
||||
@@ -267,9 +276,11 @@ func TestService_ProcessConfigMap_DeleteEventDisabled(t *testing.T) {
|
||||
cfg.ReloadOnDelete = false // Disabled by default
|
||||
svc := NewService(cfg)
|
||||
|
||||
deploy := createTestDeployment("test-deploy", "default", map[string]string{
|
||||
"configmap.reloader.stakater.com/reload": "test-cm",
|
||||
})
|
||||
deploy := createTestDeployment(
|
||||
"test-deploy", "default", map[string]string{
|
||||
"configmap.reloader.stakater.com/reload": "test-cm",
|
||||
},
|
||||
)
|
||||
|
||||
workloads := []workload.WorkloadAccessor{
|
||||
workload.NewDeploymentWorkload(deploy),
|
||||
@@ -440,9 +451,11 @@ func TestService_ProcessConfigMap_MultipleWorkloads(t *testing.T) {
|
||||
svc := NewService(cfg)
|
||||
|
||||
// Create multiple workloads
|
||||
deploy1 := createTestDeployment("deploy1", "default", map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
})
|
||||
deploy1 := createTestDeployment(
|
||||
"deploy1", "default", map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
},
|
||||
)
|
||||
deploy1.Spec.Template.Spec.Volumes = []corev1.Volume{
|
||||
{
|
||||
Name: "config-vol",
|
||||
@@ -456,9 +469,11 @@ func TestService_ProcessConfigMap_MultipleWorkloads(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
deploy2 := createTestDeployment("deploy2", "default", map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
})
|
||||
deploy2 := createTestDeployment(
|
||||
"deploy2", "default", map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
},
|
||||
)
|
||||
deploy2.Spec.Template.Spec.Volumes = []corev1.Volume{
|
||||
{
|
||||
Name: "config-vol",
|
||||
@@ -473,9 +488,11 @@ func TestService_ProcessConfigMap_MultipleWorkloads(t *testing.T) {
|
||||
}
|
||||
|
||||
// Deploy3 doesn't use the configmap
|
||||
deploy3 := createTestDeployment("deploy3", "default", map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
})
|
||||
deploy3 := createTestDeployment(
|
||||
"deploy3", "default", map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
},
|
||||
)
|
||||
|
||||
workloads := []workload.WorkloadAccessor{
|
||||
workload.NewDeploymentWorkload(deploy1),
|
||||
@@ -521,9 +538,11 @@ func TestService_ProcessConfigMap_DifferentNamespaces(t *testing.T) {
|
||||
svc := NewService(cfg)
|
||||
|
||||
// Create deployments in different namespaces
|
||||
deploy1 := createTestDeployment("deploy1", "namespace-a", map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
})
|
||||
deploy1 := createTestDeployment(
|
||||
"deploy1", "namespace-a", map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
},
|
||||
)
|
||||
deploy1.Spec.Template.Spec.Volumes = []corev1.Volume{
|
||||
{
|
||||
Name: "config-vol",
|
||||
@@ -537,9 +556,11 @@ func TestService_ProcessConfigMap_DifferentNamespaces(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
deploy2 := createTestDeployment("deploy2", "namespace-b", map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
})
|
||||
deploy2 := createTestDeployment(
|
||||
"deploy2", "namespace-b", map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
},
|
||||
)
|
||||
deploy2.Spec.Template.Spec.Volumes = []corev1.Volume{
|
||||
{
|
||||
Name: "config-vol",
|
||||
@@ -624,17 +645,19 @@ func TestService_shouldProcessEvent(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.ReloadOnCreate = tt.reloadOnCreate
|
||||
cfg.ReloadOnDelete = tt.reloadOnDelete
|
||||
svc := NewService(cfg)
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.ReloadOnCreate = tt.reloadOnCreate
|
||||
cfg.ReloadOnDelete = tt.reloadOnDelete
|
||||
svc := NewService(cfg)
|
||||
|
||||
result := svc.shouldProcessEvent(tt.eventType)
|
||||
if result != tt.expected {
|
||||
t.Errorf("shouldProcessEvent(%s) = %v, want %v", tt.eventType, result, tt.expected)
|
||||
}
|
||||
})
|
||||
result := svc.shouldProcessEvent(tt.eventType)
|
||||
if result != tt.expected {
|
||||
t.Errorf("shouldProcessEvent(%s) = %v, want %v", tt.eventType, result, tt.expected)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -713,12 +736,14 @@ func TestService_findVolumeUsingResource_ConfigMap(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := svc.findVolumeUsingResource(tt.volumes, tt.resourceName, tt.resourceType)
|
||||
if got != tt.wantVolume {
|
||||
t.Errorf("findVolumeUsingResource() = %q, want %q", got, tt.wantVolume)
|
||||
}
|
||||
})
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
got := svc.findVolumeUsingResource(tt.volumes, tt.resourceName, tt.resourceType)
|
||||
if got != tt.wantVolume {
|
||||
t.Errorf("findVolumeUsingResource() = %q, want %q", got, tt.wantVolume)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -786,12 +811,14 @@ func TestService_findVolumeUsingResource_Secret(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := svc.findVolumeUsingResource(tt.volumes, tt.resourceName, ResourceTypeSecret)
|
||||
if got != tt.wantVolume {
|
||||
t.Errorf("findVolumeUsingResource() = %q, want %q", got, tt.wantVolume)
|
||||
}
|
||||
})
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
got := svc.findVolumeUsingResource(tt.volumes, tt.resourceName, ResourceTypeSecret)
|
||||
if got != tt.wantVolume {
|
||||
t.Errorf("findVolumeUsingResource() = %q, want %q", got, tt.wantVolume)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -860,20 +887,22 @@ func TestService_findContainerWithVolumeMount(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := svc.findContainerWithVolumeMount(tt.containers, tt.volumeName)
|
||||
if tt.shouldMatch {
|
||||
if got == nil {
|
||||
t.Error("Expected to find a container, got nil")
|
||||
} else if got.Name != tt.wantName {
|
||||
t.Errorf("findContainerWithVolumeMount() container name = %q, want %q", got.Name, tt.wantName)
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
got := svc.findContainerWithVolumeMount(tt.containers, tt.volumeName)
|
||||
if tt.shouldMatch {
|
||||
if got == nil {
|
||||
t.Error("Expected to find a container, got nil")
|
||||
} else if got.Name != tt.wantName {
|
||||
t.Errorf("findContainerWithVolumeMount() container name = %q, want %q", got.Name, tt.wantName)
|
||||
}
|
||||
} else {
|
||||
if got != nil {
|
||||
t.Errorf("Expected nil, got container %q", got.Name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if got != nil {
|
||||
t.Errorf("Expected nil, got container %q", got.Name)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -882,11 +911,11 @@ func TestService_findContainerWithEnvRef_ConfigMap(t *testing.T) {
|
||||
svc := NewService(cfg)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
containers []corev1.Container
|
||||
name string
|
||||
containers []corev1.Container
|
||||
resourceName string
|
||||
wantName string
|
||||
shouldMatch bool
|
||||
wantName string
|
||||
shouldMatch bool
|
||||
}{
|
||||
{
|
||||
name: "container with ConfigMapKeyRef",
|
||||
@@ -935,7 +964,7 @@ func TestService_findContainerWithEnvRef_ConfigMap(t *testing.T) {
|
||||
Name: "app",
|
||||
Env: []corev1.EnvVar{
|
||||
{
|
||||
Name: "SIMPLE_VAR",
|
||||
Name: "SIMPLE_VAR",
|
||||
Value: "value",
|
||||
},
|
||||
},
|
||||
@@ -960,20 +989,22 @@ func TestService_findContainerWithEnvRef_ConfigMap(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := svc.findContainerWithEnvRef(tt.containers, tt.resourceName, ResourceTypeConfigMap)
|
||||
if tt.shouldMatch {
|
||||
if got == nil {
|
||||
t.Error("Expected to find a container, got nil")
|
||||
} else if got.Name != tt.wantName {
|
||||
t.Errorf("findContainerWithEnvRef() container name = %q, want %q", got.Name, tt.wantName)
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
got := svc.findContainerWithEnvRef(tt.containers, tt.resourceName, ResourceTypeConfigMap)
|
||||
if tt.shouldMatch {
|
||||
if got == nil {
|
||||
t.Error("Expected to find a container, got nil")
|
||||
} else if got.Name != tt.wantName {
|
||||
t.Errorf("findContainerWithEnvRef() container name = %q, want %q", got.Name, tt.wantName)
|
||||
}
|
||||
} else {
|
||||
if got != nil {
|
||||
t.Errorf("Expected nil, got container %q", got.Name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if got != nil {
|
||||
t.Errorf("Expected nil, got container %q", got.Name)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -982,11 +1013,11 @@ func TestService_findContainerWithEnvRef_Secret(t *testing.T) {
|
||||
svc := NewService(cfg)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
containers []corev1.Container
|
||||
name string
|
||||
containers []corev1.Container
|
||||
resourceName string
|
||||
wantName string
|
||||
shouldMatch bool
|
||||
wantName string
|
||||
shouldMatch bool
|
||||
}{
|
||||
{
|
||||
name: "container with SecretKeyRef",
|
||||
@@ -1047,20 +1078,22 @@ func TestService_findContainerWithEnvRef_Secret(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := svc.findContainerWithEnvRef(tt.containers, tt.resourceName, ResourceTypeSecret)
|
||||
if tt.shouldMatch {
|
||||
if got == nil {
|
||||
t.Error("Expected to find a container, got nil")
|
||||
} else if got.Name != tt.wantName {
|
||||
t.Errorf("findContainerWithEnvRef() container name = %q, want %q", got.Name, tt.wantName)
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
got := svc.findContainerWithEnvRef(tt.containers, tt.resourceName, ResourceTypeSecret)
|
||||
if tt.shouldMatch {
|
||||
if got == nil {
|
||||
t.Error("Expected to find a container, got nil")
|
||||
} else if got.Name != tt.wantName {
|
||||
t.Errorf("findContainerWithEnvRef() container name = %q, want %q", got.Name, tt.wantName)
|
||||
}
|
||||
} else {
|
||||
if got != nil {
|
||||
t.Errorf("Expected nil, got container %q", got.Name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if got != nil {
|
||||
t.Errorf("Expected nil, got container %q", got.Name)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1302,9 +1335,11 @@ func TestService_ProcessCreateEventDisabled(t *testing.T) {
|
||||
cfg.ReloadOnCreate = false
|
||||
svc := NewService(cfg)
|
||||
|
||||
deploy := createTestDeployment("test", "default", map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
})
|
||||
deploy := createTestDeployment(
|
||||
"test", "default", map[string]string{
|
||||
"reloader.stakater.com/auto": "true",
|
||||
},
|
||||
)
|
||||
workloads := []workload.WorkloadAccessor{workload.NewDeploymentWorkload(deploy)}
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// RandSeq generates a random string of the specified length.
|
||||
func RandSeq(n int) string {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/stakater/Reloader/internal/pkg/config"
|
||||
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/util/wait"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
const (
|
||||
// ConfigmapResourceType represents ConfigMap resource type
|
||||
ConfigmapResourceType = "configmap"
|
||||
// SecretResourceType represents Secret resource type
|
||||
SecretResourceType = "secret"
|
||||
)
|
||||
|
||||
// CreateNamespace creates a namespace with the given name.
|
||||
func CreateNamespace(name string, client kubernetes.Interface) error {
|
||||
ns := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
}
|
||||
_, err := client.CoreV1().Namespaces().Create(context.Background(), ns, metav1.CreateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteNamespace deletes the namespace with the given name.
|
||||
func DeleteNamespace(name string, client kubernetes.Interface) error {
|
||||
return client.CoreV1().Namespaces().Delete(context.Background(), name, metav1.DeleteOptions{})
|
||||
}
|
||||
|
||||
// CreateConfigMap creates a ConfigMap with the given name and data.
|
||||
func CreateConfigMap(client kubernetes.Interface, namespace, name, data string) (*corev1.ConfigMap, error) {
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Data: map[string]string{
|
||||
"url": data,
|
||||
},
|
||||
}
|
||||
return client.CoreV1().ConfigMaps(namespace).Create(context.Background(), cm, metav1.CreateOptions{})
|
||||
}
|
||||
|
||||
// UpdateConfigMap updates the ConfigMap with new label and/or data.
|
||||
func UpdateConfigMap(cm *corev1.ConfigMap, namespace, name, label, data string) error {
|
||||
if label != "" {
|
||||
if cm.Labels == nil {
|
||||
cm.Labels = make(map[string]string)
|
||||
}
|
||||
cm.Labels["test-label"] = label
|
||||
}
|
||||
if data != "" {
|
||||
cm.Data["url"] = data
|
||||
}
|
||||
// Note: caller must have a client to update
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateConfigMapWithClient updates the ConfigMap with new label and/or data.
|
||||
func UpdateConfigMapWithClient(client kubernetes.Interface, namespace, name, label, data string) error {
|
||||
ctx := context.Background()
|
||||
cm, err := client.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if label != "" {
|
||||
if cm.Labels == nil {
|
||||
cm.Labels = make(map[string]string)
|
||||
}
|
||||
cm.Labels["test-label"] = label
|
||||
}
|
||||
if data != "" {
|
||||
cm.Data["url"] = data
|
||||
}
|
||||
_, err = client.CoreV1().ConfigMaps(namespace).Update(ctx, cm, metav1.UpdateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteConfigMap deletes the ConfigMap with the given name.
|
||||
func DeleteConfigMap(client kubernetes.Interface, namespace, name string) error {
|
||||
return client.CoreV1().ConfigMaps(namespace).Delete(context.Background(), name, metav1.DeleteOptions{})
|
||||
}
|
||||
|
||||
// CreateSecret creates a Secret with the given name and data.
|
||||
func CreateSecret(client kubernetes.Interface, namespace, name, data string) (*corev1.Secret, error) {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"password": []byte(data),
|
||||
},
|
||||
}
|
||||
return client.CoreV1().Secrets(namespace).Create(context.Background(), secret, metav1.CreateOptions{})
|
||||
}
|
||||
|
||||
// UpdateSecretWithClient updates the Secret with new label and/or data.
|
||||
func UpdateSecretWithClient(client kubernetes.Interface, namespace, name, label, data string) error {
|
||||
ctx := context.Background()
|
||||
secret, err := client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if label != "" {
|
||||
if secret.Labels == nil {
|
||||
secret.Labels = make(map[string]string)
|
||||
}
|
||||
secret.Labels["test-label"] = label
|
||||
}
|
||||
if data != "" {
|
||||
secret.Data["password"] = []byte(data)
|
||||
}
|
||||
_, err = client.CoreV1().Secrets(namespace).Update(ctx, secret, metav1.UpdateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteSecret deletes the Secret with the given name.
|
||||
func DeleteSecret(client kubernetes.Interface, namespace, name string) error {
|
||||
return client.CoreV1().Secrets(namespace).Delete(context.Background(), name, metav1.DeleteOptions{})
|
||||
}
|
||||
|
||||
// CreateDeployment creates a Deployment that references a ConfigMap/Secret.
|
||||
func CreateDeployment(client kubernetes.Interface, name, namespace string, useConfigMap bool, annotations map[string]string) (*appsv1.Deployment, error) {
|
||||
replicas := int32(1)
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: annotations,
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Replicas: &replicas,
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": name},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": name},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "main",
|
||||
Image: "busybox:1.36",
|
||||
Command: []string{"sh", "-c", "while true; do sleep 3600; done"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if useConfigMap {
|
||||
deployment.Spec.Template.Spec.Containers[0].EnvFrom = []corev1.EnvFromSource{
|
||||
{
|
||||
ConfigMapRef: &corev1.ConfigMapEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
deployment.Spec.Template.Spec.Containers[0].EnvFrom = []corev1.EnvFromSource{
|
||||
{
|
||||
SecretRef: &corev1.SecretEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return client.AppsV1().Deployments(namespace).Create(context.Background(), deployment, metav1.CreateOptions{})
|
||||
}
|
||||
|
||||
// DeleteDeployment deletes the Deployment with the given name.
|
||||
func DeleteDeployment(client kubernetes.Interface, namespace, name string) error {
|
||||
return client.AppsV1().Deployments(namespace).Delete(context.Background(), name, metav1.DeleteOptions{})
|
||||
}
|
||||
|
||||
// CreateDaemonSet creates a DaemonSet that references a ConfigMap/Secret.
|
||||
func CreateDaemonSet(client kubernetes.Interface, name, namespace string, useConfigMap bool, annotations map[string]string) (*appsv1.DaemonSet, error) {
|
||||
daemonset := &appsv1.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: annotations,
|
||||
},
|
||||
Spec: appsv1.DaemonSetSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": name},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": name},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "main",
|
||||
Image: "busybox:1.36",
|
||||
Command: []string{"sh", "-c", "while true; do sleep 3600; done"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if useConfigMap {
|
||||
daemonset.Spec.Template.Spec.Containers[0].EnvFrom = []corev1.EnvFromSource{
|
||||
{
|
||||
ConfigMapRef: &corev1.ConfigMapEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
daemonset.Spec.Template.Spec.Containers[0].EnvFrom = []corev1.EnvFromSource{
|
||||
{
|
||||
SecretRef: &corev1.SecretEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return client.AppsV1().DaemonSets(namespace).Create(context.Background(), daemonset, metav1.CreateOptions{})
|
||||
}
|
||||
|
||||
// DeleteDaemonSet deletes the DaemonSet with the given name.
|
||||
func DeleteDaemonSet(client kubernetes.Interface, namespace, name string) error {
|
||||
return client.AppsV1().DaemonSets(namespace).Delete(context.Background(), name, metav1.DeleteOptions{})
|
||||
}
|
||||
|
||||
// CreateStatefulSet creates a StatefulSet that references a ConfigMap/Secret.
|
||||
func CreateStatefulSet(client kubernetes.Interface, name, namespace string, useConfigMap bool, annotations map[string]string) (*appsv1.StatefulSet, error) {
|
||||
replicas := int32(1)
|
||||
statefulset := &appsv1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: annotations,
|
||||
},
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
Replicas: &replicas,
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": name},
|
||||
},
|
||||
ServiceName: name,
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": name},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "main",
|
||||
Image: "busybox:1.36",
|
||||
Command: []string{"sh", "-c", "while true; do sleep 3600; done"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if useConfigMap {
|
||||
statefulset.Spec.Template.Spec.Containers[0].EnvFrom = []corev1.EnvFromSource{
|
||||
{
|
||||
ConfigMapRef: &corev1.ConfigMapEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
statefulset.Spec.Template.Spec.Containers[0].EnvFrom = []corev1.EnvFromSource{
|
||||
{
|
||||
SecretRef: &corev1.SecretEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return client.AppsV1().StatefulSets(namespace).Create(context.Background(), statefulset, metav1.CreateOptions{})
|
||||
}
|
||||
|
||||
// DeleteStatefulSet deletes the StatefulSet with the given name.
|
||||
func DeleteStatefulSet(client kubernetes.Interface, namespace, name string) error {
|
||||
return client.AppsV1().StatefulSets(namespace).Delete(context.Background(), name, metav1.DeleteOptions{})
|
||||
}
|
||||
|
||||
// CreateCronJob creates a CronJob that references a ConfigMap/Secret.
|
||||
func CreateCronJob(client kubernetes.Interface, name, namespace string, useConfigMap bool, annotations map[string]string) (*batchv1.CronJob, error) {
|
||||
cronjob := &batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: annotations,
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "*/5 * * * *",
|
||||
JobTemplate: batchv1.JobTemplateSpec{
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
RestartPolicy: corev1.RestartPolicyOnFailure,
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "main",
|
||||
Image: "busybox:1.36",
|
||||
Command: []string{"sh", "-c", "echo hello"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if useConfigMap {
|
||||
cronjob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].EnvFrom = []corev1.EnvFromSource{
|
||||
{
|
||||
ConfigMapRef: &corev1.ConfigMapEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
cronjob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].EnvFrom = []corev1.EnvFromSource{
|
||||
{
|
||||
SecretRef: &corev1.SecretEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return client.BatchV1().CronJobs(namespace).Create(context.Background(), cronjob, metav1.CreateOptions{})
|
||||
}
|
||||
|
||||
// DeleteCronJob deletes the CronJob with the given name.
|
||||
func DeleteCronJob(client kubernetes.Interface, namespace, name string) error {
|
||||
return client.BatchV1().CronJobs(namespace).Delete(context.Background(), name, metav1.DeleteOptions{})
|
||||
}
|
||||
|
||||
// ConvertResourceToSHA converts a resource data to SHA256 hash.
|
||||
func ConvertResourceToSHA(resourceType, namespace, name, data string) string {
|
||||
content := fmt.Sprintf("%s/%s/%s:%s", resourceType, namespace, name, data)
|
||||
hash := sha256.Sum256([]byte(content))
|
||||
return base64.StdEncoding.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// WaitForDeploymentAnnotation waits for a deployment to have the specified annotation value.
|
||||
func WaitForDeploymentAnnotation(client kubernetes.Interface, namespace, name, annotation, expectedValue string, timeout time.Duration) error {
|
||||
return wait.PollImmediate(time.Second, timeout, func() (bool, error) {
|
||||
deployment, err := client.AppsV1().Deployments(namespace).Get(context.Background(), name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false, nil // Keep waiting
|
||||
}
|
||||
value, ok := deployment.Spec.Template.Annotations[annotation]
|
||||
if !ok {
|
||||
return false, nil // Keep waiting
|
||||
}
|
||||
return value == expectedValue, nil
|
||||
})
|
||||
}
|
||||
|
||||
// WaitForDeploymentReloadedAnnotation waits for a deployment to have any reloaded annotation.
|
||||
func WaitForDeploymentReloadedAnnotation(client kubernetes.Interface, namespace, name string, cfg *config.Config, timeout time.Duration) (bool, error) {
|
||||
var found bool
|
||||
err := wait.PollImmediate(time.Second, timeout, func() (bool, error) {
|
||||
deployment, err := client.AppsV1().Deployments(namespace).Get(context.Background(), name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false, nil // Keep waiting
|
||||
}
|
||||
// Check for the last-reloaded-from annotation in pod template
|
||||
if deployment.Spec.Template.Annotations != nil {
|
||||
if _, ok := deployment.Spec.Template.Annotations[cfg.Annotations.LastReloadedFrom]; ok {
|
||||
found = true
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
if err == wait.ErrWaitTimeout {
|
||||
return found, nil
|
||||
}
|
||||
return found, err
|
||||
}
|
||||
|
||||
// WaitForDaemonSetReloadedAnnotation waits for a daemonset to have any reloaded annotation.
|
||||
func WaitForDaemonSetReloadedAnnotation(client kubernetes.Interface, namespace, name string, cfg *config.Config, timeout time.Duration) (bool, error) {
|
||||
var found bool
|
||||
err := wait.PollImmediate(time.Second, timeout, func() (bool, error) {
|
||||
daemonset, err := client.AppsV1().DaemonSets(namespace).Get(context.Background(), name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false, nil // Keep waiting
|
||||
}
|
||||
// Check for the last-reloaded-from annotation in pod template
|
||||
if daemonset.Spec.Template.Annotations != nil {
|
||||
if _, ok := daemonset.Spec.Template.Annotations[cfg.Annotations.LastReloadedFrom]; ok {
|
||||
found = true
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
if err == wait.ErrWaitTimeout {
|
||||
return found, nil
|
||||
}
|
||||
return found, err
|
||||
}
|
||||
|
||||
// WaitForStatefulSetReloadedAnnotation waits for a statefulset to have any reloaded annotation.
|
||||
func WaitForStatefulSetReloadedAnnotation(client kubernetes.Interface, namespace, name string, cfg *config.Config, timeout time.Duration) (bool, error) {
|
||||
var found bool
|
||||
err := wait.PollImmediate(time.Second, timeout, func() (bool, error) {
|
||||
statefulset, err := client.AppsV1().StatefulSets(namespace).Get(context.Background(), name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false, nil // Keep waiting
|
||||
}
|
||||
// Check for the last-reloaded-from annotation in pod template
|
||||
if statefulset.Spec.Template.Annotations != nil {
|
||||
if _, ok := statefulset.Spec.Template.Annotations[cfg.Annotations.LastReloadedFrom]; ok {
|
||||
found = true
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
if err == wait.ErrWaitTimeout {
|
||||
return found, nil
|
||||
}
|
||||
return found, err
|
||||
}
|
||||
@@ -84,18 +84,18 @@ func (r *Registry) FromObject(obj client.Object) (WorkloadAccessor, error) {
|
||||
// kindAliases maps string representations to Kind constants.
|
||||
// Supports lowercase, title case, and plural forms for user convenience.
|
||||
var kindAliases = map[string]Kind{
|
||||
"deployment": KindDeployment,
|
||||
"deployments": KindDeployment,
|
||||
"daemonset": KindDaemonSet,
|
||||
"daemonsets": KindDaemonSet,
|
||||
"statefulset": KindStatefulSet,
|
||||
"deployment": KindDeployment,
|
||||
"deployments": KindDeployment,
|
||||
"daemonset": KindDaemonSet,
|
||||
"daemonsets": KindDaemonSet,
|
||||
"statefulset": KindStatefulSet,
|
||||
"statefulsets": KindStatefulSet,
|
||||
"rollout": KindArgoRollout,
|
||||
"rollouts": KindArgoRollout,
|
||||
"job": KindJob,
|
||||
"jobs": KindJob,
|
||||
"cronjob": KindCronJob,
|
||||
"cronjobs": KindCronJob,
|
||||
"rollout": KindArgoRollout,
|
||||
"rollouts": KindArgoRollout,
|
||||
"job": KindJob,
|
||||
"jobs": KindJob,
|
||||
"cronjob": KindCronJob,
|
||||
"cronjobs": KindCronJob,
|
||||
}
|
||||
|
||||
// KindFromString converts a string to a Kind.
|
||||
|
||||
@@ -61,8 +61,8 @@ func TestRegistry_ListerFor_AllKinds(t *testing.T) {
|
||||
r := NewRegistry(true)
|
||||
|
||||
tests := []struct {
|
||||
kind Kind
|
||||
wantNil bool
|
||||
kind Kind
|
||||
wantNil bool
|
||||
}{
|
||||
{KindDeployment, false},
|
||||
{KindDaemonSet, false},
|
||||
|
||||
@@ -0,0 +1,519 @@
|
||||
// Package e2e contains end-to-end tests for Reloader.
|
||||
// These tests run against a real Kubernetes cluster (or envtest).
|
||||
//
|
||||
// To run these tests against a real cluster:
|
||||
//
|
||||
// KUBECONFIG=~/.kube/config go test -v ./test/e2e/... -count=1
|
||||
//
|
||||
// To skip these tests when running unit tests:
|
||||
//
|
||||
// go test -v ./... -short
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-logr/zerologr"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stakater/Reloader/internal/pkg/config"
|
||||
"github.com/stakater/Reloader/internal/pkg/controller"
|
||||
"github.com/stakater/Reloader/internal/pkg/metrics"
|
||||
"github.com/stakater/Reloader/internal/pkg/testutil"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
ctrllog "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
)
|
||||
|
||||
const (
|
||||
testNamespacePrefix = "test-reloader-e2e-"
|
||||
waitTimeout = 30 * time.Second
|
||||
setupDelay = 2 * time.Second
|
||||
negativeTestTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
k8sClient kubernetes.Interface
|
||||
cfg *config.Config
|
||||
namespace string
|
||||
skipE2ETests bool
|
||||
cancelManager context.CancelFunc
|
||||
restCfg *rest.Config
|
||||
)
|
||||
|
||||
// testFixture provides a clean way to set up and tear down test resources.
|
||||
type testFixture struct {
|
||||
t *testing.T
|
||||
name string
|
||||
configMaps []string
|
||||
secrets []string
|
||||
workloads []workloadInfo
|
||||
}
|
||||
|
||||
type workloadInfo struct {
|
||||
name string
|
||||
kind string // "deployment", "daemonset", "statefulset"
|
||||
}
|
||||
|
||||
// newFixture creates a new test fixture with a unique name prefix.
|
||||
func newFixture(t *testing.T, prefix string) *testFixture {
|
||||
t.Helper()
|
||||
skipIfNoCluster(t)
|
||||
return &testFixture{
|
||||
t: t,
|
||||
name: prefix + "-" + testutil.RandSeq(5),
|
||||
}
|
||||
}
|
||||
|
||||
// createConfigMap creates a ConfigMap and registers it for cleanup.
|
||||
func (f *testFixture) createConfigMap(name, data string) {
|
||||
f.t.Helper()
|
||||
_, err := testutil.CreateConfigMap(k8sClient, namespace, name, data)
|
||||
if err != nil {
|
||||
f.t.Fatalf("Failed to create ConfigMap %s: %v", name, err)
|
||||
}
|
||||
f.configMaps = append(f.configMaps, name)
|
||||
}
|
||||
|
||||
// createSecret creates a Secret and registers it for cleanup.
|
||||
func (f *testFixture) createSecret(name, data string) {
|
||||
f.t.Helper()
|
||||
_, err := testutil.CreateSecret(k8sClient, namespace, name, data)
|
||||
if err != nil {
|
||||
f.t.Fatalf("Failed to create Secret %s: %v", name, err)
|
||||
}
|
||||
f.secrets = append(f.secrets, name)
|
||||
}
|
||||
|
||||
// createDeployment creates a Deployment and registers it for cleanup.
|
||||
func (f *testFixture) createDeployment(name string, useConfigMap bool, annotations map[string]string) {
|
||||
f.t.Helper()
|
||||
_, err := testutil.CreateDeployment(k8sClient, name, namespace, useConfigMap, annotations)
|
||||
if err != nil {
|
||||
f.t.Fatalf("Failed to create Deployment %s: %v", name, err)
|
||||
}
|
||||
f.workloads = append(f.workloads, workloadInfo{name: name, kind: "deployment"})
|
||||
}
|
||||
|
||||
// createDaemonSet creates a DaemonSet and registers it for cleanup.
|
||||
func (f *testFixture) createDaemonSet(name string, useConfigMap bool, annotations map[string]string) {
|
||||
f.t.Helper()
|
||||
_, err := testutil.CreateDaemonSet(k8sClient, name, namespace, useConfigMap, annotations)
|
||||
if err != nil {
|
||||
f.t.Fatalf("Failed to create DaemonSet %s: %v", name, err)
|
||||
}
|
||||
f.workloads = append(f.workloads, workloadInfo{name: name, kind: "daemonset"})
|
||||
}
|
||||
|
||||
// createStatefulSet creates a StatefulSet and registers it for cleanup.
|
||||
func (f *testFixture) createStatefulSet(name string, useConfigMap bool, annotations map[string]string) {
|
||||
f.t.Helper()
|
||||
_, err := testutil.CreateStatefulSet(k8sClient, name, namespace, useConfigMap, annotations)
|
||||
if err != nil {
|
||||
f.t.Fatalf("Failed to create StatefulSet %s: %v", name, err)
|
||||
}
|
||||
f.workloads = append(f.workloads, workloadInfo{name: name, kind: "statefulset"})
|
||||
}
|
||||
|
||||
// waitForReady waits for all workloads to be ready.
|
||||
func (f *testFixture) waitForReady() {
|
||||
time.Sleep(setupDelay)
|
||||
}
|
||||
|
||||
// updateConfigMap updates a ConfigMap's data.
|
||||
func (f *testFixture) updateConfigMap(name, data string) {
|
||||
f.t.Helper()
|
||||
if err := testutil.UpdateConfigMapWithClient(k8sClient, namespace, name, "", data); err != nil {
|
||||
f.t.Fatalf("Failed to update ConfigMap %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// updateConfigMapLabel updates only a ConfigMap's label (not data).
|
||||
func (f *testFixture) updateConfigMapLabel(name, label string) {
|
||||
f.t.Helper()
|
||||
// Get current data first
|
||||
cm, err := k8sClient.CoreV1().ConfigMaps(namespace).Get(context.Background(), name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
f.t.Fatalf("Failed to get ConfigMap %s: %v", name, err)
|
||||
}
|
||||
data := cm.Data["url"]
|
||||
if err := testutil.UpdateConfigMapWithClient(k8sClient, namespace, name, label, data); err != nil {
|
||||
f.t.Fatalf("Failed to update ConfigMap label %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// updateSecret updates a Secret's data.
|
||||
func (f *testFixture) updateSecret(name, data string) {
|
||||
f.t.Helper()
|
||||
if err := testutil.UpdateSecretWithClient(k8sClient, namespace, name, "", data); err != nil {
|
||||
f.t.Fatalf("Failed to update Secret %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// assertDeploymentReloaded asserts that a deployment was reloaded.
|
||||
func (f *testFixture) assertDeploymentReloaded(name string, testCfg *config.Config) {
|
||||
f.t.Helper()
|
||||
if testCfg == nil {
|
||||
testCfg = cfg
|
||||
}
|
||||
updated, err := testutil.WaitForDeploymentReloadedAnnotation(k8sClient, namespace, name, testCfg, waitTimeout)
|
||||
if err != nil {
|
||||
f.t.Fatalf("Error waiting for deployment %s update: %v", name, err)
|
||||
}
|
||||
if !updated {
|
||||
f.t.Errorf("Deployment %s was not updated after resource change", name)
|
||||
}
|
||||
}
|
||||
|
||||
// assertDeploymentNotReloaded asserts that a deployment was NOT reloaded.
|
||||
func (f *testFixture) assertDeploymentNotReloaded(name string, testCfg *config.Config) {
|
||||
f.t.Helper()
|
||||
if testCfg == nil {
|
||||
testCfg = cfg
|
||||
}
|
||||
time.Sleep(negativeTestTimeout)
|
||||
updated, _ := testutil.WaitForDeploymentReloadedAnnotation(k8sClient, namespace, name, testCfg, negativeTestTimeout)
|
||||
if updated {
|
||||
f.t.Errorf("Deployment %s should not have been updated", name)
|
||||
}
|
||||
}
|
||||
|
||||
// assertDaemonSetReloaded asserts that a daemonset was reloaded.
|
||||
func (f *testFixture) assertDaemonSetReloaded(name string) {
|
||||
f.t.Helper()
|
||||
updated, err := testutil.WaitForDaemonSetReloadedAnnotation(k8sClient, namespace, name, cfg, waitTimeout)
|
||||
if err != nil {
|
||||
f.t.Fatalf("Error waiting for daemonset %s update: %v", name, err)
|
||||
}
|
||||
if !updated {
|
||||
f.t.Errorf("DaemonSet %s was not updated after resource change", name)
|
||||
}
|
||||
}
|
||||
|
||||
// assertStatefulSetReloaded asserts that a statefulset was reloaded.
|
||||
func (f *testFixture) assertStatefulSetReloaded(name string) {
|
||||
f.t.Helper()
|
||||
updated, err := testutil.WaitForStatefulSetReloadedAnnotation(k8sClient, namespace, name, cfg, waitTimeout)
|
||||
if err != nil {
|
||||
f.t.Fatalf("Error waiting for statefulset %s update: %v", name, err)
|
||||
}
|
||||
if !updated {
|
||||
f.t.Errorf("StatefulSet %s was not updated after resource change", name)
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup removes all created resources.
|
||||
func (f *testFixture) cleanup() {
|
||||
for _, w := range f.workloads {
|
||||
switch w.kind {
|
||||
case "deployment":
|
||||
_ = testutil.DeleteDeployment(k8sClient, namespace, w.name)
|
||||
case "daemonset":
|
||||
_ = testutil.DeleteDaemonSet(k8sClient, namespace, w.name)
|
||||
case "statefulset":
|
||||
_ = testutil.DeleteStatefulSet(k8sClient, namespace, w.name)
|
||||
}
|
||||
}
|
||||
for _, name := range f.configMaps {
|
||||
_ = testutil.DeleteConfigMap(k8sClient, namespace, name)
|
||||
}
|
||||
for _, name := range f.secrets {
|
||||
_ = testutil.DeleteSecret(k8sClient, namespace, name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
flag.Parse()
|
||||
|
||||
if testing.Short() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Set up zerolog as the controller-runtime logger
|
||||
zl := zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).
|
||||
Level(zerolog.WarnLevel).
|
||||
With().
|
||||
Timestamp().
|
||||
Logger()
|
||||
ctrllog.SetLogger(zerologr.New(&zl))
|
||||
|
||||
kubeconfig := os.Getenv("KUBECONFIG")
|
||||
if kubeconfig == "" {
|
||||
kubeconfig = os.Getenv("HOME") + "/.kube/config"
|
||||
}
|
||||
|
||||
var err error
|
||||
restCfg, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||
if err != nil {
|
||||
skipE2ETests = true
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
k8sClient, err = kubernetes.NewForConfig(restCfg)
|
||||
if err != nil {
|
||||
skipE2ETests = true
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if _, err = k8sClient.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{}); err != nil {
|
||||
skipE2ETests = true
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
namespace = testNamespacePrefix + testutil.RandSeq(5)
|
||||
if err := testutil.CreateNamespace(namespace, k8sClient); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cfg = config.NewDefault()
|
||||
cfg.AutoReloadAll = false
|
||||
|
||||
_, cancelManager = startManagerWithConfig(cfg, restCfg)
|
||||
|
||||
code := m.Run()
|
||||
|
||||
if cancelManager != nil {
|
||||
cancelManager()
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
_ = testutil.DeleteNamespace(namespace, k8sClient)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func skipIfNoCluster(t *testing.T) {
|
||||
if skipE2ETests {
|
||||
t.Skip("Skipping e2e test: no Kubernetes cluster available")
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigMapUpdate tests that updating a ConfigMap triggers a workload reload.
|
||||
func TestConfigMapUpdate(t *testing.T) {
|
||||
f := newFixture(t, "cm-update")
|
||||
defer f.cleanup()
|
||||
|
||||
f.createConfigMap(f.name, "initial-data")
|
||||
f.createDeployment(f.name, true, map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: f.name,
|
||||
})
|
||||
f.waitForReady()
|
||||
|
||||
f.updateConfigMap(f.name, "updated-data")
|
||||
f.assertDeploymentReloaded(f.name, nil)
|
||||
}
|
||||
|
||||
// TestSecretUpdate tests that updating a Secret triggers a workload reload.
|
||||
func TestSecretUpdate(t *testing.T) {
|
||||
f := newFixture(t, "secret-update")
|
||||
defer f.cleanup()
|
||||
|
||||
f.createSecret(f.name, "initial-secret")
|
||||
f.createDeployment(f.name, false, map[string]string{
|
||||
cfg.Annotations.SecretReload: f.name,
|
||||
})
|
||||
f.waitForReady()
|
||||
|
||||
f.updateSecret(f.name, "updated-secret")
|
||||
f.assertDeploymentReloaded(f.name, nil)
|
||||
}
|
||||
|
||||
// TestAutoReloadAll tests the auto-reload-all feature.
|
||||
func TestAutoReloadAll(t *testing.T) {
|
||||
f := newFixture(t, "auto-reload")
|
||||
defer f.cleanup()
|
||||
|
||||
f.createConfigMap(f.name, "initial-data")
|
||||
f.createDeployment(f.name, true, map[string]string{
|
||||
cfg.Annotations.Auto: "true",
|
||||
})
|
||||
f.waitForReady()
|
||||
|
||||
f.updateConfigMap(f.name, "updated-data")
|
||||
f.assertDeploymentReloaded(f.name, nil)
|
||||
}
|
||||
|
||||
// TestDaemonSetReload tests that DaemonSets are reloaded when ConfigMaps change.
|
||||
func TestDaemonSetReload(t *testing.T) {
|
||||
f := newFixture(t, "ds-reload")
|
||||
defer f.cleanup()
|
||||
|
||||
f.createConfigMap(f.name, "initial-data")
|
||||
f.createDaemonSet(f.name, true, map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: f.name,
|
||||
})
|
||||
f.waitForReady()
|
||||
|
||||
f.updateConfigMap(f.name, "updated-data")
|
||||
f.assertDaemonSetReloaded(f.name)
|
||||
}
|
||||
|
||||
// TestStatefulSetReload tests that StatefulSets are reloaded when Secrets change.
|
||||
func TestStatefulSetReload(t *testing.T) {
|
||||
f := newFixture(t, "sts-reload")
|
||||
defer f.cleanup()
|
||||
|
||||
f.createSecret(f.name, "initial-secret")
|
||||
f.createStatefulSet(f.name, false, map[string]string{
|
||||
cfg.Annotations.SecretReload: f.name,
|
||||
})
|
||||
f.waitForReady()
|
||||
|
||||
f.updateSecret(f.name, "updated-secret")
|
||||
f.assertStatefulSetReloaded(f.name)
|
||||
}
|
||||
|
||||
// TestLabelOnlyChange tests that label-only changes don't trigger reloads.
|
||||
func TestLabelOnlyChange(t *testing.T) {
|
||||
f := newFixture(t, "label-only")
|
||||
defer f.cleanup()
|
||||
|
||||
f.createConfigMap(f.name, "initial-data")
|
||||
f.createDeployment(f.name, true, map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: f.name,
|
||||
})
|
||||
f.waitForReady()
|
||||
|
||||
f.updateConfigMapLabel(f.name, "new-label")
|
||||
f.assertDeploymentNotReloaded(f.name, nil)
|
||||
}
|
||||
|
||||
// TestMultipleConfigMaps tests watching multiple ConfigMaps in a single annotation.
|
||||
func TestMultipleConfigMaps(t *testing.T) {
|
||||
f := newFixture(t, "multi-cm")
|
||||
defer f.cleanup()
|
||||
|
||||
cm1 := f.name + "-a"
|
||||
cm2 := f.name + "-b"
|
||||
|
||||
f.createConfigMap(cm1, "data-a")
|
||||
f.createConfigMap(cm2, "data-b")
|
||||
f.createDeployment(f.name, true, map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: cm1 + "," + cm2,
|
||||
})
|
||||
f.waitForReady()
|
||||
|
||||
f.updateConfigMap(cm1, "updated-data-a")
|
||||
f.assertDeploymentReloaded(f.name, nil)
|
||||
}
|
||||
|
||||
// TestAutoAnnotationDisabled tests that auto: "false" disables auto-reload.
|
||||
func TestAutoAnnotationDisabled(t *testing.T) {
|
||||
f := newFixture(t, "auto-disabled")
|
||||
defer f.cleanup()
|
||||
|
||||
testCfg := config.NewDefault()
|
||||
testCfg.AutoReloadAll = true
|
||||
|
||||
f.createConfigMap(f.name, "initial-data")
|
||||
f.createDeployment(f.name, true, map[string]string{
|
||||
testCfg.Annotations.Auto: "false",
|
||||
})
|
||||
f.waitForReady()
|
||||
|
||||
f.updateConfigMap(f.name, "updated-data")
|
||||
f.assertDeploymentNotReloaded(f.name, testCfg)
|
||||
}
|
||||
|
||||
// TestAutoWithExplicitConfigMapAnnotation tests that a deployment with auto=true
|
||||
// also reloads when an explicitly annotated (non-referenced) ConfigMap changes.
|
||||
func TestAutoWithExplicitConfigMapAnnotation(t *testing.T) {
|
||||
f := newFixture(t, "auto-explicit-cm")
|
||||
defer f.cleanup()
|
||||
|
||||
referencedCM := f.name + "-ref"
|
||||
explicitCM := f.name + "-explicit"
|
||||
|
||||
f.createConfigMap(referencedCM, "referenced-data")
|
||||
f.createConfigMap(explicitCM, "explicit-data")
|
||||
f.createDeployment(referencedCM, true, map[string]string{
|
||||
cfg.Annotations.Auto: "true",
|
||||
cfg.Annotations.ConfigmapReload: explicitCM,
|
||||
})
|
||||
f.waitForReady()
|
||||
|
||||
// Update the EXPLICIT ConfigMap (not the referenced one)
|
||||
f.updateConfigMap(explicitCM, "updated-explicit-data")
|
||||
f.assertDeploymentReloaded(referencedCM, nil)
|
||||
}
|
||||
|
||||
// TestAutoWithExplicitSecretAnnotation tests that a deployment with auto=true
|
||||
// also reloads when an explicitly annotated (non-referenced) Secret changes.
|
||||
func TestAutoWithExplicitSecretAnnotation(t *testing.T) {
|
||||
f := newFixture(t, "auto-explicit-secret")
|
||||
defer f.cleanup()
|
||||
|
||||
referencedSecret := f.name + "-ref"
|
||||
explicitSecret := f.name + "-explicit"
|
||||
|
||||
f.createSecret(referencedSecret, "referenced-secret")
|
||||
f.createSecret(explicitSecret, "explicit-secret")
|
||||
f.createDeployment(referencedSecret, false, map[string]string{
|
||||
cfg.Annotations.Auto: "true",
|
||||
cfg.Annotations.SecretReload: explicitSecret,
|
||||
})
|
||||
f.waitForReady()
|
||||
|
||||
// Update the EXPLICIT Secret (not the referenced one)
|
||||
f.updateSecret(explicitSecret, "updated-explicit-secret")
|
||||
f.assertDeploymentReloaded(referencedSecret, nil)
|
||||
}
|
||||
|
||||
// TestAutoWithBothExplicitAndReferencedChange tests that auto + explicit annotations
|
||||
// work correctly when the referenced resource changes.
|
||||
func TestAutoWithBothExplicitAndReferencedChange(t *testing.T) {
|
||||
f := newFixture(t, "auto-both")
|
||||
defer f.cleanup()
|
||||
|
||||
referencedCM := f.name + "-ref"
|
||||
explicitCM := f.name + "-explicit"
|
||||
|
||||
f.createConfigMap(referencedCM, "referenced-data")
|
||||
f.createConfigMap(explicitCM, "explicit-data")
|
||||
f.createDeployment(referencedCM, true, map[string]string{
|
||||
cfg.Annotations.Auto: "true",
|
||||
cfg.Annotations.ConfigmapReload: explicitCM,
|
||||
})
|
||||
f.waitForReady()
|
||||
|
||||
// Update the REFERENCED ConfigMap - should trigger reload via auto
|
||||
f.updateConfigMap(referencedCM, "updated-referenced-data")
|
||||
f.assertDeploymentReloaded(referencedCM, nil)
|
||||
}
|
||||
|
||||
// startManagerWithConfig creates and starts a controller-runtime manager for e2e testing.
|
||||
func startManagerWithConfig(cfg *config.Config, restConfig *rest.Config) (manager.Manager, context.CancelFunc) {
|
||||
collectors := metrics.NewCollectors()
|
||||
mgr, err := controller.NewManagerWithRestConfig(
|
||||
controller.ManagerOptions{
|
||||
Config: cfg,
|
||||
Log: ctrl.Log.WithName("test-manager"),
|
||||
Collectors: &collectors,
|
||||
}, restConfig,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create manager: %v", err)
|
||||
}
|
||||
|
||||
if err := controller.SetupReconcilers(mgr, cfg, ctrl.Log.WithName("test-reconcilers"), &collectors); err != nil {
|
||||
log.Fatalf("Failed to setup reconcilers: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
go func() {
|
||||
if err := controller.RunManager(ctx, mgr, ctrl.Log.WithName("test-runner")); err != nil {
|
||||
log.Printf("Manager exited: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
return mgr, cancel
|
||||
}
|
||||
Reference in New Issue
Block a user