diff --git a/internal/controller/postgrescluster/pgbackrest.go b/internal/controller/postgrescluster/pgbackrest.go index 89778331ab..41bddab7df 100644 --- a/internal/controller/postgrescluster/pgbackrest.go +++ b/internal/controller/postgrescluster/pgbackrest.go @@ -1475,6 +1475,15 @@ func (r *Reconciler) reconcilePGBackRest(ctx context.Context, return result, nil } + // reconcile the RBAC required to run pgBackRest Jobs (e.g. for backups) + // K8SPG-698 + sa, err := r.reconcilePGBackRestRBAC(ctx, postgresCluster) + if err != nil { + log.Error(err, "unable to create replica creation backup") + result.Requeue = true + return result, nil + } + var repoHost *appsv1.StatefulSet var repoHostName string // reconcile the pgbackrest repository host @@ -1523,14 +1532,6 @@ func (r *Reconciler) reconcilePGBackRest(ctx context.Context, result.Requeue = true } - // reconcile the RBAC required to run pgBackRest Jobs (e.g. for backups) - sa, err := r.reconcilePGBackRestRBAC(ctx, postgresCluster) - if err != nil { - log.Error(err, "unable to create replica creation backup") - result.Requeue = true - return result, nil - } - // reconcile the pgBackRest stanza for all configuration pgBackRest repos configHashMismatch, err := r.reconcileStanzaCreate(ctx, postgresCluster, instances, configHash) // If a stanza create error then requeue but don't return the error. This prevents diff --git a/percona/controller/pgcluster/controller_test.go b/percona/controller/pgcluster/controller_test.go index 8909485abe..74689b22cb 100644 --- a/percona/controller/pgcluster/controller_test.go +++ b/percona/controller/pgcluster/controller_test.go @@ -1623,6 +1623,114 @@ var _ = Describe("Validate TLS", Ordered, func() { }) }) +type saTestClient struct { + client.Client + + crName string + ns string +} + +func (sc *saTestClient) checkObject(ctx context.Context, obj client.Object) error { + sts, ok := obj.(*appsv1.StatefulSet) + if !ok { + return nil + } + serviceAccountName := sts.Spec.Template.Spec.ServiceAccountName + if serviceAccountName == "" { + return errors.New("it's not expected to have empty service account name") + } + + if err := sc.Get(ctx, types.NamespacedName{ + Name: serviceAccountName, + Namespace: sts.Namespace, + }, new(corev1.ServiceAccount)); err != nil { + if k8serrors.IsNotFound(err) { + return errors.Wrap(err, "test error: service account should be created before the statefulset") + } + return err + } + + return nil +} + +func (sc *saTestClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { + if err := sc.checkObject(ctx, obj); err != nil { + panic(err) // should panic because reconciler can ignore the error + } + return sc.Client.Patch(ctx, obj, patch, opts...) +} + +func (sc *saTestClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { + if err := sc.checkObject(ctx, obj); err != nil { + panic(err) // should panic because reconciler can ignore the error + } + return sc.Client.Create(ctx, obj, opts...) +} + +// This test ensures that the ServiceAccount associated with a StatefulSet is created +// before the StatefulSet itself. (K8SPG-698) +// The saTestClient verifies the existence of the ServiceAccount during create, patch, +// or update operations on the StatefulSet. +var _ = Describe("ServiceAccount early creation", Ordered, func() { + ctx := context.Background() + + const crName = "sa-timestamp" + const ns = crName + + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: crName, + Namespace: ns, + }, + } + crNamespacedName := types.NamespacedName{Name: crName, Namespace: ns} + + cr, err := readDefaultCR(crName, ns) + It("should read default cr.yaml", func() { + Expect(err).NotTo(HaveOccurred()) + }) + + cr.Default() + reconciler := reconciler(cr) + crunchyReconciler := crunchyReconciler() + + var cl client.Client + + BeforeAll(func() { + cl = &saTestClient{ + Client: k8sClient, + + crName: crName, + ns: ns, + } + reconciler.Client = cl + crunchyReconciler.Client = cl + + By("Creating the Namespace to perform the tests") + err := cl.Create(ctx, namespace) + Expect(err).To(Not(HaveOccurred())) + }) + + AfterAll(func() { + By("Deleting the Namespace to perform the tests") + _ = cl.Delete(ctx, namespace) + }) + + It("should create PerconaPGCluster", func() { + status := cr.Status + Expect(cl.Create(ctx, cr)).Should(Succeed()) + cr.Status = status + Expect(cl.Status().Update(ctx, cr)).Should(Succeed()) + }) + + It("Should reconcile", func() { + _, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: crNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + _, err = crunchyReconciler.Reconcile(ctx, ctrl.Request{NamespacedName: crNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + }) +}) + var _ = Describe("CR Validations", Ordered, func() { ctx := context.Background() const crName = "cr-validation"