mirror of
https://github.com/stakater/Reloader.git
synced 2026-08-27 14:37:17 +00:00
feat: e2e tests and a lot of refactoring for existing tests
This commit is contained in:
@@ -1,844 +1,159 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/go-logr/logr/testr"
|
||||
"github.com/stakater/Reloader/internal/pkg/alerting"
|
||||
"github.com/stakater/Reloader/internal/pkg/config"
|
||||
"github.com/stakater/Reloader/internal/pkg/controller"
|
||||
"github.com/stakater/Reloader/internal/pkg/events"
|
||||
"github.com/stakater/Reloader/internal/pkg/metrics"
|
||||
"github.com/stakater/Reloader/internal/pkg/reload"
|
||||
"github.com/stakater/Reloader/internal/pkg/webhook"
|
||||
"github.com/stakater/Reloader/internal/pkg/workload"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
func newTestScheme() *runtime.Scheme {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
_ = appsv1.AddToScheme(scheme)
|
||||
_ = batchv1.AddToScheme(scheme)
|
||||
return scheme
|
||||
}
|
||||
|
||||
func newTestConfigMapReconciler(t *testing.T, cfg *config.Config, objects ...runtime.Object) *controller.ConfigMapReconciler {
|
||||
scheme := newTestScheme()
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithRuntimeObjects(objects...).
|
||||
Build()
|
||||
|
||||
collectors := metrics.NewCollectors()
|
||||
|
||||
return &controller.ConfigMapReconciler{
|
||||
Client: fakeClient,
|
||||
Log: testr.New(t),
|
||||
Config: cfg,
|
||||
ReloadService: reload.NewService(cfg),
|
||||
Registry: workload.NewRegistry(cfg.ArgoRolloutsEnabled),
|
||||
Collectors: &collectors,
|
||||
EventRecorder: events.NewRecorder(nil),
|
||||
WebhookClient: webhook.NewClient("", testr.New(t)),
|
||||
Alerter: &alerting.NoOpAlerter{},
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_NotFound(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
reconciler := newTestConfigMapReconciler(t, cfg)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "nonexistent-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue for NotFound")
|
||||
}
|
||||
reconciler := newConfigMapReconciler(t, cfg)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("nonexistent-cm", "default"))
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_NotFound_ReloadOnDelete(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.ReloadOnDelete = true
|
||||
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-deployment",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "deleted-cm",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, deployment)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "deleted-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
deployment := testDeployment("test-deployment", "default", map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "deleted-cm",
|
||||
})
|
||||
reconciler := newConfigMapReconciler(t, cfg, deployment)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("deleted-cm", "default"))
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_IgnoredNamespace(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.IgnoredNamespaces = []string{"kube-system"}
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-cm",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue for ignored namespace")
|
||||
}
|
||||
cm := testConfigMap("test-cm", "kube-system")
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("test-cm", "kube-system"))
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_NoMatchingWorkloads(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-deployment",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, deployment)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
cm := testConfigMap("test-cm", "default")
|
||||
deployment := testDeployment("test-deployment", "default", nil)
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, deployment)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("test-cm", "default"))
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_MatchingDeployment_AutoAnnotation(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.AutoReloadAll = true
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-deployment",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
EnvFrom: []corev1.EnvFromSource{{
|
||||
ConfigMapRef: &corev1.ConfigMapEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: "test-cm",
|
||||
},
|
||||
},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, deployment)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
cm := testConfigMap("test-cm", "default")
|
||||
deployment := testDeploymentWithEnvFrom("test-deployment", "default", "test-cm", "")
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, deployment)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("test-cm", "default"))
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_MatchingDeployment_ExplicitAnnotation(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-deployment",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, deployment)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
cm := testConfigMap("test-cm", "default")
|
||||
deployment := testDeployment("test-deployment", "default", map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
})
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, deployment)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("test-cm", "default"))
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_WorkloadInDifferentNamespace(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "namespace-a",
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-deployment",
|
||||
Namespace: "namespace-b",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, deployment)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-cm",
|
||||
Namespace: "namespace-a",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
cm := testConfigMap("test-cm", "namespace-a")
|
||||
deployment := testDeployment("test-deployment", "namespace-b", map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
})
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, deployment)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("test-cm", "namespace-a"))
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_IgnoredWorkloadType(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.IgnoredWorkloads = []string{"deployment"}
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-deployment",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, deployment)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
cm := testConfigMap("test-cm", "default")
|
||||
deployment := testDeployment("test-deployment", "default", map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
})
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, deployment)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("test-cm", "default"))
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_DaemonSet(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
|
||||
daemonset := &appsv1.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-daemonset",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DaemonSetSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, daemonset)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
cm := testConfigMap("test-cm", "default")
|
||||
daemonset := testDaemonSet("test-daemonset", "default", map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
})
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, daemonset)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("test-cm", "default"))
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_StatefulSet(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
|
||||
statefulset := &appsv1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-statefulset",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, statefulset)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
cm := testConfigMap("test-cm", "default")
|
||||
statefulset := testStatefulSet("test-statefulset", "default", map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "test-cm",
|
||||
})
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, statefulset)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("test-cm", "default"))
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_MultipleWorkloads(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "shared-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
cm := testConfigMap("shared-cm", "default")
|
||||
deployment1 := testDeployment("deployment-1", "default", map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "shared-cm",
|
||||
})
|
||||
deployment2 := testDeployment("deployment-2", "default", map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "shared-cm",
|
||||
})
|
||||
daemonset := testDaemonSet("daemonset-1", "default", map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "shared-cm",
|
||||
})
|
||||
|
||||
deployment1 := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "deployment-1",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "shared-cm",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test1"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test1"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
deployment2 := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "deployment-2",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "shared-cm",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test2"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test2"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
daemonset := &appsv1.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "daemonset-1",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.ConfigmapReload: "shared-cm",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DaemonSetSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "daemon"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "daemon"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, deployment1, deployment2, daemonset)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "shared-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, deployment1, deployment2, daemonset)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("shared-cm", "default"))
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_VolumeMount(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.AutoReloadAll = true
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "volume-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{"config.yaml": "key: value"},
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-deployment",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
VolumeMounts: []corev1.VolumeMount{{
|
||||
Name: "config",
|
||||
MountPath: "/etc/config",
|
||||
}},
|
||||
}},
|
||||
Volumes: []corev1.Volume{{
|
||||
Name: "config",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
ConfigMap: &corev1.ConfigMapVolumeSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: "volume-cm",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, deployment)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "volume-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
cm := testConfigMap("volume-cm", "default")
|
||||
deployment := testDeploymentWithVolume("test-deployment", "default", "volume-cm", "")
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, deployment)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("volume-cm", "default"))
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_ProjectedVolume(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.AutoReloadAll = true
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "projected-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{"config.yaml": "key: value"},
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-deployment",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
VolumeMounts: []corev1.VolumeMount{{
|
||||
Name: "config",
|
||||
MountPath: "/etc/config",
|
||||
}},
|
||||
}},
|
||||
Volumes: []corev1.Volume{{
|
||||
Name: "config",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Projected: &corev1.ProjectedVolumeSource{
|
||||
Sources: []corev1.VolumeProjection{{
|
||||
ConfigMap: &corev1.ConfigMapProjection{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: "projected-cm",
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, deployment)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "projected-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
cm := testConfigMap("projected-cm", "default")
|
||||
deployment := testDeploymentWithProjectedVolume("test-deployment", "default", "projected-cm", "")
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, deployment)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("projected-cm", "default"))
|
||||
}
|
||||
|
||||
func TestConfigMapReconciler_SearchAnnotation(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.Match: "true",
|
||||
},
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-deployment",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
cfg.Annotations.Search: "true",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "test"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reconciler := newTestConfigMapReconciler(t, cfg, cm, deployment)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
cm := testConfigMapWithAnnotations("test-cm", "default", map[string]string{
|
||||
cfg.Annotations.Match: "true",
|
||||
})
|
||||
deployment := testDeployment("test-deployment", "default", map[string]string{
|
||||
cfg.Annotations.Search: "true",
|
||||
})
|
||||
reconciler := newConfigMapReconciler(t, cfg, cm, deployment)
|
||||
assertReconcileSuccess(t, reconciler, reconcileRequest("test-cm", "default"))
|
||||
}
|
||||
|
||||
@@ -11,70 +11,72 @@ import (
|
||||
|
||||
func TestCreateEventPredicate_CreateEvent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
reloadOnCreate bool
|
||||
syncAfterRestart bool
|
||||
initialized bool
|
||||
expectedResult bool
|
||||
name string
|
||||
reloadOnCreate bool
|
||||
syncAfterRestart bool
|
||||
initialized bool
|
||||
expectedResult bool
|
||||
}{
|
||||
{
|
||||
name: "reload on create enabled, initialized",
|
||||
reloadOnCreate: true,
|
||||
syncAfterRestart: false,
|
||||
initialized: true,
|
||||
expectedResult: true,
|
||||
name: "reload on create enabled, initialized",
|
||||
reloadOnCreate: true,
|
||||
syncAfterRestart: false,
|
||||
initialized: true,
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "reload on create disabled, initialized",
|
||||
reloadOnCreate: false,
|
||||
syncAfterRestart: false,
|
||||
initialized: true,
|
||||
expectedResult: false,
|
||||
name: "reload on create disabled, initialized",
|
||||
reloadOnCreate: false,
|
||||
syncAfterRestart: false,
|
||||
initialized: true,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "not initialized, sync after restart enabled",
|
||||
reloadOnCreate: true,
|
||||
syncAfterRestart: true,
|
||||
initialized: false,
|
||||
expectedResult: true,
|
||||
name: "not initialized, sync after restart enabled",
|
||||
reloadOnCreate: true,
|
||||
syncAfterRestart: true,
|
||||
initialized: false,
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "not initialized, sync after restart disabled",
|
||||
reloadOnCreate: true,
|
||||
syncAfterRestart: false,
|
||||
initialized: false,
|
||||
expectedResult: false,
|
||||
name: "not initialized, sync after restart disabled",
|
||||
reloadOnCreate: true,
|
||||
syncAfterRestart: false,
|
||||
initialized: false,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "not initialized, sync after restart disabled, reload on create disabled",
|
||||
reloadOnCreate: false,
|
||||
syncAfterRestart: false,
|
||||
initialized: false,
|
||||
expectedResult: false,
|
||||
name: "not initialized, sync after restart disabled, reload on create disabled",
|
||||
reloadOnCreate: false,
|
||||
syncAfterRestart: false,
|
||||
initialized: false,
|
||||
expectedResult: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
ReloadOnCreate: tt.reloadOnCreate,
|
||||
SyncAfterRestart: tt.syncAfterRestart,
|
||||
}
|
||||
initialized := tt.initialized
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
ReloadOnCreate: tt.reloadOnCreate,
|
||||
SyncAfterRestart: tt.syncAfterRestart,
|
||||
}
|
||||
initialized := tt.initialized
|
||||
|
||||
pred := createEventPredicate(cfg, &initialized)
|
||||
pred := createEventPredicate(cfg, &initialized)
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}
|
||||
|
||||
e := event.CreateEvent{Object: cm}
|
||||
result := pred.Create(e)
|
||||
e := event.CreateEvent{Object: cm}
|
||||
result := pred.Create(e)
|
||||
|
||||
if result != tt.expectedResult {
|
||||
t.Errorf("CreateFunc() = %v, want %v", result, tt.expectedResult)
|
||||
}
|
||||
})
|
||||
if result != tt.expectedResult {
|
||||
t.Errorf("CreateFunc() = %v, want %v", result, tt.expectedResult)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +93,6 @@ func TestCreateEventPredicate_UpdateEvent(t *testing.T) {
|
||||
e := event.UpdateEvent{ObjectOld: cm, ObjectNew: cm}
|
||||
result := pred.Update(e)
|
||||
|
||||
// Update events should always return true
|
||||
if !result {
|
||||
t.Error("UpdateFunc() should always return true")
|
||||
}
|
||||
@@ -116,25 +117,27 @@ func TestCreateEventPredicate_DeleteEvent(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
ReloadOnDelete: tt.reloadOnDelete,
|
||||
}
|
||||
initialized := true
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
ReloadOnDelete: tt.reloadOnDelete,
|
||||
}
|
||||
initialized := true
|
||||
|
||||
pred := createEventPredicate(cfg, &initialized)
|
||||
pred := createEventPredicate(cfg, &initialized)
|
||||
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}
|
||||
|
||||
e := event.DeleteEvent{Object: cm}
|
||||
result := pred.Delete(e)
|
||||
e := event.DeleteEvent{Object: cm}
|
||||
result := pred.Delete(e)
|
||||
|
||||
if result != tt.expectedResult {
|
||||
t.Errorf("DeleteFunc() = %v, want %v", result, tt.expectedResult)
|
||||
}
|
||||
})
|
||||
if result != tt.expectedResult {
|
||||
t.Errorf("DeleteFunc() = %v, want %v", result, tt.expectedResult)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +154,6 @@ func TestCreateEventPredicate_GenericEvent(t *testing.T) {
|
||||
e := event.GenericEvent{Object: cm}
|
||||
result := pred.Generic(e)
|
||||
|
||||
// Generic events should always return false
|
||||
if result {
|
||||
t.Error("GenericFunc() should always return false")
|
||||
}
|
||||
@@ -164,17 +166,14 @@ func TestBuildEventFilter(t *testing.T) {
|
||||
}
|
||||
initialized := true
|
||||
|
||||
// Create a simple always-true predicate as the resource predicate
|
||||
resourcePred := &alwaysTruePredicate{}
|
||||
|
||||
filter := BuildEventFilter(resourcePred, cfg, &initialized)
|
||||
|
||||
// The filter should be created without error
|
||||
if filter == nil {
|
||||
t.Fatal("BuildEventFilter() should return a non-nil predicate")
|
||||
}
|
||||
|
||||
// Test update event passes (since resourcePred returns true and update always returns true)
|
||||
cm := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}
|
||||
@@ -182,7 +181,6 @@ func TestBuildEventFilter(t *testing.T) {
|
||||
e := event.UpdateEvent{ObjectOld: cm, ObjectNew: cm}
|
||||
result := filter.Update(e)
|
||||
|
||||
// Since namespace filter is empty (all namespaces allowed), this should pass
|
||||
if !result {
|
||||
t.Error("UpdateFunc() should return true when all predicates pass")
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
@@ -81,6 +82,37 @@ func NewManager(opts ManagerOptions) (ctrl.Manager, error) {
|
||||
return mgr, nil
|
||||
}
|
||||
|
||||
// NewManagerWithRestConfig creates a new controller-runtime manager with the given rest.Config.
|
||||
// This is useful for testing where you have a pre-existing cluster configuration.
|
||||
func NewManagerWithRestConfig(opts ManagerOptions, restConfig *rest.Config) (ctrl.Manager, error) {
|
||||
cfg := opts.Config
|
||||
le := cfg.LeaderElection
|
||||
|
||||
mgrOpts := ctrl.Options{
|
||||
Scheme: runtimeScheme,
|
||||
Metrics: ctrlmetrics.Options{
|
||||
BindAddress: "0", // Disable metrics server in tests
|
||||
},
|
||||
HealthProbeBindAddress: "0", // Disable health probes in tests
|
||||
|
||||
// Leader election configuration
|
||||
LeaderElection: cfg.EnableHA,
|
||||
LeaderElectionID: le.LockName,
|
||||
LeaderElectionNamespace: le.Namespace,
|
||||
LeaderElectionReleaseOnCancel: le.ReleaseOnCancel,
|
||||
LeaseDuration: &le.LeaseDuration,
|
||||
RenewDeadline: &le.RenewDeadline,
|
||||
RetryPeriod: &le.RetryPeriod,
|
||||
}
|
||||
|
||||
mgr, err := ctrl.NewManager(restConfig, mgrOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating manager: %w", err)
|
||||
}
|
||||
|
||||
return mgr, nil
|
||||
}
|
||||
|
||||
// SetupReconcilers sets up all reconcilers with the manager.
|
||||
func SetupReconcilers(mgr ctrl.Manager, cfg *config.Config, log logr.Logger, collectors *metrics.Collectors) error {
|
||||
registry := workload.NewRegistry(cfg.ArgoRolloutsEnabled)
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/go-logr/logr/testr"
|
||||
"github.com/stakater/Reloader/internal/pkg/config"
|
||||
"github.com/stakater/Reloader/internal/pkg/controller"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
func TestNamespaceCache_Basic(t *testing.T) {
|
||||
cache := controller.NewNamespaceCache(true)
|
||||
|
||||
// Test Add and Contains
|
||||
cache.Add("namespace-1")
|
||||
if !cache.Contains("namespace-1") {
|
||||
t.Error("Cache should contain namespace-1")
|
||||
@@ -28,7 +19,6 @@ func TestNamespaceCache_Basic(t *testing.T) {
|
||||
t.Error("Cache should not contain namespace-2")
|
||||
}
|
||||
|
||||
// Test Remove
|
||||
cache.Remove("namespace-1")
|
||||
if cache.Contains("namespace-1") {
|
||||
t.Error("Cache should not contain namespace-1 after removal")
|
||||
@@ -38,13 +28,9 @@ func TestNamespaceCache_Basic(t *testing.T) {
|
||||
func TestNamespaceCache_Disabled(t *testing.T) {
|
||||
cache := controller.NewNamespaceCache(false)
|
||||
|
||||
// When disabled, Contains should always return true
|
||||
if !cache.Contains("any-namespace") {
|
||||
t.Error("Disabled cache should return true for any namespace")
|
||||
}
|
||||
if !cache.Contains("other-namespace") {
|
||||
t.Error("Disabled cache should return true for any namespace")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceCache_List(t *testing.T) {
|
||||
@@ -58,7 +44,6 @@ func TestNamespaceCache_List(t *testing.T) {
|
||||
t.Errorf("Expected 3 namespaces, got %d", len(list))
|
||||
}
|
||||
|
||||
// Check all namespaces are in the list
|
||||
found := make(map[string]bool)
|
||||
for _, ns := range list {
|
||||
found[ns] = true
|
||||
@@ -71,53 +56,24 @@ func TestNamespaceCache_List(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNamespaceCache_IsEnabled(t *testing.T) {
|
||||
enabledCache := controller.NewNamespaceCache(true)
|
||||
disabledCache := controller.NewNamespaceCache(false)
|
||||
|
||||
if !enabledCache.IsEnabled() {
|
||||
if !controller.NewNamespaceCache(true).IsEnabled() {
|
||||
t.Error("EnabledCache.IsEnabled() should return true")
|
||||
}
|
||||
if disabledCache.IsEnabled() {
|
||||
if controller.NewNamespaceCache(false).IsEnabled() {
|
||||
t.Error("DisabledCache.IsEnabled() should return false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceReconciler_Add(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
|
||||
ns := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ns",
|
||||
Labels: map[string]string{"env": "production"},
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithObjects(ns).
|
||||
Build()
|
||||
|
||||
cfg := config.NewDefault()
|
||||
selector, _ := labels.Parse("env=production")
|
||||
cfg.NamespaceSelectors = []labels.Selector{selector}
|
||||
|
||||
cache := controller.NewNamespaceCache(true)
|
||||
reconciler := &controller.NamespaceReconciler{
|
||||
Client: fakeClient,
|
||||
Log: testr.New(t),
|
||||
Config: cfg,
|
||||
Cache: cache,
|
||||
}
|
||||
ns := testNamespace("test-ns", map[string]string{"env": "production"})
|
||||
reconciler := newNamespaceReconciler(t, cfg, cache, ns)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "test-ns"},
|
||||
}
|
||||
|
||||
_, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
assertReconcileSuccess(t, reconciler, namespaceRequest("test-ns"))
|
||||
|
||||
if !cache.Contains("test-ns") {
|
||||
t.Error("Cache should contain test-ns after reconcile")
|
||||
@@ -125,45 +81,17 @@ func TestNamespaceReconciler_Add(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNamespaceReconciler_Remove_LabelChange(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
|
||||
// Namespace with non-matching labels
|
||||
ns := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ns",
|
||||
Labels: map[string]string{"env": "staging"},
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithObjects(ns).
|
||||
Build()
|
||||
|
||||
cfg := config.NewDefault()
|
||||
selector, _ := labels.Parse("env=production")
|
||||
cfg.NamespaceSelectors = []labels.Selector{selector}
|
||||
|
||||
cache := controller.NewNamespaceCache(true)
|
||||
// Pre-populate cache
|
||||
cache.Add("test-ns")
|
||||
cache.Add("test-ns") // Pre-populate
|
||||
|
||||
reconciler := &controller.NamespaceReconciler{
|
||||
Client: fakeClient,
|
||||
Log: testr.New(t),
|
||||
Config: cfg,
|
||||
Cache: cache,
|
||||
}
|
||||
ns := testNamespace("test-ns", map[string]string{"env": "staging"}) // Non-matching
|
||||
reconciler := newNamespaceReconciler(t, cfg, cache, ns)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "test-ns"},
|
||||
}
|
||||
|
||||
_, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
assertReconcileSuccess(t, reconciler, namespaceRequest("test-ns"))
|
||||
|
||||
if cache.Contains("test-ns") {
|
||||
t.Error("Cache should not contain test-ns after reconcile (labels no longer match)")
|
||||
@@ -171,37 +99,16 @@ func TestNamespaceReconciler_Remove_LabelChange(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNamespaceReconciler_Remove_Delete(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
|
||||
// No namespace in cluster (simulates delete)
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
Build()
|
||||
|
||||
cfg := config.NewDefault()
|
||||
selector, _ := labels.Parse("env=production")
|
||||
cfg.NamespaceSelectors = []labels.Selector{selector}
|
||||
|
||||
cache := controller.NewNamespaceCache(true)
|
||||
// Pre-populate cache
|
||||
cache.Add("deleted-ns")
|
||||
cache.Add("deleted-ns") // Pre-populate
|
||||
|
||||
reconciler := &controller.NamespaceReconciler{
|
||||
Client: fakeClient,
|
||||
Log: testr.New(t),
|
||||
Config: cfg,
|
||||
Cache: cache,
|
||||
}
|
||||
reconciler := newNamespaceReconciler(t, cfg, cache) // No namespace in cluster
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "deleted-ns"},
|
||||
}
|
||||
|
||||
_, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
assertReconcileSuccess(t, reconciler, namespaceRequest("deleted-ns"))
|
||||
|
||||
if cache.Contains("deleted-ns") {
|
||||
t.Error("Cache should not contain deleted-ns after reconcile")
|
||||
@@ -209,85 +116,32 @@ func TestNamespaceReconciler_Remove_Delete(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNamespaceReconciler_MultipleSelectors(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
|
||||
ns := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ns",
|
||||
Labels: map[string]string{"team": "platform"},
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithObjects(ns).
|
||||
Build()
|
||||
|
||||
cfg := config.NewDefault()
|
||||
selector1, _ := labels.Parse("env=production")
|
||||
selector2, _ := labels.Parse("team=platform")
|
||||
cfg.NamespaceSelectors = []labels.Selector{selector1, selector2}
|
||||
|
||||
cache := controller.NewNamespaceCache(true)
|
||||
reconciler := &controller.NamespaceReconciler{
|
||||
Client: fakeClient,
|
||||
Log: testr.New(t),
|
||||
Config: cfg,
|
||||
Cache: cache,
|
||||
}
|
||||
ns := testNamespace("test-ns", map[string]string{"team": "platform"})
|
||||
reconciler := newNamespaceReconciler(t, cfg, cache, ns)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "test-ns"},
|
||||
}
|
||||
assertReconcileSuccess(t, reconciler, namespaceRequest("test-ns"))
|
||||
|
||||
_, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
|
||||
// Should be added because it matches second selector (team=platform)
|
||||
if !cache.Contains("test-ns") {
|
||||
t.Error("Cache should contain test-ns (matches second selector)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceReconciler_NoLabels(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
|
||||
// Namespace with no labels
|
||||
ns := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ns",
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithObjects(ns).
|
||||
Build()
|
||||
|
||||
cfg := config.NewDefault()
|
||||
selector, _ := labels.Parse("env=production")
|
||||
cfg.NamespaceSelectors = []labels.Selector{selector}
|
||||
|
||||
cache := controller.NewNamespaceCache(true)
|
||||
reconciler := &controller.NamespaceReconciler{
|
||||
Client: fakeClient,
|
||||
Log: testr.New(t),
|
||||
Config: cfg,
|
||||
Cache: cache,
|
||||
}
|
||||
ns := testNamespace("test-ns", nil) // No labels
|
||||
reconciler := newNamespaceReconciler(t, cfg, cache, ns)
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "test-ns"},
|
||||
}
|
||||
|
||||
_, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
assertReconcileSuccess(t, reconciler, namespaceRequest("test-ns"))
|
||||
|
||||
if cache.Contains("test-ns") {
|
||||
t.Error("Cache should not contain test-ns (no labels)")
|
||||
|
||||
@@ -1,118 +1,286 @@
|
||||
package controller
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stakater/Reloader/internal/pkg/config"
|
||||
"github.com/stakater/Reloader/internal/pkg/controller"
|
||||
"github.com/stakater/Reloader/internal/pkg/reload"
|
||||
"github.com/stakater/Reloader/internal/pkg/workload"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
func TestUpdateWorkloadWithRetry_SwitchCases(t *testing.T) {
|
||||
// Test that the switch statement correctly identifies workload types
|
||||
// Note: Full integration tests require a fake k8s client, so we just test type detection
|
||||
|
||||
func TestUpdateWorkloadWithRetry_WorkloadTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
workload workload.WorkloadAccessor
|
||||
expectedKind workload.Kind
|
||||
object runtime.Object
|
||||
workload func(runtime.Object) workload.WorkloadAccessor
|
||||
resourceType reload.ResourceType
|
||||
verify func(t *testing.T, c client.Client)
|
||||
}{
|
||||
{
|
||||
name: "deployment workload",
|
||||
workload: workload.NewDeploymentWorkload(&appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}),
|
||||
expectedKind: workload.KindDeployment,
|
||||
name: "Deployment",
|
||||
object: testDeployment("test-deployment", "default", nil),
|
||||
workload: func(o runtime.Object) workload.WorkloadAccessor {
|
||||
return workload.NewDeploymentWorkload(o.(*appsv1.Deployment))
|
||||
},
|
||||
resourceType: reload.ResourceTypeConfigMap,
|
||||
verify: func(t *testing.T, c client.Client) {
|
||||
var result appsv1.Deployment
|
||||
if err := c.Get(context.Background(), types.NamespacedName{Name: "test-deployment", Namespace: "default"}, &result); err != nil {
|
||||
t.Fatalf("Failed to get deployment: %v", err)
|
||||
}
|
||||
if result.Spec.Template.Annotations == nil {
|
||||
t.Fatal("Expected pod template annotations to be set")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "daemonset workload",
|
||||
workload: workload.NewDaemonSetWorkload(&appsv1.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}),
|
||||
expectedKind: workload.KindDaemonSet,
|
||||
name: "DaemonSet",
|
||||
object: testDaemonSet("test-daemonset", "default", nil),
|
||||
workload: func(o runtime.Object) workload.WorkloadAccessor {
|
||||
return workload.NewDaemonSetWorkload(o.(*appsv1.DaemonSet))
|
||||
},
|
||||
resourceType: reload.ResourceTypeSecret,
|
||||
verify: func(t *testing.T, c client.Client) {
|
||||
var result appsv1.DaemonSet
|
||||
if err := c.Get(context.Background(), types.NamespacedName{Name: "test-daemonset", Namespace: "default"}, &result); err != nil {
|
||||
t.Fatalf("Failed to get daemonset: %v", err)
|
||||
}
|
||||
if result.Spec.Template.Annotations == nil {
|
||||
t.Fatal("Expected pod template annotations to be set")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "statefulset workload",
|
||||
workload: workload.NewStatefulSetWorkload(&appsv1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}),
|
||||
expectedKind: workload.KindStatefulSet,
|
||||
name: "StatefulSet",
|
||||
object: testStatefulSet("test-statefulset", "default", nil),
|
||||
workload: func(o runtime.Object) workload.WorkloadAccessor {
|
||||
return workload.NewStatefulSetWorkload(o.(*appsv1.StatefulSet))
|
||||
},
|
||||
resourceType: reload.ResourceTypeConfigMap,
|
||||
verify: func(t *testing.T, c client.Client) {
|
||||
var result appsv1.StatefulSet
|
||||
if err := c.Get(context.Background(), types.NamespacedName{Name: "test-statefulset", Namespace: "default"}, &result); err != nil {
|
||||
t.Fatalf("Failed to get statefulset: %v", err)
|
||||
}
|
||||
if result.Spec.Template.Annotations == nil {
|
||||
t.Fatal("Expected pod template annotations to be set")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "job workload",
|
||||
workload: workload.NewJobWorkload(&batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}),
|
||||
expectedKind: workload.KindJob,
|
||||
name: "Job",
|
||||
object: testJob("test-job", "default"),
|
||||
workload: func(o runtime.Object) workload.WorkloadAccessor {
|
||||
return workload.NewJobWorkload(o.(*batchv1.Job))
|
||||
},
|
||||
resourceType: reload.ResourceTypeConfigMap,
|
||||
verify: func(t *testing.T, c client.Client) {
|
||||
var jobs batchv1.JobList
|
||||
if err := c.List(context.Background(), &jobs, client.InNamespace("default")); err != nil {
|
||||
t.Fatalf("Failed to list jobs: %v", err)
|
||||
}
|
||||
if len(jobs.Items) != 1 {
|
||||
t.Errorf("Expected 1 job (recreated), got %d", len(jobs.Items))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cronjob workload",
|
||||
workload: workload.NewCronJobWorkload(&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
|
||||
}),
|
||||
expectedKind: workload.KindCronJob,
|
||||
name: "CronJob",
|
||||
object: testCronJob("test-cronjob", "default"),
|
||||
workload: func(o runtime.Object) workload.WorkloadAccessor {
|
||||
return workload.NewCronJobWorkload(o.(*batchv1.CronJob))
|
||||
},
|
||||
resourceType: reload.ResourceTypeSecret,
|
||||
verify: func(t *testing.T, c client.Client) {
|
||||
var jobs batchv1.JobList
|
||||
if err := c.List(context.Background(), &jobs, client.InNamespace("default")); err != nil {
|
||||
t.Fatalf("Failed to list jobs: %v", err)
|
||||
}
|
||||
if len(jobs.Items) != 1 {
|
||||
t.Errorf("Expected 1 job from cronjob, got %d", len(jobs.Items))
|
||||
}
|
||||
if len(jobs.Items) > 0 && jobs.Items[0].Annotations["cronjob.kubernetes.io/instantiate"] != "manual" {
|
||||
t.Error("Expected job to have manual instantiate annotation")
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Verify the workload kind is correctly identified
|
||||
if tt.workload.Kind() != tt.expectedKind {
|
||||
t.Errorf("workload.Kind() = %v, want %v", tt.workload.Kind(), tt.expectedKind)
|
||||
}
|
||||
})
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
reloadService := reload.NewService(cfg)
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(testScheme()).
|
||||
WithRuntimeObjects(tt.object).
|
||||
Build()
|
||||
|
||||
wl := tt.workload(tt.object)
|
||||
|
||||
updated, err := controller.UpdateWorkloadWithRetry(
|
||||
context.Background(),
|
||||
fakeClient,
|
||||
reloadService,
|
||||
wl,
|
||||
"test-resource",
|
||||
tt.resourceType,
|
||||
"default",
|
||||
"abc123",
|
||||
false,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateWorkloadWithRetry failed: %v", err)
|
||||
}
|
||||
if !updated {
|
||||
t.Error("Expected workload to be updated")
|
||||
}
|
||||
|
||||
tt.verify(t, fakeClient)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJobWorkloadTypeCast(t *testing.T) {
|
||||
// Test that JobWorkload type cast works correctly
|
||||
job := &batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-job", Namespace: "default"},
|
||||
}
|
||||
jobWl := workload.NewJobWorkload(job)
|
||||
|
||||
if jobWl.GetName() != "test-job" {
|
||||
t.Errorf("JobWorkload.GetName() = %v, want test-job", jobWl.GetName())
|
||||
}
|
||||
|
||||
// Test GetJob method
|
||||
gotJob := jobWl.GetJob()
|
||||
if gotJob.Name != "test-job" {
|
||||
t.Errorf("JobWorkload.GetJob().Name = %v, want test-job", gotJob.Name)
|
||||
}
|
||||
|
||||
// Verify it satisfies WorkloadAccessor interface
|
||||
var _ workload.WorkloadAccessor = jobWl
|
||||
}
|
||||
|
||||
func TestCronJobWorkloadTypeCast(t *testing.T) {
|
||||
// Test that CronJobWorkload type cast works correctly
|
||||
cronJob := &batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-cronjob", Namespace: "default"},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "*/5 * * * *",
|
||||
func TestUpdateWorkloadWithRetry_Strategies(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
strategy config.ReloadStrategy
|
||||
verify func(t *testing.T, cfg *config.Config, result *appsv1.Deployment)
|
||||
}{
|
||||
{
|
||||
name: "EnvVarStrategy",
|
||||
strategy: config.ReloadStrategyEnvVars,
|
||||
verify: func(t *testing.T, cfg *config.Config, result *appsv1.Deployment) {
|
||||
found := false
|
||||
for _, env := range result.Spec.Template.Spec.Containers[0].Env {
|
||||
if env.Name == "STAKATER_TEST_CM_CONFIGMAP" && env.Value == "abc123" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Expected STAKATER_TEST_CM_CONFIGMAP env var to be set")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AnnotationStrategy",
|
||||
strategy: config.ReloadStrategyAnnotations,
|
||||
verify: func(t *testing.T, cfg *config.Config, result *appsv1.Deployment) {
|
||||
if result.Spec.Template.Annotations == nil {
|
||||
t.Fatal("Expected pod template annotations to be set")
|
||||
}
|
||||
if _, ok := result.Spec.Template.Annotations[cfg.Annotations.LastReloadedFrom]; !ok {
|
||||
t.Errorf("Expected %s annotation to be set", cfg.Annotations.LastReloadedFrom)
|
||||
}
|
||||
for _, env := range result.Spec.Template.Spec.Containers[0].Env {
|
||||
if env.Name == "STAKATER_TEST_CM_CONFIGMAP" {
|
||||
t.Error("Annotation strategy should not add env vars")
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
cronJobWl := workload.NewCronJobWorkload(cronJob)
|
||||
|
||||
if cronJobWl.GetName() != "test-cronjob" {
|
||||
t.Errorf("CronJobWorkload.GetName() = %v, want test-cronjob", cronJobWl.GetName())
|
||||
for _, tt := range tests {
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
cfg.ReloadStrategy = tt.strategy
|
||||
reloadService := reload.NewService(cfg)
|
||||
|
||||
deployment := testDeployment("test-deployment", "default", nil)
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(testScheme()).
|
||||
WithObjects(deployment).
|
||||
Build()
|
||||
|
||||
wl := workload.NewDeploymentWorkload(deployment)
|
||||
|
||||
updated, err := controller.UpdateWorkloadWithRetry(
|
||||
context.Background(),
|
||||
fakeClient,
|
||||
reloadService,
|
||||
wl,
|
||||
"test-cm",
|
||||
reload.ResourceTypeConfigMap,
|
||||
"default",
|
||||
"abc123",
|
||||
false,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateWorkloadWithRetry failed: %v", err)
|
||||
}
|
||||
if !updated {
|
||||
t.Error("Expected workload to be updated")
|
||||
}
|
||||
|
||||
var result appsv1.Deployment
|
||||
if err := fakeClient.Get(
|
||||
context.Background(), types.NamespacedName{Name: "test-deployment", Namespace: "default"}, &result,
|
||||
); err != nil {
|
||||
t.Fatalf("Failed to get deployment: %v", err)
|
||||
}
|
||||
|
||||
tt.verify(t, cfg, &result)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateWorkloadWithRetry_NoUpdate(t *testing.T) {
|
||||
cfg := config.NewDefault()
|
||||
reloadService := reload.NewService(cfg)
|
||||
|
||||
deployment := testDeployment("test-deployment", "default", nil)
|
||||
deployment.Spec.Template.Spec.Containers[0].Env = []corev1.EnvVar{
|
||||
{
|
||||
Name: "STAKATER_TEST_CM_CONFIGMAP",
|
||||
Value: "abc123",
|
||||
},
|
||||
}
|
||||
|
||||
// Test GetCronJob method
|
||||
gotCronJob := cronJobWl.GetCronJob()
|
||||
if gotCronJob.Name != "test-cronjob" {
|
||||
t.Errorf("CronJobWorkload.GetCronJob().Name = %v, want test-cronjob", gotCronJob.Name)
|
||||
}
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(testScheme()).
|
||||
WithObjects(deployment).
|
||||
Build()
|
||||
|
||||
// Verify it satisfies WorkloadAccessor interface
|
||||
var _ workload.WorkloadAccessor = cronJobWl
|
||||
wl := workload.NewDeploymentWorkload(deployment)
|
||||
|
||||
updated, err := controller.UpdateWorkloadWithRetry(
|
||||
context.Background(),
|
||||
fakeClient,
|
||||
reloadService,
|
||||
wl,
|
||||
"test-cm",
|
||||
reload.ResourceTypeConfigMap,
|
||||
"default",
|
||||
"abc123", // Same hash as already set
|
||||
false,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateWorkloadWithRetry failed: %v", err)
|
||||
}
|
||||
if updated {
|
||||
t.Error("Expected workload NOT to be updated (same hash)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceTypeKind(t *testing.T) {
|
||||
// Test that ResourceType.Kind() returns correct values
|
||||
tests := []struct {
|
||||
resourceType reload.ResourceType
|
||||
expectedKind string
|
||||
@@ -122,10 +290,12 @@ func TestResourceTypeKind(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.resourceType), func(t *testing.T) {
|
||||
if got := tt.resourceType.Kind(); got != tt.expectedKind {
|
||||
t.Errorf("ResourceType.Kind() = %v, want %v", got, tt.expectedKind)
|
||||
}
|
||||
})
|
||||
t.Run(
|
||||
string(tt.resourceType), func(t *testing.T) {
|
||||
if got := tt.resourceType.Kind(); got != tt.expectedKind {
|
||||
t.Errorf("ResourceType.Kind() = %v, want %v", got, tt.expectedKind)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,418 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/go-logr/logr/testr"
|
||||
"github.com/stakater/Reloader/internal/pkg/alerting"
|
||||
"github.com/stakater/Reloader/internal/pkg/config"
|
||||
"github.com/stakater/Reloader/internal/pkg/controller"
|
||||
"github.com/stakater/Reloader/internal/pkg/events"
|
||||
"github.com/stakater/Reloader/internal/pkg/metrics"
|
||||
"github.com/stakater/Reloader/internal/pkg/reload"
|
||||
"github.com/stakater/Reloader/internal/pkg/webhook"
|
||||
"github.com/stakater/Reloader/internal/pkg/workload"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
// testScheme is a shared scheme for all controller tests.
|
||||
func testScheme() *runtime.Scheme {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
_ = appsv1.AddToScheme(scheme)
|
||||
_ = batchv1.AddToScheme(scheme)
|
||||
return scheme
|
||||
}
|
||||
|
||||
// newConfigMapReconciler creates a ConfigMapReconciler for testing.
|
||||
func newConfigMapReconciler(t *testing.T, cfg *config.Config, objects ...runtime.Object) *controller.ConfigMapReconciler {
|
||||
t.Helper()
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(testScheme()).
|
||||
WithRuntimeObjects(objects...).
|
||||
Build()
|
||||
|
||||
collectors := metrics.NewCollectors()
|
||||
|
||||
return &controller.ConfigMapReconciler{
|
||||
Client: fakeClient,
|
||||
Log: testr.New(t),
|
||||
Config: cfg,
|
||||
ReloadService: reload.NewService(cfg),
|
||||
Registry: workload.NewRegistry(cfg.ArgoRolloutsEnabled),
|
||||
Collectors: &collectors,
|
||||
EventRecorder: events.NewRecorder(nil),
|
||||
WebhookClient: webhook.NewClient("", testr.New(t)),
|
||||
Alerter: &alerting.NoOpAlerter{},
|
||||
}
|
||||
}
|
||||
|
||||
// newSecretReconciler creates a SecretReconciler for testing.
|
||||
func newSecretReconciler(t *testing.T, cfg *config.Config, objects ...runtime.Object) *controller.SecretReconciler {
|
||||
t.Helper()
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(testScheme()).
|
||||
WithRuntimeObjects(objects...).
|
||||
Build()
|
||||
|
||||
collectors := metrics.NewCollectors()
|
||||
|
||||
return &controller.SecretReconciler{
|
||||
Client: fakeClient,
|
||||
Log: testr.New(t),
|
||||
Config: cfg,
|
||||
ReloadService: reload.NewService(cfg),
|
||||
Registry: workload.NewRegistry(cfg.ArgoRolloutsEnabled),
|
||||
Collectors: &collectors,
|
||||
EventRecorder: events.NewRecorder(nil),
|
||||
WebhookClient: webhook.NewClient("", testr.New(t)),
|
||||
Alerter: &alerting.NoOpAlerter{},
|
||||
}
|
||||
}
|
||||
|
||||
// testConfigMap creates a ConfigMap for testing.
|
||||
func testConfigMap(name, namespace string) *corev1.ConfigMap {
|
||||
return &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Data: map[string]string{"key": "value"},
|
||||
}
|
||||
}
|
||||
|
||||
// testConfigMapWithAnnotations creates a ConfigMap with annotations.
|
||||
func testConfigMapWithAnnotations(name, namespace string, annotations map[string]string) *corev1.ConfigMap {
|
||||
cm := testConfigMap(name, namespace)
|
||||
cm.Annotations = annotations
|
||||
return cm
|
||||
}
|
||||
|
||||
// testSecret creates a Secret for testing.
|
||||
func testSecret(name, namespace string) *corev1.Secret {
|
||||
return &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Data: map[string][]byte{"key": []byte("value")},
|
||||
}
|
||||
}
|
||||
|
||||
// testSecretWithAnnotations creates a Secret with annotations.
|
||||
func testSecretWithAnnotations(name, namespace string, annotations map[string]string) *corev1.Secret {
|
||||
secret := testSecret(name, namespace)
|
||||
secret.Annotations = annotations
|
||||
return secret
|
||||
}
|
||||
|
||||
// testDeployment creates a minimal Deployment for testing.
|
||||
func testDeployment(name, namespace string, annotations map[string]string) *appsv1.Deployment {
|
||||
return &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: annotations,
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": name},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": name},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// testDeploymentWithEnvFrom creates a Deployment with EnvFrom referencing a ConfigMap or Secret.
|
||||
func testDeploymentWithEnvFrom(name, namespace string, configMapName, secretName string) *appsv1.Deployment {
|
||||
d := testDeployment(name, namespace, nil)
|
||||
if configMapName != "" {
|
||||
d.Spec.Template.Spec.Containers[0].EnvFrom = append(
|
||||
d.Spec.Template.Spec.Containers[0].EnvFrom,
|
||||
corev1.EnvFromSource{
|
||||
ConfigMapRef: &corev1.ConfigMapEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{Name: configMapName},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
if secretName != "" {
|
||||
d.Spec.Template.Spec.Containers[0].EnvFrom = append(
|
||||
d.Spec.Template.Spec.Containers[0].EnvFrom,
|
||||
corev1.EnvFromSource{
|
||||
SecretRef: &corev1.SecretEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{Name: secretName},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// testDeploymentWithVolume creates a Deployment with a volume from ConfigMap or Secret.
|
||||
func testDeploymentWithVolume(name, namespace string, configMapName, secretName string) *appsv1.Deployment {
|
||||
d := testDeployment(name, namespace, nil)
|
||||
d.Spec.Template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{
|
||||
{
|
||||
Name: "config",
|
||||
MountPath: "/etc/config",
|
||||
},
|
||||
}
|
||||
|
||||
if configMapName != "" {
|
||||
d.Spec.Template.Spec.Volumes = []corev1.Volume{
|
||||
{
|
||||
Name: "config",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
ConfigMap: &corev1.ConfigMapVolumeSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{Name: configMapName},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
if secretName != "" {
|
||||
d.Spec.Template.Spec.Volumes = []corev1.Volume{
|
||||
{
|
||||
Name: "config",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
SecretName: secretName,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// testDeploymentWithProjectedVolume creates a Deployment with a projected volume.
|
||||
func testDeploymentWithProjectedVolume(name, namespace string, configMapName, secretName string) *appsv1.Deployment {
|
||||
d := testDeployment(name, namespace, nil)
|
||||
d.Spec.Template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{
|
||||
{
|
||||
Name: "config",
|
||||
MountPath: "/etc/config",
|
||||
},
|
||||
}
|
||||
|
||||
var sources []corev1.VolumeProjection
|
||||
if configMapName != "" {
|
||||
sources = append(
|
||||
sources, corev1.VolumeProjection{
|
||||
ConfigMap: &corev1.ConfigMapProjection{
|
||||
LocalObjectReference: corev1.LocalObjectReference{Name: configMapName},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
if secretName != "" {
|
||||
sources = append(
|
||||
sources, corev1.VolumeProjection{
|
||||
Secret: &corev1.SecretProjection{
|
||||
LocalObjectReference: corev1.LocalObjectReference{Name: secretName},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
d.Spec.Template.Spec.Volumes = []corev1.Volume{
|
||||
{
|
||||
Name: "config",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Projected: &corev1.ProjectedVolumeSource{Sources: sources},
|
||||
},
|
||||
},
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// testDaemonSet creates a minimal DaemonSet for testing.
|
||||
func testDaemonSet(name, namespace string, annotations map[string]string) *appsv1.DaemonSet {
|
||||
return &appsv1.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: annotations,
|
||||
},
|
||||
Spec: appsv1.DaemonSetSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": name},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": name},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// testStatefulSet creates a minimal StatefulSet for testing.
|
||||
func testStatefulSet(name, namespace string, annotations map[string]string) *appsv1.StatefulSet {
|
||||
return &appsv1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: annotations,
|
||||
},
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": name},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": name},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "main",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// reconcileRequest creates a ctrl.Request for the given name and namespace.
|
||||
func reconcileRequest(name, namespace string) ctrl.Request {
|
||||
return ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// namespaceRequest creates a ctrl.Request for a namespace (no namespace field needed).
|
||||
func namespaceRequest(name string) ctrl.Request {
|
||||
return ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{Name: name},
|
||||
}
|
||||
}
|
||||
|
||||
// testNamespace creates a Namespace with optional labels.
|
||||
func testNamespace(name string, labels map[string]string) *corev1.Namespace {
|
||||
return &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Labels: labels,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// newNamespaceReconciler creates a NamespaceReconciler for testing.
|
||||
func newNamespaceReconciler(t *testing.T, cfg *config.Config, cache *controller.NamespaceCache, objects ...runtime.Object) *controller.NamespaceReconciler {
|
||||
t.Helper()
|
||||
scheme := runtime.NewScheme()
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithRuntimeObjects(objects...).
|
||||
Build()
|
||||
|
||||
return &controller.NamespaceReconciler{
|
||||
Client: fakeClient,
|
||||
Log: testr.New(t),
|
||||
Config: cfg,
|
||||
Cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
// assertReconcileSuccess runs reconcile and asserts no error and no requeue.
|
||||
func assertReconcileSuccess(t *testing.T, reconciler interface {
|
||||
Reconcile(context.Context, ctrl.Request) (ctrl.Result, error)
|
||||
}, req ctrl.Request) {
|
||||
t.Helper()
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile failed: %v", err)
|
||||
}
|
||||
if result.Requeue {
|
||||
t.Error("Should not requeue")
|
||||
}
|
||||
}
|
||||
|
||||
// testJob creates a minimal Job for testing.
|
||||
func testJob(name, namespace string) *batchv1.Job {
|
||||
return &batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "main",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// testCronJob creates a minimal CronJob for testing.
|
||||
func testCronJob(name, namespace string) *batchv1.CronJob {
|
||||
return &batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
UID: "test-uid",
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "*/5 * * * *",
|
||||
JobTemplate: batchv1.JobTemplateSpec{
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "main",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user