From debebc53fcd6e5c7e1568756eca520456628924a Mon Sep 17 00:00:00 2001 From: Erkki Eilonen Date: Thu, 16 Jul 2026 11:08:18 +0700 Subject: [PATCH 1/4] Add imagePullSecrets to the LavinMQ spec Passes spec.imagePullSecrets through to the StatefulSet pod template so the LavinMQ image can be pulled from private registries. --- api/v1alpha1/lavinmq_types.go | 3 +++ api/v1alpha1/zz_generated.deepcopy.go | 5 +++++ config/crd/bases/cloudamqp.com_lavinmqs.yaml | 18 ++++++++++++++++++ internal/reconciler/statefulset.go | 4 ++++ 4 files changed, 30 insertions(+) diff --git a/api/v1alpha1/lavinmq_types.go b/api/v1alpha1/lavinmq_types.go index 23c925e..5a9d06c 100644 --- a/api/v1alpha1/lavinmq_types.go +++ b/api/v1alpha1/lavinmq_types.go @@ -33,6 +33,9 @@ type LavinMQSpec struct { // +optional Image string `json:"image,omitempty"` + // +optional + ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` + // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=3 // +kubebuilder:default=1 diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index a263440..bf975c5 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -138,6 +138,11 @@ func (in *LavinMQList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LavinMQSpec) DeepCopyInto(out *LavinMQSpec) { *out = *in + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]v1.LocalObjectReference, len(*in)) + copy(*out, *in) + } if in.NodeSelector != nil { in, out := &in.NodeSelector, &out.NodeSelector *out = make(map[string]string, len(*in)) diff --git a/config/crd/bases/cloudamqp.com_lavinmqs.yaml b/config/crd/bases/cloudamqp.com_lavinmqs.yaml index 16763f9..9767273 100644 --- a/config/crd/bases/cloudamqp.com_lavinmqs.yaml +++ b/config/crd/bases/cloudamqp.com_lavinmqs.yaml @@ -401,6 +401,24 @@ spec: image: default: cloudamqp/lavinmq:2.4.1 type: string + imagePullSecrets: + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array nodeSelector: additionalProperties: type: string diff --git a/internal/reconciler/statefulset.go b/internal/reconciler/statefulset.go index 1e3eaf7..5121d28 100644 --- a/internal/reconciler/statefulset.go +++ b/internal/reconciler/statefulset.go @@ -192,6 +192,10 @@ func (b *StatefulSetReconciler) appendSpec(sts *appsv1.StatefulSet) *appsv1.Stat }, } + if b.Instance.Spec.ImagePullSecrets != nil { + sts.Spec.Template.Spec.ImagePullSecrets = b.Instance.Spec.ImagePullSecrets + } + return sts } func (b *StatefulSetReconciler) portsFromSpec() []corev1.ContainerPort { From 16929b364259a8d39e7d55d9c63c6af5ff82c834 Mon Sep 17 00:00:00 2001 From: Erkki Eilonen Date: Thu, 16 Jul 2026 13:52:32 +0700 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- config/crd/bases/cloudamqp.com_lavinmqs.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/crd/bases/cloudamqp.com_lavinmqs.yaml b/config/crd/bases/cloudamqp.com_lavinmqs.yaml index 9767273..58ab29c 100644 --- a/config/crd/bases/cloudamqp.com_lavinmqs.yaml +++ b/config/crd/bases/cloudamqp.com_lavinmqs.yaml @@ -419,6 +419,7 @@ spec: type: object x-kubernetes-map-type: atomic type: array + x-kubernetes-list-type: atomic nodeSelector: additionalProperties: type: string From d792f8f7677b981999026ec7e93c395aec77355d Mon Sep 17 00:00:00 2001 From: Erkki Eilonen Date: Thu, 16 Jul 2026 13:56:03 +0700 Subject: [PATCH 3/4] Sync imagePullSecrets on StatefulSet updates Setting imagePullSecrets in appendSpec only affected newly created StatefulSets; diffTemplate never synced the field, so changing or removing spec.imagePullSecrets left existing StatefulSets untouched. Diff it like the other pod template fields and cover the full lifecycle (create, change, remove) with a test. --- internal/reconciler/statefulset.go | 5 +++ internal/reconciler/statefulset_test.go | 58 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/internal/reconciler/statefulset.go b/internal/reconciler/statefulset.go index 5121d28..dd0f006 100644 --- a/internal/reconciler/statefulset.go +++ b/internal/reconciler/statefulset.go @@ -360,6 +360,11 @@ func (b *StatefulSetReconciler) diffTemplate(old *corev1.PodSpec) { old.NodeSelector = b.Instance.Spec.NodeSelector } + if !reflect.DeepEqual(old.ImagePullSecrets, b.Instance.Spec.ImagePullSecrets) { + b.Logger.Info("imagePullSecrets changed, updating") + old.ImagePullSecrets = b.Instance.Spec.ImagePullSecrets + } + index := slices.IndexFunc(old.Volumes, func(v corev1.Volume) bool { return v.Name == "tls" }) diff --git a/internal/reconciler/statefulset_test.go b/internal/reconciler/statefulset_test.go index 661e4e4..fd44cc0 100644 --- a/internal/reconciler/statefulset_test.go +++ b/internal/reconciler/statefulset_test.go @@ -260,3 +260,61 @@ func deleteConfigMap(t *testing.T, configMap *corev1.ConfigMap) { err := k8sClient.Delete(t.Context(), configMap) assert.NoErrorf(t, err, "Failed to delete ConfigMap") } + +func TestImagePullSecrets(t *testing.T) { + t.Parallel() + instance := testutils.GetDefaultInstance(&testutils.DefaultInstanceSettings{}) + instance.Spec.ImagePullSecrets = []corev1.LocalObjectReference{{Name: "registry-creds"}} + + err := testutils.CreateNamespace(t.Context(), k8sClient, instance.Namespace) + assert.NoErrorf(t, err, "Failed to create namespace") + defer testutils.DeleteNamespace(t.Context(), k8sClient, instance.Namespace) + + configMap := createConfigMap(t, instance) + defer deleteConfigMap(t, configMap) + + rc := &reconciler.StatefulSetReconciler{ + ResourceReconciler: &reconciler.ResourceReconciler{ + Instance: instance, + Scheme: scheme.Scheme, + Client: k8sClient, + }, + } + + err = k8sClient.Create(t.Context(), instance) + assert.NoErrorf(t, err, "Failed to create instance") + + // Created StatefulSet carries the pull secrets + _, err = rc.Reconcile(t.Context()) + assert.NoErrorf(t, err, "Failed to reconcile instance") + + sts := &appsv1.StatefulSet{} + stsName := types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace} + err = k8sClient.Get(t.Context(), stsName, sts) + assert.NoErrorf(t, err, "Failed to get statefulset") + assert.Equal(t, []corev1.LocalObjectReference{{Name: "registry-creds"}}, sts.Spec.Template.Spec.ImagePullSecrets) + + // Changing the secret propagates to the existing StatefulSet + instance.Spec.ImagePullSecrets = []corev1.LocalObjectReference{{Name: "other-creds"}} + err = k8sClient.Update(t.Context(), instance) + assert.NoErrorf(t, err, "Failed to update instance") + + _, err = rc.Reconcile(t.Context()) + assert.NoErrorf(t, err, "Failed to reconcile instance") + + err = k8sClient.Get(t.Context(), stsName, sts) + assert.NoErrorf(t, err, "Failed to get statefulset") + assert.Equal(t, []corev1.LocalObjectReference{{Name: "other-creds"}}, sts.Spec.Template.Spec.ImagePullSecrets) + + // Removing the secrets clears them from the StatefulSet + instance.Spec.ImagePullSecrets = nil + err = k8sClient.Update(t.Context(), instance) + assert.NoErrorf(t, err, "Failed to update instance") + + _, err = rc.Reconcile(t.Context()) + assert.NoErrorf(t, err, "Failed to reconcile instance") + + err = k8sClient.Get(t.Context(), stsName, sts) + assert.NoErrorf(t, err, "Failed to get statefulset") + assert.Empty(t, sts.Spec.Template.Spec.ImagePullSecrets) +} From 859213a26cb47cd1fa3426fb6a5ae6a1824ac813 Mon Sep 17 00:00:00 2001 From: Erkki Eilonen Date: Thu, 16 Jul 2026 14:17:28 +0700 Subject: [PATCH 4/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- internal/reconciler/statefulset_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/reconciler/statefulset_test.go b/internal/reconciler/statefulset_test.go index fd44cc0..0e8183e 100644 --- a/internal/reconciler/statefulset_test.go +++ b/internal/reconciler/statefulset_test.go @@ -268,8 +268,7 @@ func TestImagePullSecrets(t *testing.T) { err := testutils.CreateNamespace(t.Context(), k8sClient, instance.Namespace) assert.NoErrorf(t, err, "Failed to create namespace") - defer testutils.DeleteNamespace(t.Context(), k8sClient, instance.Namespace) - + defer func() { _ = testutils.DeleteNamespace(t.Context(), k8sClient, instance.Namespace) }() configMap := createConfigMap(t, instance) defer deleteConfigMap(t, configMap)