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..58ab29c 100644 --- a/config/crd/bases/cloudamqp.com_lavinmqs.yaml +++ b/config/crd/bases/cloudamqp.com_lavinmqs.yaml @@ -401,6 +401,25 @@ 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 + x-kubernetes-list-type: atomic nodeSelector: additionalProperties: type: string diff --git a/internal/reconciler/statefulset.go b/internal/reconciler/statefulset.go index 1e3eaf7..dd0f006 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 { @@ -356,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..0e8183e 100644 --- a/internal/reconciler/statefulset_test.go +++ b/internal/reconciler/statefulset_test.go @@ -260,3 +260,60 @@ 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 func() { _ = 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) +}