diff --git a/go.mod b/go.mod index a6d752df..4c8e4169 100644 --- a/go.mod +++ b/go.mod @@ -4,23 +4,20 @@ go 1.26.3 require ( github.com/argoproj/argo-rollouts v1.9.0 - github.com/go-logr/logr v1.4.3 - github.com/go-logr/zerologr v1.2.3 github.com/onsi/ginkgo/v2 v2.27.4 github.com/onsi/gomega v1.39.0 github.com/openshift/api v0.0.0-20260402111718-ad9eb11110b6 github.com/openshift/client-go v0.0.0-20260330134249-7e1499aaacd7 + github.com/parnurzeal/gorequest v0.3.0 github.com/prometheus/client_golang v1.23.2 - github.com/prometheus/client_model v0.6.2 - github.com/rs/zerolog v1.35.1 + github.com/sirupsen/logrus v1.9.4 github.com/spf13/cobra v1.10.2 - github.com/spf13/pflag v1.0.10 - github.com/spf13/viper v1.12.0 - k8s.io/api v0.36.0 - k8s.io/apimachinery v0.36.0 - k8s.io/client-go v0.36.0 + github.com/stretchr/testify v1.11.1 + k8s.io/api v0.35.3 + k8s.io/apimachinery v0.35.3 + k8s.io/client-go v0.35.3 + k8s.io/kubectl v0.35.3 k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 - sigs.k8s.io/controller-runtime v0.24.1 sigs.k8s.io/secrets-store-csi-driver v1.5.5 ) @@ -55,7 +52,6 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bkielbasa/cyclop v1.2.3 // indirect - github.com/blang/semver/v4 v4.0.0 // indirect github.com/blizzy78/varnamelen v0.8.0 // indirect github.com/bombsimon/wsl/v4 v4.7.0 // indirect github.com/bombsimon/wsl/v5 v5.3.0 // indirect @@ -79,18 +75,18 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect - github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/ettle/strcase v0.2.0 // indirect - github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fatih/structtag v1.2.0 // indirect github.com/firefart/nonamedreturns v1.0.6 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.9.1 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect github.com/ghostiam/protogetter v0.3.18 // indirect github.com/go-critic/go-critic v0.14.3 // indirect - github.com/go-errors/errors v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-openapi/jsonpointer v0.22.5 // indirect github.com/go-openapi/jsonreference v0.21.5 // indirect github.com/go-openapi/swag v0.25.5 // indirect @@ -178,20 +174,23 @@ require ( github.com/mgechev/revive v1.13.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/moby/spdystream v0.5.1 // indirect + github.com/moby/spdystream v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect - github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/moricho/tparallel v0.3.2 // indirect + github.com/moul/http2curl v1.0.0 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/nakabonne/nestif v0.3.1 // indirect github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect github.com/nunnatsa/ginkgolinter v0.21.2 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.1 // indirect github.com/quasilyte/go-ruleguard v0.4.5 // indirect @@ -209,18 +208,18 @@ require ( github.com/sashamelentyev/interfacebloat v1.1.0 // indirect github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect github.com/securego/gosec/v2 v2.22.11 // indirect - github.com/sergi/go-diff v1.2.0 // indirect - github.com/sirupsen/logrus v1.9.4 // indirect github.com/sivchari/containedctx v1.0.3 // indirect + github.com/smartystreets/goconvey v1.7.2 // indirect github.com/sonatard/noctx v0.4.0 // indirect github.com/sourcegraph/go-diff v0.7.0 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.12.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/stretchr/testify v1.11.1 // indirect github.com/subosito/gotenv v1.4.1 // indirect github.com/tetafro/godot v1.5.4 // indirect github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect @@ -233,7 +232,6 @@ require ( github.com/uudashr/iface v1.4.1 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xen0n/gosmopolitan v1.3.0 // indirect - github.com/xlab/treeprint v1.2.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yagipy/maintidx v1.0.0 // indirect github.com/yeya24/promlinter v0.3.0 // indirect @@ -245,7 +243,7 @@ require ( go.augendre.info/fatcontext v0.9.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.1 // indirect + go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 // indirect @@ -259,25 +257,18 @@ require ( golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.44.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect honnef.co/go/tools v0.6.1 // indirect - k8s.io/apiextensions-apiserver v0.36.0 // indirect - k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20260330154417-16be699c7b31 // indirect - k8s.io/streaming v0.36.0 // indirect mvdan.cc/gofumpt v0.9.2 // indirect mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect - sigs.k8s.io/kustomize/api v0.20.1 // indirect - sigs.k8s.io/kustomize/cmd/config v0.20.1 // indirect - sigs.k8s.io/kustomize/kustomize/v5 v5.7.1 // indirect - sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/internal/pkg/handler/create_test.go b/internal/pkg/handler/create_test.go new file mode 100644 index 00000000..ef21f06b --- /dev/null +++ b/internal/pkg/handler/create_test.go @@ -0,0 +1,353 @@ +package handler + +import ( + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/stakater/Reloader/internal/pkg/constants" + "github.com/stakater/Reloader/internal/pkg/metrics" +) + +func TestResourceCreatedHandler_GetConfig(t *testing.T) { + tests := []struct { + name string + resource interface{} + expectedName string + expectedNS string + expectedType string + expectSHANotEmpty bool + expectOldSHAEmpty bool + }{ + { + name: "ConfigMap with data", + resource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-configmap", + Namespace: "test-ns", + }, + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + expectedName: "my-configmap", + expectedNS: "test-ns", + expectedType: constants.ConfigmapEnvVarPostfix, + expectSHANotEmpty: true, + expectOldSHAEmpty: true, + }, + { + name: "ConfigMap with empty data", + resource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "empty-configmap", + Namespace: "default", + }, + Data: map[string]string{}, + }, + expectedName: "empty-configmap", + expectedNS: "default", + expectedType: constants.ConfigmapEnvVarPostfix, + expectSHANotEmpty: true, + expectOldSHAEmpty: true, + }, + { + name: "ConfigMap with binary data", + resource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binary-configmap", + Namespace: "default", + }, + BinaryData: map[string][]byte{ + "binary-key": []byte("binary-value"), + }, + }, + expectedName: "binary-configmap", + expectedNS: "default", + expectedType: constants.ConfigmapEnvVarPostfix, + expectSHANotEmpty: true, + expectOldSHAEmpty: true, + }, + { + name: "ConfigMap with annotations", + resource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "annotated-configmap", + Namespace: "default", + Annotations: map[string]string{ + "reloader.stakater.com/match": "true", + }, + }, + Data: map[string]string{"key": "value"}, + }, + expectedName: "annotated-configmap", + expectedNS: "default", + expectedType: constants.ConfigmapEnvVarPostfix, + expectSHANotEmpty: true, + expectOldSHAEmpty: true, + }, + { + name: "Secret with data", + resource: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-secret", + Namespace: "secret-ns", + }, + Data: map[string][]byte{ + "password": []byte("secret-password"), + }, + }, + expectedName: "my-secret", + expectedNS: "secret-ns", + expectedType: constants.SecretEnvVarPostfix, + expectSHANotEmpty: true, + expectOldSHAEmpty: true, + }, + { + name: "Secret with empty data", + resource: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "empty-secret", + Namespace: "default", + }, + Data: map[string][]byte{}, + }, + expectedName: "empty-secret", + expectedNS: "default", + expectedType: constants.SecretEnvVarPostfix, + expectSHANotEmpty: true, + expectOldSHAEmpty: true, + }, + { + name: "Secret with StringData", + resource: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "stringdata-secret", + Namespace: "default", + }, + StringData: map[string]string{ + "username": "admin", + }, + }, + expectedName: "stringdata-secret", + expectedNS: "default", + expectedType: constants.SecretEnvVarPostfix, + expectSHANotEmpty: true, + expectOldSHAEmpty: true, + }, + { + name: "Secret with labels", + resource: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "labeled-secret", + Namespace: "default", + Labels: map[string]string{ + "app": "test", + }, + }, + Data: map[string][]byte{"key": []byte("value")}, + }, + expectedName: "labeled-secret", + expectedNS: "default", + expectedType: constants.SecretEnvVarPostfix, + expectSHANotEmpty: true, + expectOldSHAEmpty: true, + }, + { + name: "Invalid resource type - string", + resource: "invalid-string", + expectedName: "", + expectedNS: "", + expectedType: "", + expectSHANotEmpty: false, + expectOldSHAEmpty: true, + }, + { + name: "Invalid resource type - int", + resource: 123, + expectedName: "", + expectedNS: "", + expectedType: "", + expectSHANotEmpty: false, + expectOldSHAEmpty: true, + }, + { + name: "Invalid resource type - struct", + resource: struct{ Name string }{Name: "test"}, + expectedName: "", + expectedNS: "", + expectedType: "", + expectSHANotEmpty: false, + expectOldSHAEmpty: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler := ResourceCreatedHandler{ + Resource: tt.resource, + Collectors: metrics.NewCollectors(), + } + + config, oldSHA := handler.GetConfig() + + assert.Equal(t, tt.expectedName, config.ResourceName) + assert.Equal(t, tt.expectedNS, config.Namespace) + assert.Equal(t, tt.expectedType, config.Type) + + if tt.expectSHANotEmpty { + assert.NotEmpty(t, config.SHAValue, "SHA should not be empty") + } + + if tt.expectOldSHAEmpty { + assert.Empty(t, oldSHA, "oldSHA should always be empty for create handler") + } + }) + } +} + +func TestResourceCreatedHandler_GetConfig_Annotations(t *testing.T) { + cm := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "annotated-cm", + Namespace: "default", + Annotations: map[string]string{ + "reloader.stakater.com/match": "true", + "reloader.stakater.com/search": "true", + }, + }, + Data: map[string]string{"key": "value"}, + } + + handler := ResourceCreatedHandler{ + Resource: cm, + Collectors: metrics.NewCollectors(), + } + + config, _ := handler.GetConfig() + + assert.NotNil(t, config.ResourceAnnotations) + assert.Equal(t, "true", config.ResourceAnnotations["reloader.stakater.com/match"]) + assert.Equal(t, "true", config.ResourceAnnotations["reloader.stakater.com/search"]) +} + +func TestResourceCreatedHandler_GetConfig_Labels(t *testing.T) { + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "labeled-secret", + Namespace: "default", + Labels: map[string]string{ + "app": "myapp", + "version": "v1", + }, + }, + Data: map[string][]byte{"key": []byte("value")}, + } + + handler := ResourceCreatedHandler{ + Resource: secret, + Collectors: metrics.NewCollectors(), + } + + config, _ := handler.GetConfig() + + assert.NotNil(t, config.Labels) + assert.Equal(t, "myapp", config.Labels["app"]) + assert.Equal(t, "v1", config.Labels["version"]) +} + +func TestResourceCreatedHandler_Handle(t *testing.T) { + tests := []struct { + name string + resource interface{} + expectError bool + }{ + { + name: "Nil resource", + resource: nil, + expectError: false, + }, + { + name: "Valid ConfigMap - no workloads to update", + resource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cm", + Namespace: "default", + }, + Data: map[string]string{"key": "value"}, + }, + expectError: false, + }, + { + name: "Valid Secret - no workloads to update", + resource: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "default", + }, + Data: map[string][]byte{"key": []byte("value")}, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler := ResourceCreatedHandler{ + Resource: tt.resource, + Collectors: metrics.NewCollectors(), + } + + err := handler.Handle() + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestResourceCreatedHandler_SHAConsistency(t *testing.T) { + data := map[string]string{"key": "value"} + + cm1 := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm1", Namespace: "default"}, + Data: data, + } + cm2 := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm2", Namespace: "default"}, + Data: data, + } + + handler1 := ResourceCreatedHandler{Resource: cm1, Collectors: metrics.NewCollectors()} + handler2 := ResourceCreatedHandler{Resource: cm2, Collectors: metrics.NewCollectors()} + + config1, _ := handler1.GetConfig() + config2, _ := handler2.GetConfig() + + assert.Equal(t, config1.SHAValue, config2.SHAValue) +} + +func TestResourceCreatedHandler_SHADifference(t *testing.T) { + cm1 := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"}, + Data: map[string]string{"key": "value1"}, + } + cm2 := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"}, + Data: map[string]string{"key": "value2"}, + } + + handler1 := ResourceCreatedHandler{Resource: cm1, Collectors: metrics.NewCollectors()} + handler2 := ResourceCreatedHandler{Resource: cm2, Collectors: metrics.NewCollectors()} + + config1, _ := handler1.GetConfig() + config2, _ := handler2.GetConfig() + + assert.NotEqual(t, config1.SHAValue, config2.SHAValue) +} diff --git a/internal/pkg/handler/delete_test.go b/internal/pkg/handler/delete_test.go new file mode 100644 index 00000000..812b0d18 --- /dev/null +++ b/internal/pkg/handler/delete_test.go @@ -0,0 +1,353 @@ +package handler + +import ( + "testing" + + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/stakater/Reloader/internal/pkg/callbacks" + "github.com/stakater/Reloader/internal/pkg/constants" + "github.com/stakater/Reloader/internal/pkg/options" + "github.com/stakater/Reloader/pkg/common" +) + +// mockDeploymentForDelete creates a deployment with containers for testing delete strategies +func mockDeploymentForDelete(name, namespace string, containers []v1.Container, volumes []v1.Volume) *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: appsv1.DeploymentSpec{ + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, + Spec: v1.PodSpec{ + Containers: containers, + Volumes: volumes, + }, + }, + }, + } +} + +// Mock funcs for testing +func mockContainersFunc(item runtime.Object) []v1.Container { + deployment, ok := item.(*appsv1.Deployment) + if !ok { + return nil + } + return deployment.Spec.Template.Spec.Containers +} + +func mockInitContainersFunc(item runtime.Object) []v1.Container { + deployment, ok := item.(*appsv1.Deployment) + if !ok { + return nil + } + return deployment.Spec.Template.Spec.InitContainers +} + +func mockVolumesFunc(item runtime.Object) []v1.Volume { + deployment, ok := item.(*appsv1.Deployment) + if !ok { + return nil + } + return deployment.Spec.Template.Spec.Volumes +} + +func mockPodAnnotationsFunc(item runtime.Object) map[string]string { + deployment, ok := item.(*appsv1.Deployment) + if !ok { + return nil + } + return deployment.Spec.Template.Annotations +} + +func mockPatchTemplatesFunc() callbacks.PatchTemplates { + return callbacks.PatchTemplates{ + AnnotationTemplate: `{"spec":{"template":{"metadata":{"annotations":{"%s":"%s"}}}}}`, + EnvVarTemplate: `{"spec":{"template":{"spec":{"containers":[{"name":"%s","env":[{"name":"%s","value":"%s"}]}]}}}}`, + DeleteEnvVarTemplate: `[{"op":"remove","path":"/spec/template/spec/containers/%d/env/%d"}]`, + } +} + +func TestRemoveContainerEnvVars(t *testing.T) { + tests := []struct { + name string + containers []v1.Container + volumes []v1.Volume + config common.Config + autoReload bool + expected constants.Result + envVarRemoved bool + }{ + { + name: "Remove existing env var - configmap envFrom", + containers: []v1.Container{ + { + Name: "app", + EnvFrom: []v1.EnvFromSource{ + { + ConfigMapRef: &v1.ConfigMapEnvSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: "my-configmap", + }, + }, + }, + }, + Env: []v1.EnvVar{ + {Name: "STAKATER_MY_CONFIGMAP_CONFIGMAP", Value: "sha-value"}, + }, + }, + }, + volumes: []v1.Volume{}, + config: common.Config{ + ResourceName: "my-configmap", + Type: constants.ConfigmapEnvVarPostfix, + }, + autoReload: true, + expected: constants.Updated, + envVarRemoved: true, + }, + { + name: "No env var to remove", + containers: []v1.Container{ + { + Name: "app", + EnvFrom: []v1.EnvFromSource{ + { + ConfigMapRef: &v1.ConfigMapEnvSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: "my-configmap", + }, + }, + }, + }, + Env: []v1.EnvVar{}, + }, + }, + volumes: []v1.Volume{}, + config: common.Config{ + ResourceName: "my-configmap", + Type: constants.ConfigmapEnvVarPostfix, + }, + autoReload: true, + expected: constants.NotUpdated, + envVarRemoved: false, + }, + { + name: "Remove existing env var - secret envFrom", + containers: []v1.Container{ + { + Name: "app", + EnvFrom: []v1.EnvFromSource{ + { + SecretRef: &v1.SecretEnvSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: "my-secret", + }, + }, + }, + }, + Env: []v1.EnvVar{ + {Name: "STAKATER_MY_SECRET_SECRET", Value: "sha-value"}, + }, + }, + }, + volumes: []v1.Volume{}, + config: common.Config{ + ResourceName: "my-secret", + Type: constants.SecretEnvVarPostfix, + }, + autoReload: true, + expected: constants.Updated, + envVarRemoved: true, + }, + { + name: "No container found", + containers: []v1.Container{}, + volumes: []v1.Volume{}, + config: common.Config{ + ResourceName: "my-configmap", + Type: constants.ConfigmapEnvVarPostfix, + }, + autoReload: true, + expected: constants.NoContainerFound, + envVarRemoved: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + deployment := mockDeploymentForDelete("test-deploy", "default", tt.containers, tt.volumes) + + funcs := callbacks.RollingUpgradeFuncs{ + ContainersFunc: mockContainersFunc, + InitContainersFunc: mockInitContainersFunc, + VolumesFunc: mockVolumesFunc, + PodAnnotationsFunc: mockPodAnnotationsFunc, + PatchTemplatesFunc: mockPatchTemplatesFunc, + SupportsPatch: true, + } + + result := removeContainerEnvVars(funcs, deployment, tt.config, tt.autoReload) + + assert.Equal(t, tt.expected, result.Result) + + if tt.envVarRemoved { + containers := deployment.Spec.Template.Spec.Containers + for _, c := range containers { + for _, env := range c.Env { + envVarName := getEnvVarName(tt.config.ResourceName, tt.config.Type) + assert.NotEqual(t, envVarName, env.Name, "Env var should have been removed") + } + } + } + }) + } +} + +func TestInvokeDeleteStrategy(t *testing.T) { + originalStrategy := options.ReloadStrategy + defer func() { + options.ReloadStrategy = originalStrategy + }() + + tests := []struct { + name string + reloadStrategy string + containers []v1.Container + volumes []v1.Volume + config common.Config + }{ + { + name: "Annotations strategy", + reloadStrategy: constants.AnnotationsReloadStrategy, + containers: []v1.Container{ + { + Name: "app", + EnvFrom: []v1.EnvFromSource{ + { + ConfigMapRef: &v1.ConfigMapEnvSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: "my-configmap", + }, + }, + }, + }, + }, + }, + volumes: []v1.Volume{}, + config: common.Config{ + ResourceName: "my-configmap", + Type: constants.ConfigmapEnvVarPostfix, + SHAValue: "sha-value", + }, + }, + { + name: "EnvVars strategy", + reloadStrategy: constants.EnvVarsReloadStrategy, + containers: []v1.Container{ + { + Name: "app", + EnvFrom: []v1.EnvFromSource{ + { + ConfigMapRef: &v1.ConfigMapEnvSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: "my-configmap", + }, + }, + }, + }, + Env: []v1.EnvVar{ + {Name: "STAKATER_MY_CONFIGMAP_CONFIGMAP", Value: "sha-value"}, + }, + }, + }, + volumes: []v1.Volume{}, + config: common.Config{ + ResourceName: "my-configmap", + Type: constants.ConfigmapEnvVarPostfix, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + options.ReloadStrategy = tt.reloadStrategy + + deployment := mockDeploymentForDelete("test-deploy", "default", tt.containers, tt.volumes) + + funcs := callbacks.RollingUpgradeFuncs{ + ContainersFunc: mockContainersFunc, + InitContainersFunc: mockInitContainersFunc, + VolumesFunc: mockVolumesFunc, + PodAnnotationsFunc: mockPodAnnotationsFunc, + PatchTemplatesFunc: mockPatchTemplatesFunc, + SupportsPatch: true, + } + + result := invokeDeleteStrategy(funcs, deployment, tt.config, true) + + assert.NotNil(t, result) + }) + } +} + +func TestRemovePodAnnotations(t *testing.T) { + tests := []struct { + name string + containers []v1.Container + volumes []v1.Volume + config common.Config + }{ + { + name: "Remove pod annotations - configmap", + containers: []v1.Container{ + { + Name: "app", + EnvFrom: []v1.EnvFromSource{ + { + ConfigMapRef: &v1.ConfigMapEnvSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: "my-configmap", + }, + }, + }, + }, + }, + }, + volumes: []v1.Volume{}, + config: common.Config{ + ResourceName: "my-configmap", + Type: constants.ConfigmapEnvVarPostfix, + SHAValue: "sha-value", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + deployment := mockDeploymentForDelete("test-deploy", "default", tt.containers, tt.volumes) + + funcs := callbacks.RollingUpgradeFuncs{ + ContainersFunc: mockContainersFunc, + InitContainersFunc: mockInitContainersFunc, + VolumesFunc: mockVolumesFunc, + PodAnnotationsFunc: mockPodAnnotationsFunc, + PatchTemplatesFunc: mockPatchTemplatesFunc, + SupportsPatch: false, + } + + result := removePodAnnotations(funcs, deployment, tt.config, true) + + assert.Equal(t, constants.Updated, result.Result) + }) + } +} diff --git a/internal/pkg/handler/handlers_test.go b/internal/pkg/handler/handlers_test.go new file mode 100644 index 00000000..dedefcc9 --- /dev/null +++ b/internal/pkg/handler/handlers_test.go @@ -0,0 +1,281 @@ +package handler + +import ( + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/stakater/Reloader/internal/pkg/constants" + "github.com/stakater/Reloader/internal/pkg/metrics" +) + +// Helper function to create a test ConfigMap +func createTestConfigMap(data map[string]string) *v1.ConfigMap { + return &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cm", + Namespace: "default", + }, + Data: data, + } +} + +// Helper function to create a test Secret +func createTestSecret(data map[string][]byte) *v1.Secret { + return &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "default", + }, + Data: data, + } +} + +// Helper function to create test metrics collectors +func createTestCollectors() metrics.Collectors { + return metrics.NewCollectors() +} + +// ============================================================ +// ResourceCreatedHandler Tests +// ============================================================ + +func TestResourceCreatedHandler_GetConfig_ConfigMap(t *testing.T) { + cm := createTestConfigMap(map[string]string{"key": "value"}) + handler := ResourceCreatedHandler{ + Resource: cm, + Collectors: createTestCollectors(), + } + + config, oldSHA := handler.GetConfig() + + assert.Equal(t, "test-cm", config.ResourceName) + assert.Equal(t, "default", config.Namespace) + assert.Equal(t, constants.ConfigmapEnvVarPostfix, config.Type) + assert.NotEmpty(t, config.SHAValue) + assert.Empty(t, oldSHA) +} + +func TestResourceCreatedHandler_GetConfig_Secret(t *testing.T) { + secret := createTestSecret(map[string][]byte{"key": []byte("value")}) + handler := ResourceCreatedHandler{ + Resource: secret, + Collectors: createTestCollectors(), + } + + config, oldSHA := handler.GetConfig() + + assert.Equal(t, "test-secret", config.ResourceName) + assert.Equal(t, "default", config.Namespace) + assert.Equal(t, constants.SecretEnvVarPostfix, config.Type) + assert.NotEmpty(t, config.SHAValue) + assert.Empty(t, oldSHA) +} + +func TestResourceCreatedHandler_GetConfig_InvalidResource(t *testing.T) { + handler := ResourceCreatedHandler{ + Resource: "invalid", + Collectors: createTestCollectors(), + } + + config, _ := handler.GetConfig() + + assert.Empty(t, config.ResourceName) +} + +func TestResourceCreatedHandler_Handle_NilResource(t *testing.T) { + handler := ResourceCreatedHandler{ + Resource: nil, + Collectors: createTestCollectors(), + } + + err := handler.Handle() + + assert.NoError(t, err) +} + +// ============================================================ +// ResourceDeleteHandler Tests +// ============================================================ + +func TestResourceDeleteHandler_GetConfig_ConfigMap(t *testing.T) { + cm := createTestConfigMap(map[string]string{"key": "value"}) + handler := ResourceDeleteHandler{ + Resource: cm, + Collectors: createTestCollectors(), + } + + config, oldSHA := handler.GetConfig() + + assert.Equal(t, "test-cm", config.ResourceName) + assert.Equal(t, "default", config.Namespace) + assert.Equal(t, constants.ConfigmapEnvVarPostfix, config.Type) + assert.NotEmpty(t, config.SHAValue) + assert.Empty(t, oldSHA) +} + +func TestResourceDeleteHandler_GetConfig_Secret(t *testing.T) { + secret := createTestSecret(map[string][]byte{"key": []byte("value")}) + handler := ResourceDeleteHandler{ + Resource: secret, + Collectors: createTestCollectors(), + } + + config, oldSHA := handler.GetConfig() + + assert.Equal(t, "test-secret", config.ResourceName) + assert.Equal(t, "default", config.Namespace) + assert.Equal(t, constants.SecretEnvVarPostfix, config.Type) + assert.NotEmpty(t, config.SHAValue) + assert.Empty(t, oldSHA) +} + +func TestResourceDeleteHandler_GetConfig_InvalidResource(t *testing.T) { + handler := ResourceDeleteHandler{ + Resource: "invalid", + Collectors: createTestCollectors(), + } + + config, _ := handler.GetConfig() + + assert.Empty(t, config.ResourceName) +} + +func TestResourceDeleteHandler_Handle_NilResource(t *testing.T) { + handler := ResourceDeleteHandler{ + Resource: nil, + Collectors: createTestCollectors(), + } + + err := handler.Handle() + + assert.NoError(t, err) +} + +// ============================================================ +// ResourceUpdatedHandler Tests +// ============================================================ + +func TestResourceUpdatedHandler_GetConfig_ConfigMap(t *testing.T) { + oldCM := createTestConfigMap(map[string]string{"key": "old-value"}) + newCM := createTestConfigMap(map[string]string{"key": "new-value"}) + + handler := ResourceUpdatedHandler{ + Resource: newCM, + OldResource: oldCM, + Collectors: createTestCollectors(), + } + + config, oldSHA := handler.GetConfig() + + assert.Equal(t, "test-cm", config.ResourceName) + assert.Equal(t, "default", config.Namespace) + assert.Equal(t, constants.ConfigmapEnvVarPostfix, config.Type) + assert.NotEmpty(t, config.SHAValue) + assert.NotEmpty(t, oldSHA) + assert.NotEqual(t, config.SHAValue, oldSHA) +} + +func TestResourceUpdatedHandler_GetConfig_ConfigMap_SameData(t *testing.T) { + oldCM := createTestConfigMap(map[string]string{"key": "same-value"}) + newCM := createTestConfigMap(map[string]string{"key": "same-value"}) + + handler := ResourceUpdatedHandler{ + Resource: newCM, + OldResource: oldCM, + Collectors: createTestCollectors(), + } + + config, oldSHA := handler.GetConfig() + + assert.Equal(t, "test-cm", config.ResourceName) + assert.Equal(t, config.SHAValue, oldSHA) +} + +func TestResourceUpdatedHandler_GetConfig_Secret(t *testing.T) { + oldSecret := createTestSecret(map[string][]byte{"key": []byte("old-value")}) + newSecret := createTestSecret(map[string][]byte{"key": []byte("new-value")}) + + handler := ResourceUpdatedHandler{ + Resource: newSecret, + OldResource: oldSecret, + Collectors: createTestCollectors(), + } + + config, oldSHA := handler.GetConfig() + + assert.Equal(t, "test-secret", config.ResourceName) + assert.Equal(t, "default", config.Namespace) + assert.Equal(t, constants.SecretEnvVarPostfix, config.Type) + assert.NotEmpty(t, config.SHAValue) + assert.NotEmpty(t, oldSHA) + assert.NotEqual(t, config.SHAValue, oldSHA) +} + +func TestResourceUpdatedHandler_GetConfig_Secret_SameData(t *testing.T) { + oldSecret := createTestSecret(map[string][]byte{"key": []byte("same-value")}) + newSecret := createTestSecret(map[string][]byte{"key": []byte("same-value")}) + + handler := ResourceUpdatedHandler{ + Resource: newSecret, + OldResource: oldSecret, + Collectors: createTestCollectors(), + } + + config, oldSHA := handler.GetConfig() + + assert.Equal(t, "test-secret", config.ResourceName) + assert.Equal(t, config.SHAValue, oldSHA) +} + +func TestResourceUpdatedHandler_GetConfig_InvalidResource(t *testing.T) { + handler := ResourceUpdatedHandler{ + Resource: "invalid", + OldResource: "invalid", + Collectors: createTestCollectors(), + } + + config, _ := handler.GetConfig() + + assert.Empty(t, config.ResourceName) +} + +func TestResourceUpdatedHandler_Handle_NilResource(t *testing.T) { + handler := ResourceUpdatedHandler{ + Resource: nil, + OldResource: nil, + Collectors: createTestCollectors(), + } + + err := handler.Handle() + + assert.NoError(t, err) +} + +func TestResourceUpdatedHandler_Handle_NilOldResource(t *testing.T) { + cm := createTestConfigMap(map[string]string{"key": "value"}) + handler := ResourceUpdatedHandler{ + Resource: cm, + OldResource: nil, + Collectors: createTestCollectors(), + } + + err := handler.Handle() + + assert.NoError(t, err) +} + +func TestResourceUpdatedHandler_Handle_NoChange(t *testing.T) { + cm := createTestConfigMap(map[string]string{"key": "same-value"}) + handler := ResourceUpdatedHandler{ + Resource: cm, + OldResource: cm, + Collectors: createTestCollectors(), + } + + err := handler.Handle() + + assert.NoError(t, err) +} diff --git a/internal/pkg/handler/update_test.go b/internal/pkg/handler/update_test.go new file mode 100644 index 00000000..1ae10d41 --- /dev/null +++ b/internal/pkg/handler/update_test.go @@ -0,0 +1,520 @@ +package handler + +import ( + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/stakater/Reloader/internal/pkg/constants" + "github.com/stakater/Reloader/internal/pkg/metrics" +) + +func TestResourceUpdatedHandler_GetConfig(t *testing.T) { + tests := []struct { + name string + oldResource any + newResource any + expectedName string + expectedNS string + expectedType string + expectSHANotEmpty bool + expectSHAChanged bool + }{ + { + name: "ConfigMap data changed", + oldResource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "my-cm", Namespace: "default"}, + Data: map[string]string{"key": "old-value"}, + }, + newResource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "my-cm", Namespace: "default"}, + Data: map[string]string{"key": "new-value"}, + }, + expectedName: "my-cm", + expectedNS: "default", + expectedType: constants.ConfigmapEnvVarPostfix, + expectSHANotEmpty: true, + expectSHAChanged: true, + }, + { + name: "ConfigMap data unchanged", + oldResource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "my-cm", Namespace: "default"}, + Data: map[string]string{"key": "same-value"}, + }, + newResource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "my-cm", Namespace: "default"}, + Data: map[string]string{"key": "same-value"}, + }, + expectedName: "my-cm", + expectedNS: "default", + expectedType: constants.ConfigmapEnvVarPostfix, + expectSHANotEmpty: true, + expectSHAChanged: false, + }, + { + name: "ConfigMap key added", + oldResource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "my-cm", Namespace: "default"}, + Data: map[string]string{"key1": "value1"}, + }, + newResource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "my-cm", Namespace: "default"}, + Data: map[string]string{"key1": "value1", "key2": "value2"}, + }, + expectedName: "my-cm", + expectedNS: "default", + expectedType: constants.ConfigmapEnvVarPostfix, + expectSHANotEmpty: true, + expectSHAChanged: true, + }, + { + name: "ConfigMap key removed", + oldResource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "my-cm", Namespace: "default"}, + Data: map[string]string{"key1": "value1", "key2": "value2"}, + }, + newResource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "my-cm", Namespace: "default"}, + Data: map[string]string{"key1": "value1"}, + }, + expectedName: "my-cm", + expectedNS: "default", + expectedType: constants.ConfigmapEnvVarPostfix, + expectSHANotEmpty: true, + expectSHAChanged: true, + }, + { + name: "ConfigMap only labels changed - SHA unchanged", + oldResource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-cm", + Namespace: "default", + Labels: map[string]string{"version": "v1"}, + }, + Data: map[string]string{"key": "value"}, + }, + newResource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-cm", + Namespace: "default", + Labels: map[string]string{"version": "v2"}, + }, + Data: map[string]string{"key": "value"}, + }, + expectedName: "my-cm", + expectedNS: "default", + expectedType: constants.ConfigmapEnvVarPostfix, + expectSHANotEmpty: true, + expectSHAChanged: false, + }, + { + name: "ConfigMap only annotations changed - SHA unchanged", + oldResource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-cm", + Namespace: "default", + Annotations: map[string]string{"note": "old"}, + }, + Data: map[string]string{"key": "value"}, + }, + newResource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-cm", + Namespace: "default", + Annotations: map[string]string{"note": "new"}, + }, + Data: map[string]string{"key": "value"}, + }, + expectedName: "my-cm", + expectedNS: "default", + expectedType: constants.ConfigmapEnvVarPostfix, + expectSHANotEmpty: true, + expectSHAChanged: false, + }, + { + name: "Secret data changed", + oldResource: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "my-secret", Namespace: "default"}, + Data: map[string][]byte{"password": []byte("old-pass")}, + }, + newResource: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "my-secret", Namespace: "default"}, + Data: map[string][]byte{"password": []byte("new-pass")}, + }, + expectedName: "my-secret", + expectedNS: "default", + expectedType: constants.SecretEnvVarPostfix, + expectSHANotEmpty: true, + expectSHAChanged: true, + }, + { + name: "Secret data unchanged", + oldResource: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "my-secret", Namespace: "default"}, + Data: map[string][]byte{"password": []byte("same-pass")}, + }, + newResource: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "my-secret", Namespace: "default"}, + Data: map[string][]byte{"password": []byte("same-pass")}, + }, + expectedName: "my-secret", + expectedNS: "default", + expectedType: constants.SecretEnvVarPostfix, + expectSHANotEmpty: true, + expectSHAChanged: false, + }, + { + name: "Secret key added", + oldResource: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "my-secret", Namespace: "default"}, + Data: map[string][]byte{"key1": []byte("value1")}, + }, + newResource: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "my-secret", Namespace: "default"}, + Data: map[string][]byte{"key1": []byte("value1"), "key2": []byte("value2")}, + }, + expectedName: "my-secret", + expectedNS: "default", + expectedType: constants.SecretEnvVarPostfix, + expectSHANotEmpty: true, + expectSHAChanged: true, + }, + { + name: "Secret only labels changed - SHA unchanged", + oldResource: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-secret", + Namespace: "default", + Labels: map[string]string{"env": "dev"}, + }, + Data: map[string][]byte{"key": []byte("value")}, + }, + newResource: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-secret", + Namespace: "default", + Labels: map[string]string{"env": "prod"}, + }, + Data: map[string][]byte{"key": []byte("value")}, + }, + expectedName: "my-secret", + expectedNS: "default", + expectedType: constants.SecretEnvVarPostfix, + expectSHANotEmpty: true, + expectSHAChanged: false, + }, + { + name: "Invalid resource type", + oldResource: "invalid", + newResource: "invalid", + expectedName: "", + expectedNS: "", + expectedType: "", + expectSHANotEmpty: false, + expectSHAChanged: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler := ResourceUpdatedHandler{ + Resource: tt.newResource, + OldResource: tt.oldResource, + Collectors: metrics.NewCollectors(), + } + + config, oldSHA := handler.GetConfig() + + assert.Equal(t, tt.expectedName, config.ResourceName) + assert.Equal(t, tt.expectedNS, config.Namespace) + assert.Equal(t, tt.expectedType, config.Type) + + if tt.expectSHANotEmpty { + assert.NotEmpty(t, config.SHAValue, "new SHA should not be empty") + assert.NotEmpty(t, oldSHA, "old SHA should not be empty") + } + + if tt.expectSHAChanged { + assert.NotEqual(t, config.SHAValue, oldSHA, "SHA should have changed") + } else if tt.expectSHANotEmpty { + assert.Equal(t, config.SHAValue, oldSHA, "SHA should not have changed") + } + }) + } +} + +func TestResourceUpdatedHandler_Handle(t *testing.T) { + tests := []struct { + name string + oldResource any + newResource any + expectError bool + }{ + { + name: "Both resources nil", + oldResource: nil, + newResource: nil, + expectError: false, + }, + { + name: "Old resource nil", + oldResource: nil, + newResource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"}, + Data: map[string]string{"key": "value"}, + }, + expectError: false, + }, + { + name: "New resource nil", + oldResource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"}, + Data: map[string]string{"key": "value"}, + }, + newResource: nil, + expectError: false, + }, + { + name: "ConfigMap unchanged - no action", + oldResource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"}, + Data: map[string]string{"key": "same"}, + }, + newResource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"}, + Data: map[string]string{"key": "same"}, + }, + expectError: false, + }, + { + name: "ConfigMap changed - triggers update", + oldResource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"}, + Data: map[string]string{"key": "old"}, + }, + newResource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"}, + Data: map[string]string{"key": "new"}, + }, + expectError: false, + }, + { + name: "Secret unchanged - no action", + oldResource: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "secret", Namespace: "default"}, + Data: map[string][]byte{"key": []byte("same")}, + }, + newResource: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "secret", Namespace: "default"}, + Data: map[string][]byte{"key": []byte("same")}, + }, + expectError: false, + }, + { + name: "Secret changed - triggers update", + oldResource: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "secret", Namespace: "default"}, + Data: map[string][]byte{"key": []byte("old")}, + }, + newResource: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "secret", Namespace: "default"}, + Data: map[string][]byte{"key": []byte("new")}, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler := ResourceUpdatedHandler{ + Resource: tt.newResource, + OldResource: tt.oldResource, + Collectors: metrics.NewCollectors(), + } + + err := handler.Handle() + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestResourceUpdatedHandler_GetConfig_Annotations(t *testing.T) { + oldCM := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cm", + Namespace: "default", + Annotations: map[string]string{ + "old-annotation": "old-value", + }, + }, + Data: map[string]string{"key": "value"}, + } + + newCM := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cm", + Namespace: "default", + Annotations: map[string]string{ + "new-annotation": "new-value", + }, + }, + Data: map[string]string{"key": "value"}, + } + + handler := ResourceUpdatedHandler{ + Resource: newCM, + OldResource: oldCM, + Collectors: metrics.NewCollectors(), + } + + config, _ := handler.GetConfig() + + assert.Equal(t, "new-value", config.ResourceAnnotations["new-annotation"]) + _, hasOld := config.ResourceAnnotations["old-annotation"] + assert.False(t, hasOld) +} + +func TestResourceUpdatedHandler_GetConfig_Labels(t *testing.T) { + oldSecret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "default", + Labels: map[string]string{"version": "v1"}, + }, + Data: map[string][]byte{"key": []byte("value")}, + } + + newSecret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "default", + Labels: map[string]string{"version": "v2"}, + }, + Data: map[string][]byte{"key": []byte("value")}, + } + + handler := ResourceUpdatedHandler{ + Resource: newSecret, + OldResource: oldSecret, + Collectors: metrics.NewCollectors(), + } + + config, _ := handler.GetConfig() + + assert.Equal(t, "v2", config.Labels["version"]) +} + +func TestResourceUpdatedHandler_EmptyToNonEmpty(t *testing.T) { + oldCM := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"}, + Data: map[string]string{}, + } + newCM := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"}, + Data: map[string]string{"key": "value"}, + } + + handler := ResourceUpdatedHandler{ + Resource: newCM, + OldResource: oldCM, + Collectors: metrics.NewCollectors(), + } + + config, oldSHA := handler.GetConfig() + + assert.NotEqual(t, config.SHAValue, oldSHA, "SHA should change when data is added") +} + +func TestResourceUpdatedHandler_NonEmptyToEmpty(t *testing.T) { + oldCM := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"}, + Data: map[string]string{"key": "value"}, + } + newCM := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"}, + Data: map[string]string{}, + } + + handler := ResourceUpdatedHandler{ + Resource: newCM, + OldResource: oldCM, + Collectors: metrics.NewCollectors(), + } + + config, oldSHA := handler.GetConfig() + + assert.NotEqual(t, config.SHAValue, oldSHA, "SHA should change when data is removed") +} + +func TestResourceUpdatedHandler_BinaryDataChange(t *testing.T) { + oldCM := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"}, + BinaryData: map[string][]byte{"binary": []byte("old-binary")}, + } + newCM := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"}, + BinaryData: map[string][]byte{"binary": []byte("new-binary")}, + } + + handler := ResourceUpdatedHandler{ + Resource: newCM, + OldResource: oldCM, + Collectors: metrics.NewCollectors(), + } + + config, oldSHA := handler.GetConfig() + + assert.NotEqual(t, config.SHAValue, oldSHA, "SHA should change when binary data changes") +} + +func TestResourceUpdatedHandler_MixedDataAndBinaryData(t *testing.T) { + oldCM := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"}, + Data: map[string]string{"text": "value"}, + BinaryData: map[string][]byte{"binary": []byte("binary-value")}, + } + newCM := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"}, + Data: map[string]string{"text": "value"}, + BinaryData: map[string][]byte{"binary": []byte("new-binary-value")}, + } + + handler := ResourceUpdatedHandler{ + Resource: newCM, + OldResource: oldCM, + Collectors: metrics.NewCollectors(), + } + + config, oldSHA := handler.GetConfig() + + assert.NotEqual(t, config.SHAValue, oldSHA, "SHA should change when binary data changes") +} + +func TestResourceUpdatedHandler_DifferentNamespaces(t *testing.T) { + oldCM := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "ns1"}, + Data: map[string]string{"key": "value"}, + } + newCM := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "ns2"}, + Data: map[string]string{"key": "value"}, + } + + handler := ResourceUpdatedHandler{ + Resource: newCM, + OldResource: oldCM, + Collectors: metrics.NewCollectors(), + } + + config, _ := handler.GetConfig() + + assert.Equal(t, "ns2", config.Namespace) +}