feat: A lot of refactoring and CSI test cases

This commit is contained in:
TheiLLeniumStudios
2026-01-10 13:42:10 +01:00
parent 46e7d74bd1
commit f0e6d3af58
79 changed files with 6434 additions and 3987 deletions

View File

@@ -4,11 +4,12 @@ import (
"time"
"github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/record"
"github.com/stakater/Reloader/internal/pkg/metrics"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/pkg/common"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/record"
)
// ResourceCreatedHandler contains new objects
@@ -59,10 +60,10 @@ func (r ResourceCreatedHandler) Handle() error {
func (r ResourceCreatedHandler) GetConfig() (common.Config, string) {
var oldSHAData string
var config common.Config
if _, ok := r.Resource.(*v1.ConfigMap); ok {
config = common.GetConfigmapConfig(r.Resource.(*v1.ConfigMap))
} else if _, ok := r.Resource.(*v1.Secret); ok {
config = common.GetSecretConfig(r.Resource.(*v1.Secret))
if cm, ok := r.Resource.(*v1.ConfigMap); ok {
config = common.GetConfigmapConfig(cm)
} else if secret, ok := r.Resource.(*v1.Secret); ok {
config = common.GetSecretConfig(secret)
} else {
logrus.Warnf("Invalid resource: Resource should be 'Secret' or 'Configmap' but found, %v", r.Resource)
}

View File

@@ -3,20 +3,21 @@ package handler
import (
"testing"
"github.com/stakater/Reloader/internal/pkg/constants"
"github.com/stakater/Reloader/internal/pkg/metrics"
"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
name string
resource interface{}
expectedName string
expectedNS string
expectedType string
expectSHANotEmpty bool
expectOldSHAEmpty bool
}{

View File

@@ -6,6 +6,7 @@ import (
"time"
"github.com/sirupsen/logrus"
"github.com/stakater/Reloader/internal/pkg/callbacks"
"github.com/stakater/Reloader/internal/pkg/constants"
"github.com/stakater/Reloader/internal/pkg/metrics"
@@ -67,10 +68,10 @@ func (r ResourceDeleteHandler) Handle() error {
func (r ResourceDeleteHandler) GetConfig() (common.Config, string) {
var oldSHAData string
var config common.Config
if _, ok := r.Resource.(*v1.ConfigMap); ok {
config = common.GetConfigmapConfig(r.Resource.(*v1.ConfigMap))
} else if _, ok := r.Resource.(*v1.Secret); ok {
config = common.GetSecretConfig(r.Resource.(*v1.Secret))
if cm, ok := r.Resource.(*v1.ConfigMap); ok {
config = common.GetConfigmapConfig(cm)
} else if secret, ok := r.Resource.(*v1.Secret); ok {
config = common.GetSecretConfig(secret)
} else {
logrus.Warnf("Invalid resource: Resource should be 'Secret' or 'Configmap' but found, %v", r.Resource)
}
@@ -98,7 +99,7 @@ func removeContainerEnvVars(upgradeFuncs callbacks.RollingUpgradeFuncs, item run
return InvokeStrategyResult{constants.NoContainerFound, nil}
}
//remove if env var exists
// remove if env var exists
if len(container.Env) > 0 {
index := slices.IndexFunc(container.Env, func(envVariable v1.EnvVar) bool {
return envVariable.Name == envVar

View File

@@ -3,15 +3,16 @@ package handler
import (
"testing"
"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"
"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

View File

@@ -3,30 +3,31 @@ package handler
import (
"testing"
"github.com/stakater/Reloader/internal/pkg/constants"
"github.com/stakater/Reloader/internal/pkg/metrics"
"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(name, namespace string, data map[string]string) *v1.ConfigMap {
func createTestConfigMap(data map[string]string) *v1.ConfigMap {
return &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Name: "test-cm",
Namespace: "default",
},
Data: data,
}
}
// Helper function to create a test Secret
func createTestSecret(name, namespace string, data map[string][]byte) *v1.Secret {
func createTestSecret(data map[string][]byte) *v1.Secret {
return &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Name: "test-secret",
Namespace: "default",
},
Data: data,
}
@@ -42,7 +43,7 @@ func createTestCollectors() metrics.Collectors {
// ============================================================
func TestResourceCreatedHandler_GetConfig_ConfigMap(t *testing.T) {
cm := createTestConfigMap("test-cm", "default", map[string]string{"key": "value"})
cm := createTestConfigMap(map[string]string{"key": "value"})
handler := ResourceCreatedHandler{
Resource: cm,
Collectors: createTestCollectors(),
@@ -58,7 +59,7 @@ func TestResourceCreatedHandler_GetConfig_ConfigMap(t *testing.T) {
}
func TestResourceCreatedHandler_GetConfig_Secret(t *testing.T) {
secret := createTestSecret("test-secret", "default", map[string][]byte{"key": []byte("value")})
secret := createTestSecret(map[string][]byte{"key": []byte("value")})
handler := ResourceCreatedHandler{
Resource: secret,
Collectors: createTestCollectors(),
@@ -103,7 +104,7 @@ func TestResourceCreatedHandler_Handle_NilResource(t *testing.T) {
// ============================================================
func TestResourceDeleteHandler_GetConfig_ConfigMap(t *testing.T) {
cm := createTestConfigMap("test-cm", "default", map[string]string{"key": "value"})
cm := createTestConfigMap(map[string]string{"key": "value"})
handler := ResourceDeleteHandler{
Resource: cm,
Collectors: createTestCollectors(),
@@ -119,7 +120,7 @@ func TestResourceDeleteHandler_GetConfig_ConfigMap(t *testing.T) {
}
func TestResourceDeleteHandler_GetConfig_Secret(t *testing.T) {
secret := createTestSecret("test-secret", "default", map[string][]byte{"key": []byte("value")})
secret := createTestSecret(map[string][]byte{"key": []byte("value")})
handler := ResourceDeleteHandler{
Resource: secret,
Collectors: createTestCollectors(),
@@ -161,8 +162,8 @@ func TestResourceDeleteHandler_Handle_NilResource(t *testing.T) {
// ============================================================
func TestResourceUpdatedHandler_GetConfig_ConfigMap(t *testing.T) {
oldCM := createTestConfigMap("test-cm", "default", map[string]string{"key": "old-value"})
newCM := createTestConfigMap("test-cm", "default", map[string]string{"key": "new-value"})
oldCM := createTestConfigMap(map[string]string{"key": "old-value"})
newCM := createTestConfigMap(map[string]string{"key": "new-value"})
handler := ResourceUpdatedHandler{
Resource: newCM,
@@ -182,8 +183,8 @@ func TestResourceUpdatedHandler_GetConfig_ConfigMap(t *testing.T) {
}
func TestResourceUpdatedHandler_GetConfig_ConfigMap_SameData(t *testing.T) {
oldCM := createTestConfigMap("test-cm", "default", map[string]string{"key": "same-value"})
newCM := createTestConfigMap("test-cm", "default", map[string]string{"key": "same-value"})
oldCM := createTestConfigMap(map[string]string{"key": "same-value"})
newCM := createTestConfigMap(map[string]string{"key": "same-value"})
handler := ResourceUpdatedHandler{
Resource: newCM,
@@ -199,8 +200,8 @@ func TestResourceUpdatedHandler_GetConfig_ConfigMap_SameData(t *testing.T) {
}
func TestResourceUpdatedHandler_GetConfig_Secret(t *testing.T) {
oldSecret := createTestSecret("test-secret", "default", map[string][]byte{"key": []byte("old-value")})
newSecret := createTestSecret("test-secret", "default", map[string][]byte{"key": []byte("new-value")})
oldSecret := createTestSecret(map[string][]byte{"key": []byte("old-value")})
newSecret := createTestSecret(map[string][]byte{"key": []byte("new-value")})
handler := ResourceUpdatedHandler{
Resource: newSecret,
@@ -219,8 +220,8 @@ func TestResourceUpdatedHandler_GetConfig_Secret(t *testing.T) {
}
func TestResourceUpdatedHandler_GetConfig_Secret_SameData(t *testing.T) {
oldSecret := createTestSecret("test-secret", "default", map[string][]byte{"key": []byte("same-value")})
newSecret := createTestSecret("test-secret", "default", map[string][]byte{"key": []byte("same-value")})
oldSecret := createTestSecret(map[string][]byte{"key": []byte("same-value")})
newSecret := createTestSecret(map[string][]byte{"key": []byte("same-value")})
handler := ResourceUpdatedHandler{
Resource: newSecret,
@@ -260,7 +261,7 @@ func TestResourceUpdatedHandler_Handle_NilResource(t *testing.T) {
}
func TestResourceUpdatedHandler_Handle_NilOldResource(t *testing.T) {
cm := createTestConfigMap("test-cm", "default", map[string]string{"key": "value"})
cm := createTestConfigMap(map[string]string{"key": "value"})
handler := ResourceUpdatedHandler{
Resource: cm,
OldResource: nil,
@@ -275,7 +276,7 @@ func TestResourceUpdatedHandler_Handle_NilOldResource(t *testing.T) {
func TestResourceUpdatedHandler_Handle_NoChange(t *testing.T) {
// When SHA values are the same, Handle should return nil without doing anything
cm := createTestConfigMap("test-cm", "default", map[string]string{"key": "same-value"})
cm := createTestConfigMap(map[string]string{"key": "same-value"})
handler := ResourceUpdatedHandler{
Resource: cm,
OldResource: cm, // Same resource = same SHA

View File

@@ -7,11 +7,12 @@ import (
"time"
"github.com/sirupsen/logrus"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/pkg/kube"
app "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
patchtypes "k8s.io/apimachinery/pkg/types"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/pkg/kube"
)
// Keeps track of currently active timers

View File

@@ -6,14 +6,15 @@ import (
"testing"
"time"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/pkg/kube"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
testclient "k8s.io/client-go/kubernetes/fake"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/pkg/kube"
)
func TestIsPaused(t *testing.T) {
@@ -377,7 +378,7 @@ func FindDeploymentByName(deployments []runtime.Object, deploymentName string) (
for _, deployment := range deployments {
accessor, err := meta.Accessor(deployment)
if err != nil {
return nil, fmt.Errorf("error getting accessor for item: %v", err)
return nil, fmt.Errorf("error getting accessor for item: %w", err)
}
if accessor.GetName() == deploymentName {
deploymentObj, ok := deployment.(*appsv1.Deployment)

View File

@@ -4,13 +4,14 @@ import (
"time"
"github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/record"
csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1"
"github.com/stakater/Reloader/internal/pkg/metrics"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/internal/pkg/util"
"github.com/stakater/Reloader/pkg/common"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/record"
csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1"
)
// ResourceUpdatedHandler contains updated objects

View File

@@ -3,11 +3,12 @@ package handler
import (
"testing"
"github.com/stakater/Reloader/internal/pkg/constants"
"github.com/stakater/Reloader/internal/pkg/metrics"
"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) {

View File

@@ -14,14 +14,6 @@ import (
"github.com/parnurzeal/gorequest"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
alert "github.com/stakater/Reloader/internal/pkg/alerts"
"github.com/stakater/Reloader/internal/pkg/callbacks"
"github.com/stakater/Reloader/internal/pkg/constants"
"github.com/stakater/Reloader/internal/pkg/metrics"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/internal/pkg/util"
"github.com/stakater/Reloader/pkg/common"
"github.com/stakater/Reloader/pkg/kube"
app "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -32,6 +24,15 @@ import (
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/retry"
alert "github.com/stakater/Reloader/internal/pkg/alerts"
"github.com/stakater/Reloader/internal/pkg/callbacks"
"github.com/stakater/Reloader/internal/pkg/constants"
"github.com/stakater/Reloader/internal/pkg/metrics"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/internal/pkg/util"
"github.com/stakater/Reloader/pkg/common"
"github.com/stakater/Reloader/pkg/kube"
)
// GetDeploymentRollingUpgradeFuncs returns all callback funcs for a deployment
@@ -617,7 +618,7 @@ func updateContainerEnvVars(upgradeFuncs callbacks.RollingUpgradeFuncs, item run
return InvokeStrategyResult{constants.NotUpdated, nil}
}
//update if env var exists
// update if env var exists
updateResult := updateEnvVar(container, envVar, config.SHAValue)
// if no existing env var exists lets create one
@@ -680,10 +681,10 @@ func populateAnnotationsFromSecretProviderClass(clients kube.Clients, config *co
}
func jsonEscape(toEscape string) (string, error) {
bytes, err := json.Marshal(toEscape)
data, err := json.Marshal(toEscape)
if err != nil {
return "", err
}
escaped := string(bytes)
escaped := string(data)
return escaped[1 : len(escaped)-1], nil
}

View File

@@ -1,20 +1,29 @@
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"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
)
func TestGetRollingUpgradeFuncs(t *testing.T) {
tests := []struct {
name string
getFuncs func() callbacks.RollingUpgradeFuncs
resourceType string
name string
getFuncs func() callbacks.RollingUpgradeFuncs
resourceType string
supportsPatch bool
}{
{
@@ -495,12 +504,12 @@ func TestGetEnvVarName(t *testing.T) {
func TestUpdateEnvVar(t *testing.T) {
tests := []struct {
name string
container *v1.Container
envVar string
shaData string
expected constants.Result
newValue string // expected value after update
name string
container *v1.Container
envVar string
shaData string
expected constants.Result
newValue string // expected value after update
}{
{
name: "Update existing env var with different value",
@@ -670,3 +679,704 @@ func TestCreateReloadedAnnotations(t *testing.T) {
})
}
}
// 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", // Returns first container when init container has the mount
},
{
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, // Explicit annotation should use first container fallback
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, // Auto mode should NOT use first container fallback
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, // No regular containers to return
},
{
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) {
// Should not happen in tests, but return success to prevent infinite loop
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) {
// Test CSI SecretProviderClass volume specifically
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) {
// Save original value and restore after test
originalStrategy := options.ReloadStrategy
defer func() { options.ReloadStrategy = originalStrategy }()
// Create a minimal deployment for testing
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, // Creates env var when not found
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
options.ReloadStrategy = tt.reloadStrategy
// Reset annotations for each test
deployment.Spec.Template.Annotations = map[string]string{}
result := invokeReloadStrategy(funcs, deployment, config, tt.autoReload)
assert.Equal(t, tt.expectResult, result.Result)
})
}
}