package handler import ( "errors" "testing" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/util/retry" "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" ) func TestGetRollingUpgradeFuncs(t *testing.T) { tests := []struct { name string getFuncs func() callbacks.RollingUpgradeFuncs resourceType string supportsPatch bool }{ { name: "Deployment", getFuncs: GetDeploymentRollingUpgradeFuncs, resourceType: "Deployment", supportsPatch: true, }, { name: "CronJob", getFuncs: GetCronJobCreateJobFuncs, resourceType: "CronJob", supportsPatch: false, }, { name: "Job", getFuncs: GetJobCreateJobFuncs, resourceType: "Job", supportsPatch: false, }, { name: "DaemonSet", getFuncs: GetDaemonSetRollingUpgradeFuncs, resourceType: "DaemonSet", supportsPatch: true, }, { name: "StatefulSet", getFuncs: GetStatefulSetRollingUpgradeFuncs, resourceType: "StatefulSet", supportsPatch: true, }, { name: "ArgoRollout", getFuncs: GetArgoRolloutRollingUpgradeFuncs, resourceType: "Rollout", supportsPatch: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { funcs := tt.getFuncs() assert.Equal(t, tt.resourceType, funcs.ResourceType) assert.Equal(t, tt.supportsPatch, funcs.SupportsPatch) assert.NotNil(t, funcs.ItemFunc) assert.NotNil(t, funcs.ItemsFunc) assert.NotNil(t, funcs.AnnotationsFunc) assert.NotNil(t, funcs.PodAnnotationsFunc) assert.NotNil(t, funcs.ContainersFunc) assert.NotNil(t, funcs.InitContainersFunc) assert.NotNil(t, funcs.UpdateFunc) assert.NotNil(t, funcs.PatchFunc) assert.NotNil(t, funcs.PatchTemplatesFunc) assert.NotNil(t, funcs.VolumesFunc) }) } } func TestGetVolumeMountName(t *testing.T) { tests := []struct { name string volumes []v1.Volume mountType string volumeName string expected string }{ { name: "ConfigMap volume match", volumes: []v1.Volume{ { Name: "config-volume", VolumeSource: v1.VolumeSource{ ConfigMap: &v1.ConfigMapVolumeSource{ LocalObjectReference: v1.LocalObjectReference{ Name: "my-configmap", }, }, }, }, }, mountType: constants.ConfigmapEnvVarPostfix, volumeName: "my-configmap", expected: "config-volume", }, { name: "Secret volume match", volumes: []v1.Volume{ { Name: "secret-volume", VolumeSource: v1.VolumeSource{ Secret: &v1.SecretVolumeSource{ SecretName: "my-secret", }, }, }, }, mountType: constants.SecretEnvVarPostfix, volumeName: "my-secret", expected: "secret-volume", }, { name: "ConfigMap in projected volume", volumes: []v1.Volume{ { Name: "projected-volume", VolumeSource: v1.VolumeSource{ Projected: &v1.ProjectedVolumeSource{ Sources: []v1.VolumeProjection{ { ConfigMap: &v1.ConfigMapProjection{ LocalObjectReference: v1.LocalObjectReference{ Name: "projected-configmap", }, }, }, }, }, }, }, }, mountType: constants.ConfigmapEnvVarPostfix, volumeName: "projected-configmap", expected: "projected-volume", }, { name: "Secret in projected volume", volumes: []v1.Volume{ { Name: "projected-volume", VolumeSource: v1.VolumeSource{ Projected: &v1.ProjectedVolumeSource{ Sources: []v1.VolumeProjection{ { Secret: &v1.SecretProjection{ LocalObjectReference: v1.LocalObjectReference{ Name: "projected-secret", }, }, }, }, }, }, }, }, mountType: constants.SecretEnvVarPostfix, volumeName: "projected-secret", expected: "projected-volume", }, { name: "No match - wrong configmap name", volumes: []v1.Volume{ { Name: "config-volume", VolumeSource: v1.VolumeSource{ ConfigMap: &v1.ConfigMapVolumeSource{ LocalObjectReference: v1.LocalObjectReference{ Name: "other-configmap", }, }, }, }, }, mountType: constants.ConfigmapEnvVarPostfix, volumeName: "my-configmap", expected: "", }, { name: "No match - wrong type", volumes: []v1.Volume{ { Name: "secret-volume", VolumeSource: v1.VolumeSource{ Secret: &v1.SecretVolumeSource{ SecretName: "my-secret", }, }, }, }, mountType: constants.ConfigmapEnvVarPostfix, volumeName: "my-secret", expected: "", }, { name: "Empty volumes", volumes: []v1.Volume{}, mountType: constants.ConfigmapEnvVarPostfix, volumeName: "any", expected: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := getVolumeMountName(tt.volumes, tt.mountType, tt.volumeName) assert.Equal(t, tt.expected, result) }) } } func TestGetContainerWithVolumeMount(t *testing.T) { tests := []struct { name string containers []v1.Container volumeMountName string expectFound bool expectedName string }{ { name: "Container with matching volume mount", containers: []v1.Container{ { Name: "app", VolumeMounts: []v1.VolumeMount{ {Name: "config-volume", MountPath: "/etc/config"}, }, }, }, volumeMountName: "config-volume", expectFound: true, expectedName: "app", }, { name: "Multiple containers, second has mount", containers: []v1.Container{ { Name: "init", VolumeMounts: []v1.VolumeMount{}, }, { Name: "app", VolumeMounts: []v1.VolumeMount{ {Name: "config-volume", MountPath: "/etc/config"}, }, }, }, volumeMountName: "config-volume", expectFound: true, expectedName: "app", }, { name: "No matching volume mount", containers: []v1.Container{ { Name: "app", VolumeMounts: []v1.VolumeMount{ {Name: "other-volume", MountPath: "/etc/other"}, }, }, }, volumeMountName: "config-volume", expectFound: false, }, { name: "Empty containers", containers: []v1.Container{}, volumeMountName: "config-volume", expectFound: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := getContainerWithVolumeMount(tt.containers, tt.volumeMountName) if tt.expectFound { assert.NotNil(t, result) assert.Equal(t, tt.expectedName, result.Name) } else { assert.Nil(t, result) } }) } } func TestGetContainerWithEnvReference(t *testing.T) { tests := []struct { name string containers []v1.Container resourceName string resourceType string expectFound bool expectedName string }{ { name: "Container with ConfigMapKeyRef", containers: []v1.Container{ { Name: "app", Env: []v1.EnvVar{ { Name: "CONFIG_VALUE", ValueFrom: &v1.EnvVarSource{ ConfigMapKeyRef: &v1.ConfigMapKeySelector{ LocalObjectReference: v1.LocalObjectReference{ Name: "my-configmap", }, Key: "key", }, }, }, }, }, }, resourceName: "my-configmap", resourceType: constants.ConfigmapEnvVarPostfix, expectFound: true, expectedName: "app", }, { name: "Container with SecretKeyRef", containers: []v1.Container{ { Name: "app", Env: []v1.EnvVar{ { Name: "SECRET_VALUE", ValueFrom: &v1.EnvVarSource{ SecretKeyRef: &v1.SecretKeySelector{ LocalObjectReference: v1.LocalObjectReference{ Name: "my-secret", }, Key: "key", }, }, }, }, }, }, resourceName: "my-secret", resourceType: constants.SecretEnvVarPostfix, expectFound: true, expectedName: "app", }, { name: "Container with ConfigMapRef (envFrom)", containers: []v1.Container{ { Name: "app", EnvFrom: []v1.EnvFromSource{ { ConfigMapRef: &v1.ConfigMapEnvSource{ LocalObjectReference: v1.LocalObjectReference{ Name: "my-configmap", }, }, }, }, }, }, resourceName: "my-configmap", resourceType: constants.ConfigmapEnvVarPostfix, expectFound: true, expectedName: "app", }, { name: "Container with SecretRef (envFrom)", containers: []v1.Container{ { Name: "app", EnvFrom: []v1.EnvFromSource{ { SecretRef: &v1.SecretEnvSource{ LocalObjectReference: v1.LocalObjectReference{ Name: "my-secret", }, }, }, }, }, }, resourceName: "my-secret", resourceType: constants.SecretEnvVarPostfix, expectFound: true, expectedName: "app", }, { name: "No match - wrong resource name", containers: []v1.Container{ { Name: "app", EnvFrom: []v1.EnvFromSource{ { ConfigMapRef: &v1.ConfigMapEnvSource{ LocalObjectReference: v1.LocalObjectReference{ Name: "other-configmap", }, }, }, }, }, }, resourceName: "my-configmap", resourceType: constants.ConfigmapEnvVarPostfix, expectFound: false, }, { name: "No match - wrong type (looking for secret but has configmap)", containers: []v1.Container{ { Name: "app", EnvFrom: []v1.EnvFromSource{ { ConfigMapRef: &v1.ConfigMapEnvSource{ LocalObjectReference: v1.LocalObjectReference{ Name: "my-resource", }, }, }, }, }, }, resourceName: "my-resource", resourceType: constants.SecretEnvVarPostfix, expectFound: false, }, { name: "Empty containers", containers: []v1.Container{}, resourceName: "any", resourceType: constants.ConfigmapEnvVarPostfix, expectFound: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := getContainerWithEnvReference(tt.containers, tt.resourceName, tt.resourceType) if tt.expectFound { assert.NotNil(t, result) assert.Equal(t, tt.expectedName, result.Name) } else { assert.Nil(t, result) } }) } } func TestGetEnvVarName(t *testing.T) { tests := []struct { name string resourceName string typeName string expected string }{ { name: "ConfigMap with simple name", resourceName: "my-config", typeName: constants.ConfigmapEnvVarPostfix, expected: "STAKATER_MY_CONFIG_CONFIGMAP", }, { name: "Secret with simple name", resourceName: "my-secret", typeName: constants.SecretEnvVarPostfix, expected: "STAKATER_MY_SECRET_SECRET", }, { name: "Name with hyphens", resourceName: "my-app-config", typeName: constants.ConfigmapEnvVarPostfix, expected: "STAKATER_MY_APP_CONFIG_CONFIGMAP", }, { name: "Name with dots", resourceName: "my.app.config", typeName: constants.ConfigmapEnvVarPostfix, expected: "STAKATER_MY_APP_CONFIG_CONFIGMAP", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := getEnvVarName(tt.resourceName, tt.typeName) assert.Equal(t, tt.expected, result) }) } } func TestUpdateEnvVar(t *testing.T) { tests := []struct { name string container *v1.Container envVar string shaData string expected constants.Result newValue string }{ { name: "Update existing env var with different value", container: &v1.Container{ Name: "app", Env: []v1.EnvVar{ {Name: "STAKATER_CONFIG_CONFIGMAP", Value: "old-sha"}, }, }, envVar: "STAKATER_CONFIG_CONFIGMAP", shaData: "new-sha", expected: constants.Updated, newValue: "new-sha", }, { name: "No update when value is same", container: &v1.Container{ Name: "app", Env: []v1.EnvVar{ {Name: "STAKATER_CONFIG_CONFIGMAP", Value: "same-sha"}, }, }, envVar: "STAKATER_CONFIG_CONFIGMAP", shaData: "same-sha", expected: constants.NotUpdated, newValue: "same-sha", }, { name: "Env var not found", container: &v1.Container{ Name: "app", Env: []v1.EnvVar{ {Name: "OTHER_VAR", Value: "value"}, }, }, envVar: "STAKATER_CONFIG_CONFIGMAP", shaData: "new-sha", expected: constants.NoEnvVarFound, }, { name: "Empty env list", container: &v1.Container{ Name: "app", Env: []v1.EnvVar{}, }, envVar: "STAKATER_CONFIG_CONFIGMAP", shaData: "new-sha", expected: constants.NoEnvVarFound, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := updateEnvVar(tt.container, tt.envVar, tt.shaData) assert.Equal(t, tt.expected, result) if tt.expected == constants.Updated || tt.expected == constants.NotUpdated { for _, env := range tt.container.Env { if env.Name == tt.envVar { assert.Equal(t, tt.newValue, env.Value) break } } } }) } } func TestGetReloaderAnnotationKey(t *testing.T) { result := getReloaderAnnotationKey() expected := "reloader.stakater.com/last-reloaded-from" assert.Equal(t, expected, result) } func TestJsonEscape(t *testing.T) { tests := []struct { name string input string expected string hasError bool }{ { name: "Simple string", input: "hello", expected: "hello", hasError: false, }, { name: "String with quotes", input: `say "hello"`, expected: `say \"hello\"`, hasError: false, }, { name: "String with backslash", input: `path\to\file`, expected: `path\\to\\file`, hasError: false, }, { name: "String with newline", input: "line1\nline2", expected: `line1\nline2`, hasError: false, }, { name: "JSON-like string", input: `{"key":"value"}`, expected: `{\"key\":\"value\"}`, hasError: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := jsonEscape(tt.input) if tt.hasError { assert.Error(t, err) } else { assert.NoError(t, err) assert.Equal(t, tt.expected, result) } }) } } func TestCreateReloadedAnnotations(t *testing.T) { tests := []struct { name string target *common.ReloadSource hasError bool }{ { name: "Nil target", target: nil, hasError: true, }, { name: "Valid target", target: &common.ReloadSource{ Name: "my-configmap", Type: "CONFIGMAP", }, hasError: false, }, } funcs := callbacks.RollingUpgradeFuncs{ SupportsPatch: false, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { annotations, _, err := createReloadedAnnotations(tt.target, funcs) if tt.hasError { assert.Error(t, err) assert.Nil(t, annotations) } else { assert.NoError(t, err) assert.NotNil(t, annotations) _, exists := annotations[getReloaderAnnotationKey()] assert.True(t, exists) } }) } } // Helper function to create a mock deployment for testing func createTestDeployment(containers []v1.Container, initContainers []v1.Container, volumes []v1.Volume) *appsv1.Deployment { return &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "test-deployment", Namespace: "default", }, Spec: appsv1.DeploymentSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: containers, InitContainers: initContainers, Volumes: volumes, }, }, }, } } // mockRollingUpgradeFuncs creates mock callbacks for testing getContainerUsingResource func mockRollingUpgradeFuncs(deployment *appsv1.Deployment) callbacks.RollingUpgradeFuncs { return callbacks.RollingUpgradeFuncs{ VolumesFunc: func(item runtime.Object) []v1.Volume { return deployment.Spec.Template.Spec.Volumes }, ContainersFunc: func(item runtime.Object) []v1.Container { return deployment.Spec.Template.Spec.Containers }, InitContainersFunc: func(item runtime.Object) []v1.Container { return deployment.Spec.Template.Spec.InitContainers }, } } func TestGetContainerUsingResource(t *testing.T) { tests := []struct { name string containers []v1.Container initContainers []v1.Container volumes []v1.Volume config common.Config autoReload bool expectNil bool expectedName string }{ { name: "Volume mount in regular container", containers: []v1.Container{ { Name: "app", VolumeMounts: []v1.VolumeMount{ {Name: "config-volume", MountPath: "/etc/config"}, }, }, }, initContainers: []v1.Container{}, volumes: []v1.Volume{ { Name: "config-volume", VolumeSource: v1.VolumeSource{ ConfigMap: &v1.ConfigMapVolumeSource{ LocalObjectReference: v1.LocalObjectReference{Name: "my-configmap"}, }, }, }, }, config: common.Config{ ResourceName: "my-configmap", Type: constants.ConfigmapEnvVarPostfix, }, autoReload: false, expectNil: false, expectedName: "app", }, { name: "Volume mount in init container returns first regular container", containers: []v1.Container{ {Name: "main-app"}, {Name: "sidecar"}, }, initContainers: []v1.Container{ { Name: "init", VolumeMounts: []v1.VolumeMount{ {Name: "secret-volume", MountPath: "/etc/secrets"}, }, }, }, volumes: []v1.Volume{ { Name: "secret-volume", VolumeSource: v1.VolumeSource{ Secret: &v1.SecretVolumeSource{SecretName: "my-secret"}, }, }, }, config: common.Config{ ResourceName: "my-secret", Type: constants.SecretEnvVarPostfix, }, autoReload: false, expectNil: false, expectedName: "main-app", }, { name: "EnvFrom ConfigMap in regular container", containers: []v1.Container{ { Name: "app", EnvFrom: []v1.EnvFromSource{ { ConfigMapRef: &v1.ConfigMapEnvSource{ LocalObjectReference: v1.LocalObjectReference{Name: "env-configmap"}, }, }, }, }, }, initContainers: []v1.Container{}, volumes: []v1.Volume{}, config: common.Config{ ResourceName: "env-configmap", Type: constants.ConfigmapEnvVarPostfix, }, autoReload: false, expectNil: false, expectedName: "app", }, { name: "EnvFrom Secret in init container returns first regular container", containers: []v1.Container{ {Name: "main-app"}, }, initContainers: []v1.Container{ { Name: "init", EnvFrom: []v1.EnvFromSource{ { SecretRef: &v1.SecretEnvSource{ LocalObjectReference: v1.LocalObjectReference{Name: "init-secret"}, }, }, }, }, }, volumes: []v1.Volume{}, config: common.Config{ ResourceName: "init-secret", Type: constants.SecretEnvVarPostfix, }, autoReload: false, expectNil: false, expectedName: "main-app", }, { name: "autoReload=false with no mount returns first container (explicit annotation)", containers: []v1.Container{ {Name: "first-container"}, {Name: "second-container"}, }, initContainers: []v1.Container{}, volumes: []v1.Volume{}, config: common.Config{ ResourceName: "external-configmap", Type: constants.ConfigmapEnvVarPostfix, }, autoReload: false, expectNil: false, expectedName: "first-container", }, { name: "autoReload=true with no mount returns nil", containers: []v1.Container{ {Name: "app"}, }, initContainers: []v1.Container{}, volumes: []v1.Volume{}, config: common.Config{ ResourceName: "unmounted-configmap", Type: constants.ConfigmapEnvVarPostfix, }, autoReload: true, expectNil: true, }, { name: "Empty containers returns nil", containers: []v1.Container{}, initContainers: []v1.Container{}, volumes: []v1.Volume{}, config: common.Config{ ResourceName: "any-configmap", Type: constants.ConfigmapEnvVarPostfix, }, autoReload: false, expectNil: true, }, { name: "Init container with volume but no regular containers returns nil", containers: []v1.Container{}, initContainers: []v1.Container{ { Name: "init", VolumeMounts: []v1.VolumeMount{ {Name: "config-volume", MountPath: "/etc/config"}, }, }, }, volumes: []v1.Volume{ { Name: "config-volume", VolumeSource: v1.VolumeSource{ ConfigMap: &v1.ConfigMapVolumeSource{ LocalObjectReference: v1.LocalObjectReference{Name: "init-only-cm"}, }, }, }, }, config: common.Config{ ResourceName: "init-only-cm", Type: constants.ConfigmapEnvVarPostfix, }, autoReload: false, expectNil: true, }, { name: "CSI SecretProviderClass volume", containers: []v1.Container{ { Name: "app", VolumeMounts: []v1.VolumeMount{ {Name: "csi-volume", MountPath: "/mnt/secrets"}, }, }, }, initContainers: []v1.Container{}, volumes: []v1.Volume{ { Name: "csi-volume", VolumeSource: v1.VolumeSource{ CSI: &v1.CSIVolumeSource{ Driver: "secrets-store.csi.k8s.io", VolumeAttributes: map[string]string{ "secretProviderClass": "my-spc", }, }, }, }, }, config: common.Config{ ResourceName: "my-spc", Type: constants.SecretProviderClassEnvVarPostfix, }, autoReload: false, expectNil: false, expectedName: "app", }, { name: "Env ValueFrom ConfigMapKeyRef", containers: []v1.Container{ { Name: "app", Env: []v1.EnvVar{ { Name: "CONFIG_VALUE", ValueFrom: &v1.EnvVarSource{ ConfigMapKeyRef: &v1.ConfigMapKeySelector{ LocalObjectReference: v1.LocalObjectReference{Name: "keyref-cm"}, Key: "my-key", }, }, }, }, }, }, initContainers: []v1.Container{}, volumes: []v1.Volume{}, config: common.Config{ ResourceName: "keyref-cm", Type: constants.ConfigmapEnvVarPostfix, }, autoReload: false, expectNil: false, expectedName: "app", }, { name: "Env ValueFrom SecretKeyRef", containers: []v1.Container{ { Name: "app", Env: []v1.EnvVar{ { Name: "SECRET_VALUE", ValueFrom: &v1.EnvVarSource{ SecretKeyRef: &v1.SecretKeySelector{ LocalObjectReference: v1.LocalObjectReference{Name: "keyref-secret"}, Key: "password", }, }, }, }, }, }, initContainers: []v1.Container{}, volumes: []v1.Volume{}, config: common.Config{ ResourceName: "keyref-secret", Type: constants.SecretEnvVarPostfix, }, autoReload: false, expectNil: false, expectedName: "app", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { deployment := createTestDeployment(tt.containers, tt.initContainers, tt.volumes) funcs := mockRollingUpgradeFuncs(deployment) result := getContainerUsingResource(funcs, deployment, tt.config, tt.autoReload) if tt.expectNil { assert.Nil(t, result, "Expected nil container") } else { assert.NotNil(t, result, "Expected non-nil container") assert.Equal(t, tt.expectedName, result.Name) } }) } } func TestRetryOnConflict(t *testing.T) { tests := []struct { name string fnResults []struct { matched bool err error } expectMatched bool expectError bool }{ { name: "Success on first try", fnResults: []struct { matched bool err error }{ {matched: true, err: nil}, }, expectMatched: true, expectError: false, }, { name: "Conflict then success", fnResults: []struct { matched bool err error }{ {matched: false, err: apierrors.NewConflict(schema.GroupResource{Group: "", Resource: "deployments"}, "test", errors.New("conflict"))}, {matched: true, err: nil}, }, expectMatched: true, expectError: false, }, { name: "Non-conflict error returns immediately", fnResults: []struct { matched bool err error }{ {matched: false, err: errors.New("some other error")}, }, expectMatched: false, expectError: true, }, { name: "Multiple conflicts then success", fnResults: []struct { matched bool err error }{ {matched: false, err: apierrors.NewConflict(schema.GroupResource{}, "test", errors.New("conflict 1"))}, {matched: false, err: apierrors.NewConflict(schema.GroupResource{}, "test", errors.New("conflict 2"))}, {matched: true, err: nil}, }, expectMatched: true, expectError: false, }, { name: "Not matched but no error", fnResults: []struct { matched bool err error }{ {matched: false, err: nil}, }, expectMatched: false, expectError: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { callCount := 0 fn := func(fetchResource bool) (bool, error) { if callCount >= len(tt.fnResults) { return true, nil } result := tt.fnResults[callCount] callCount++ return result.matched, result.err } matched, err := retryOnConflict(retry.DefaultRetry, fn) assert.Equal(t, tt.expectMatched, matched) if tt.expectError { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } func TestGetVolumeMountNameCSI(t *testing.T) { tests := []struct { name string volumes []v1.Volume mountType string volumeName string expected string }{ { name: "CSI SecretProviderClass volume match", volumes: []v1.Volume{ { Name: "csi-secrets", VolumeSource: v1.VolumeSource{ CSI: &v1.CSIVolumeSource{ Driver: "secrets-store.csi.k8s.io", VolumeAttributes: map[string]string{ "secretProviderClass": "my-vault-spc", }, }, }, }, }, mountType: constants.SecretProviderClassEnvVarPostfix, volumeName: "my-vault-spc", expected: "csi-secrets", }, { name: "CSI volume with different SPC name - no match", volumes: []v1.Volume{ { Name: "csi-secrets", VolumeSource: v1.VolumeSource{ CSI: &v1.CSIVolumeSource{ Driver: "secrets-store.csi.k8s.io", VolumeAttributes: map[string]string{ "secretProviderClass": "other-spc", }, }, }, }, }, mountType: constants.SecretProviderClassEnvVarPostfix, volumeName: "my-vault-spc", expected: "", }, { name: "CSI volume without secretProviderClass attribute", volumes: []v1.Volume{ { Name: "csi-volume", VolumeSource: v1.VolumeSource{ CSI: &v1.CSIVolumeSource{ Driver: "other-csi-driver", VolumeAttributes: map[string]string{}, }, }, }, }, mountType: constants.SecretProviderClassEnvVarPostfix, volumeName: "any-spc", expected: "", }, { name: "CSI volume with nil VolumeAttributes", volumes: []v1.Volume{ { Name: "csi-volume", VolumeSource: v1.VolumeSource{ CSI: &v1.CSIVolumeSource{ Driver: "secrets-store.csi.k8s.io", }, }, }, }, mountType: constants.SecretProviderClassEnvVarPostfix, volumeName: "any-spc", expected: "", }, { name: "Multiple volumes with CSI match", volumes: []v1.Volume{ { Name: "config-volume", VolumeSource: v1.VolumeSource{ ConfigMap: &v1.ConfigMapVolumeSource{ LocalObjectReference: v1.LocalObjectReference{Name: "my-cm"}, }, }, }, { Name: "csi-secrets", VolumeSource: v1.VolumeSource{ CSI: &v1.CSIVolumeSource{ Driver: "secrets-store.csi.k8s.io", VolumeAttributes: map[string]string{ "secretProviderClass": "target-spc", }, }, }, }, }, mountType: constants.SecretProviderClassEnvVarPostfix, volumeName: "target-spc", expected: "csi-secrets", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := getVolumeMountName(tt.volumes, tt.mountType, tt.volumeName) assert.Equal(t, tt.expected, result) }) } } func TestSecretProviderClassAnnotationReloaded(t *testing.T) { tests := []struct { name string oldAnnotations map[string]string newConfig common.Config expected bool }{ { name: "Annotation contains matching SPC name and SHA", oldAnnotations: map[string]string{ "reloader.stakater.com/last-reloaded-from": `{"name":"my-spc","sha":"abc123"}`, }, newConfig: common.Config{ ResourceName: "my-spc", SHAValue: "abc123", }, expected: true, }, { name: "Annotation contains SPC name but different SHA", oldAnnotations: map[string]string{ "reloader.stakater.com/last-reloaded-from": `{"name":"my-spc","sha":"old-sha"}`, }, newConfig: common.Config{ ResourceName: "my-spc", SHAValue: "new-sha", }, expected: false, }, { name: "Annotation contains different SPC name", oldAnnotations: map[string]string{ "reloader.stakater.com/last-reloaded-from": `{"name":"other-spc","sha":"abc123"}`, }, newConfig: common.Config{ ResourceName: "my-spc", SHAValue: "abc123", }, expected: false, }, { name: "Empty annotations", oldAnnotations: map[string]string{}, newConfig: common.Config{ ResourceName: "my-spc", SHAValue: "abc123", }, expected: false, }, { name: "Nil annotations", oldAnnotations: nil, newConfig: common.Config{ ResourceName: "my-spc", SHAValue: "abc123", }, expected: false, }, { name: "Annotation key missing", oldAnnotations: map[string]string{ "other-annotation": "some-value", }, newConfig: common.Config{ ResourceName: "my-spc", SHAValue: "abc123", }, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := secretProviderClassAnnotationReloaded(tt.oldAnnotations, tt.newConfig) assert.Equal(t, tt.expected, result) }) } } func TestInvokeReloadStrategy(t *testing.T) { originalStrategy := options.ReloadStrategy defer func() { options.ReloadStrategy = originalStrategy }() deployment := createTestDeployment( []v1.Container{ { Name: "app", EnvFrom: []v1.EnvFromSource{ { ConfigMapRef: &v1.ConfigMapEnvSource{ LocalObjectReference: v1.LocalObjectReference{Name: "my-configmap"}, }, }, }, }, }, []v1.Container{}, []v1.Volume{}, ) deployment.Spec.Template.Annotations = map[string]string{} funcs := callbacks.RollingUpgradeFuncs{ VolumesFunc: func(item runtime.Object) []v1.Volume { return deployment.Spec.Template.Spec.Volumes }, ContainersFunc: func(item runtime.Object) []v1.Container { return deployment.Spec.Template.Spec.Containers }, InitContainersFunc: func(item runtime.Object) []v1.Container { return deployment.Spec.Template.Spec.InitContainers }, PodAnnotationsFunc: func(item runtime.Object) map[string]string { return deployment.Spec.Template.Annotations }, SupportsPatch: false, } config := common.Config{ ResourceName: "my-configmap", Type: constants.ConfigmapEnvVarPostfix, SHAValue: "sha256:abc123", Namespace: "default", } tests := []struct { name string reloadStrategy string autoReload bool expectResult constants.Result }{ { name: "Annotations strategy", reloadStrategy: constants.AnnotationsReloadStrategy, autoReload: false, expectResult: constants.Updated, }, { name: "Env vars strategy with container found", reloadStrategy: constants.EnvVarsReloadStrategy, autoReload: false, expectResult: constants.Updated, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { options.ReloadStrategy = tt.reloadStrategy deployment.Spec.Template.Annotations = map[string]string{} result := invokeReloadStrategy(funcs, deployment, config, tt.autoReload) assert.Equal(t, tt.expectResult, result.Result) }) } }