Skip to content

Commit ecf4af6

Browse files
authored
Merge pull request #1796 from splunk/jkoterba/feature/postgres-cluster-tests
pgcluster integration tests
2 parents 8b9b538 + 45545ba commit ecf4af6

2 files changed

Lines changed: 294 additions & 40 deletions

File tree

internal/controller/postgrescluster_controller_test.go

Lines changed: 290 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,67 +18,319 @@ package controller
1818

1919
import (
2020
"context"
21+
"fmt"
2122

23+
apierrors "k8s.io/apimachinery/pkg/api/errors"
24+
"k8s.io/apimachinery/pkg/api/meta"
25+
"k8s.io/apimachinery/pkg/api/resource"
26+
"k8s.io/client-go/tools/record"
27+
28+
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
2229
. "github.com/onsi/ginkgo/v2"
2330
. "github.com/onsi/gomega"
24-
"k8s.io/apimachinery/pkg/api/errors"
2531
"k8s.io/apimachinery/pkg/types"
32+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
2633
"sigs.k8s.io/controller-runtime/pkg/reconcile"
2734

2835
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2936

3037
enterprisev4 "github.com/splunk/splunk-operator/api/v4"
38+
"github.com/splunk/splunk-operator/pkg/postgresql/cluster/core"
3139
)
3240

41+
/*
42+
* Test cases:
43+
* PC-01 creates managed resources and status refs
44+
* PC-02 adds finalizer on reconcile
45+
* PC-07 is idempotent across repeated reconciles
46+
* PC-03 Delete policy removes children and finalizer
47+
* PC-04 Retain policy preserves children and removes ownerRefs
48+
* PC-05 fails when PostgresClusterClass is missing
49+
* PC-06 restores drifted managed spec
50+
* PC-08 triggers on generation/finalizer/deletion changes
51+
* PC-09 ignores no-op updates
52+
*/
53+
3354
var _ = Describe("PostgresCluster Controller", func() {
34-
Context("When reconciling a resource", func() {
35-
const resourceName = "test-resource"
3655

37-
ctx := context.Background()
56+
const (
57+
postgresVersion = "15.10"
58+
clusterMemberCount = int32(2)
59+
storageAmount = "1Gi"
60+
poolerEnabled = false
61+
deletePolicy = "Delete"
62+
retainPolicy = "Retain"
63+
namespace = "default"
64+
classNamePrefix = "postgresql-dev-"
65+
clusterNamePrefix = "postgresql-cluster-dev-"
66+
provisioner = "postgresql.cnpg.io"
67+
)
68+
69+
var (
70+
ctx context.Context
71+
clusterName string
72+
className string
73+
pgCluster *enterprisev4.PostgresCluster
74+
pgClusterClass *enterprisev4.PostgresClusterClass
75+
pgClusterKey types.NamespacedName
76+
pgClusterClassKey types.NamespacedName
77+
reconciler *PostgresClusterReconciler
78+
req reconcile.Request
79+
)
80+
81+
reconcileNTimes := func(times int) {
82+
for i := 0; i < times; i++ {
83+
_, err := reconciler.Reconcile(ctx, req)
84+
Expect(err).NotTo(HaveOccurred())
85+
}
86+
}
87+
88+
BeforeEach(func() {
89+
nameSuffix := fmt.Sprintf("%d-%d-%d",
90+
GinkgoParallelProcess(),
91+
GinkgoRandomSeed(),
92+
CurrentSpecReport().LeafNodeLocation.LineNumber,
93+
)
94+
95+
ctx = context.Background()
96+
clusterName = clusterNamePrefix + nameSuffix
97+
className = classNamePrefix + nameSuffix
98+
pgClusterKey = types.NamespacedName{Name: clusterName, Namespace: namespace}
99+
pgClusterClassKey = types.NamespacedName{Name: className, Namespace: namespace}
100+
101+
pgClusterClass = &enterprisev4.PostgresClusterClass{
102+
ObjectMeta: metav1.ObjectMeta{Name: className},
103+
Spec: enterprisev4.PostgresClusterClassSpec{
104+
Provisioner: provisioner,
105+
Config: &enterprisev4.PostgresClusterClassConfig{
106+
Instances: &[]int32{clusterMemberCount}[0],
107+
Storage: &[]resource.Quantity{resource.MustParse(storageAmount)}[0],
108+
PostgresVersion: &[]string{postgresVersion}[0],
109+
ConnectionPoolerEnabled: &[]bool{poolerEnabled}[0],
110+
},
111+
},
112+
}
113+
114+
Expect(k8sClient.Create(ctx, pgClusterClass)).To(Succeed())
115+
116+
pgCluster = &enterprisev4.PostgresCluster{
117+
ObjectMeta: metav1.ObjectMeta{Name: clusterName, Namespace: namespace},
118+
Spec: enterprisev4.PostgresClusterSpec{
119+
Class: className,
120+
ClusterDeletionPolicy: &[]string{deletePolicy}[0],
121+
},
122+
}
123+
124+
reconciler = &PostgresClusterReconciler{
125+
Client: k8sClient,
126+
Scheme: k8sClient.Scheme(),
127+
Recorder: record.NewFakeRecorder(100),
128+
}
129+
req = reconcile.Request{NamespacedName: types.NamespacedName{Name: clusterName, Namespace: namespace}}
130+
})
131+
132+
AfterEach(func() {
133+
By("Deleting PostgresCluster and letting reconcile run finalizer cleanup")
38134

39-
typeNamespacedName := types.NamespacedName{
40-
Name: resourceName,
41-
Namespace: "default", // TODO(user):Modify as needed
135+
// Best-effort delete (object might already be gone in some specs)
136+
err := k8sClient.Get(ctx, pgClusterKey, pgCluster)
137+
if err == nil {
138+
Expect(k8sClient.Delete(ctx, pgCluster)).To(Succeed())
139+
} else {
140+
Expect(apierrors.IsNotFound(err)).To(BeTrue())
42141
}
43-
postgresCluster := &enterprisev4.PostgresCluster{}
44-
45-
BeforeEach(func() {
46-
By("creating the custom resource for the Kind PostgresCluster")
47-
err := k8sClient.Get(ctx, typeNamespacedName, postgresCluster)
48-
if err != nil && errors.IsNotFound(err) {
49-
resource := &enterprisev4.PostgresCluster{
50-
ObjectMeta: metav1.ObjectMeta{
51-
Name: resourceName,
52-
Namespace: "default",
53-
},
54-
// TODO(user): Specify other spec details if needed.
142+
143+
// Drive delete reconcile path until finalizer is removed and object disappears
144+
Eventually(func() bool {
145+
_, recErr := reconciler.Reconcile(ctx, req)
146+
if recErr != nil {
147+
// Some envtest runs may not have CNPG CRDs installed in the API server.
148+
// In that case, remove finalizer directly so fixture teardown remains deterministic.
149+
if meta.IsNoMatchError(recErr) {
150+
current := &enterprisev4.PostgresCluster{}
151+
getErr := k8sClient.Get(ctx, pgClusterKey, current)
152+
if apierrors.IsNotFound(getErr) {
153+
return true
154+
}
155+
if getErr != nil {
156+
return false
157+
}
158+
controllerutil.RemoveFinalizer(current, core.PostgresClusterFinalizerName)
159+
if err := k8sClient.Update(ctx, current); err != nil && !apierrors.IsNotFound(err) {
160+
return false
161+
}
162+
if err := k8sClient.Delete(ctx, current); err != nil && !apierrors.IsNotFound(err) {
163+
return false
164+
}
165+
} else {
166+
return false
55167
}
56-
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
57168
}
169+
getErr := k8sClient.Get(ctx, pgClusterKey, &enterprisev4.PostgresCluster{})
170+
return apierrors.IsNotFound(getErr)
171+
}, "10s", "500ms").Should(BeTrue())
172+
173+
By("Cleaning up PostgresClusterClass fixture")
174+
err = k8sClient.Get(ctx, pgClusterClassKey, pgClusterClass)
175+
if err == nil {
176+
Expect(k8sClient.Delete(ctx, pgClusterClass)).To(Succeed())
177+
} else {
178+
Expect(apierrors.IsNotFound(err)).To(BeTrue())
179+
}
180+
})
181+
182+
When("under typical usage and expecting healthy PostgresCluster state", func() {
183+
Context("when reconciling", func() {
184+
// PC-02
185+
It("adds finalizer on reconcile", func() {
186+
Expect(k8sClient.Create(ctx, pgCluster)).To(Succeed())
187+
reconcileNTimes(1)
188+
189+
pc := &enterprisev4.PostgresCluster{}
190+
Expect(k8sClient.Get(ctx, pgClusterKey, pc)).To(Succeed())
191+
Expect(controllerutil.ContainsFinalizer(pc, core.PostgresClusterFinalizerName)).To(BeTrue())
192+
})
193+
194+
// PC-01
195+
It("creates managed resources and status refs", func() {
196+
Expect(k8sClient.Create(ctx, pgCluster)).To(Succeed())
197+
// pass 1: add finalizer; pass 2: create CNPG cluster/secret/status.
198+
reconcileNTimes(2)
199+
200+
pc := &enterprisev4.PostgresCluster{}
201+
Expect(k8sClient.Get(ctx, pgClusterKey, pc)).To(Succeed())
202+
cond := meta.FindStatusCondition(pc.Status.Conditions, "ClusterReady")
203+
Expect(cond).NotTo(BeNil())
204+
Expect(cond.Status).To(Equal(metav1.ConditionFalse))
205+
Expect(cond.Reason).To(Equal("ClusterBuildSucceeded"))
206+
207+
// Simulate external CNPG controller status progression.
208+
cnpg := &cnpgv1.Cluster{}
209+
Expect(k8sClient.Get(ctx, pgClusterKey, cnpg)).To(Succeed())
210+
cnpg.Status.Phase = cnpgv1.PhaseHealthy
211+
Expect(k8sClient.Status().Update(ctx, cnpg)).To(Succeed())
212+
reconcileNTimes(1)
213+
214+
// Expect cnpg status progression propagation
215+
Expect(k8sClient.Get(ctx, pgClusterKey, pc)).To(Succeed())
216+
cond = meta.FindStatusCondition(pc.Status.Conditions, "ClusterReady")
217+
Expect(cond).NotTo(BeNil())
218+
Expect(cond.Status).To(Equal(metav1.ConditionTrue))
219+
Expect(cond.Reason).To(Equal("CNPGClusterHealthy"))
220+
Expect(pc.Status.Resources).NotTo(BeNil())
221+
Expect(pc.Status.Resources.SuperUserSecretRef).NotTo(BeNil())
222+
Expect(pc.Status.Resources.ConfigMapRef).NotTo(BeNil())
223+
})
224+
225+
// PC-07
226+
It("is idempotent across repeated reconciles", func() {
227+
Expect(k8sClient.Create(ctx, pgCluster)).To(Succeed())
228+
reconcileNTimes(2)
229+
reconcileNTimes(3)
230+
231+
cnpg := &cnpgv1.Cluster{}
232+
Expect(k8sClient.Get(ctx, pgClusterKey, cnpg)).To(Succeed())
233+
Expect(cnpg.Spec.Instances).To(Equal(int(clusterMemberCount)))
234+
235+
pc := &enterprisev4.PostgresCluster{}
236+
Expect(k8sClient.Get(ctx, pgClusterKey, pc)).To(Succeed())
237+
cond := meta.FindStatusCondition(pc.Status.Conditions, "ClusterReady")
238+
Expect(cond).NotTo(BeNil())
239+
Expect(cond.ObservedGeneration).To(Equal(pc.Generation))
240+
})
58241
})
242+
})
59243

60-
AfterEach(func() {
61-
// TODO(user): Cleanup logic after each test, like removing the resource instance.
62-
resource := &enterprisev4.PostgresCluster{}
63-
err := k8sClient.Get(ctx, typeNamespacedName, resource)
64-
Expect(err).NotTo(HaveOccurred())
244+
When("deleting a PostgresCluster", func() {
245+
// PC-03
246+
Context("and clusterDeletionPolicy is set to Delete", func() {
247+
It("removes children and finalizer", func() {
248+
Expect(k8sClient.Create(ctx, pgCluster)).To(Succeed())
249+
reconcileNTimes(2)
250+
251+
pc := &enterprisev4.PostgresCluster{}
252+
Expect(k8sClient.Get(ctx, pgClusterKey, pc)).To(Succeed())
253+
Expect(k8sClient.Delete(ctx, pc)).To(Succeed())
65254

66-
By("Cleanup the specific resource instance PostgresCluster")
67-
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
255+
Eventually(func() bool {
256+
_, err := reconciler.Reconcile(ctx, req)
257+
if err != nil {
258+
return false
259+
}
260+
getErr := k8sClient.Get(ctx, pgClusterKey, &enterprisev4.PostgresCluster{})
261+
return apierrors.IsNotFound(getErr)
262+
}, "30s", "250ms").Should(BeTrue())
263+
})
68264
})
69-
It("should successfully reconcile the resource", func() {
70-
By("Reconciling the created resource")
71-
controllerReconciler := &PostgresClusterReconciler{
72-
Client: k8sClient,
73-
Scheme: k8sClient.Scheme(),
74-
}
75265

76-
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
77-
NamespacedName: typeNamespacedName,
266+
// PC-04
267+
Context("when clusterDeletionPolicy is set to Retain", func() {
268+
It("preserves retained resources and removes owner refs", func() {
269+
Expect(k8sClient.Create(ctx, pgCluster)).To(Succeed())
270+
reconcileNTimes(2)
271+
272+
pc := &enterprisev4.PostgresCluster{}
273+
Expect(k8sClient.Get(ctx, pgClusterKey, pc)).To(Succeed())
274+
Expect(k8sClient.Delete(ctx, pc)).To(Succeed())
275+
276+
Eventually(func() bool {
277+
_, err := reconciler.Reconcile(ctx, req)
278+
if err != nil {
279+
return false
280+
}
281+
getErr := k8sClient.Get(ctx, pgClusterKey, &enterprisev4.PostgresCluster{})
282+
return apierrors.IsNotFound(getErr)
283+
}, "30s", "250ms").Should(BeTrue())
284+
})
285+
})
286+
})
287+
288+
When("reconciling with invalid or drifted dependencies", func() {
289+
// PC-05
290+
Context("when referenced class does not exist", func() {
291+
It("fails with class-not-found condition", func() {
292+
badName := "bad-" + clusterName
293+
badKey := types.NamespacedName{Name: badName, Namespace: namespace}
294+
295+
bad := &enterprisev4.PostgresCluster{
296+
ObjectMeta: metav1.ObjectMeta{Name: badName, Namespace: namespace},
297+
Spec: enterprisev4.PostgresClusterSpec{Class: "missing-class"},
298+
}
299+
Expect(k8sClient.Create(ctx, bad)).To(Succeed())
300+
DeferCleanup(func() { _ = k8sClient.Delete(ctx, bad) })
301+
302+
// pass 1 adds finalizer, pass 2 reaches class lookup and sets failure condition.
303+
_, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: badKey})
304+
Expect(err).NotTo(HaveOccurred())
305+
_, err = reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: badKey})
306+
Expect(err).To(HaveOccurred())
307+
308+
Eventually(func() bool {
309+
current := &enterprisev4.PostgresCluster{}
310+
if err := k8sClient.Get(ctx, badKey, current); err != nil {
311+
return false
312+
}
313+
cond := meta.FindStatusCondition(current.Status.Conditions, "ClusterReady")
314+
return cond != nil && cond.Reason == "ClusterClassNotFound"
315+
}, "20s", "250ms").Should(BeTrue())
316+
})
317+
})
318+
319+
// PC-06
320+
Context("when managed child spec drifts from desired state", func() {
321+
It("restores drifted managed spec", func() {
322+
Expect(k8sClient.Create(ctx, pgCluster)).To(Succeed())
323+
reconcileNTimes(2)
324+
325+
cnpg := &cnpgv1.Cluster{}
326+
Expect(k8sClient.Get(ctx, pgClusterKey, cnpg)).To(Succeed())
327+
cnpg.Spec.Instances = 8
328+
Expect(k8sClient.Update(ctx, cnpg)).To(Succeed())
329+
330+
reconcileNTimes(2)
331+
Expect(k8sClient.Get(ctx, pgClusterKey, cnpg)).To(Succeed())
332+
Expect(cnpg.Spec.Instances).To(Equal(int(clusterMemberCount)))
78333
})
79-
Expect(err).NotTo(HaveOccurred())
80-
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
81-
// Example: If you expect a certain status condition after reconciliation, verify it here.
82334
})
83335
})
84336
})

internal/controller/postgresdatabase_controller_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3333
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3434
"k8s.io/apimachinery/pkg/types"
35+
"k8s.io/client-go/tools/record"
3536
ctrl "sigs.k8s.io/controller-runtime"
3637
"sigs.k8s.io/controller-runtime/pkg/client"
3738
"sigs.k8s.io/controller-runtime/pkg/reconcile"
@@ -41,8 +42,9 @@ const postgresDatabaseFinalizer = "postgresdatabases.enterprise.splunk.com/final
4142

4243
func reconcilePostgresDatabase(ctx context.Context, nn types.NamespacedName) (ctrl.Result, error) {
4344
reconciler := &PostgresDatabaseReconciler{
44-
Client: k8sClient,
45-
Scheme: k8sClient.Scheme(),
45+
Client: k8sClient,
46+
Scheme: k8sClient.Scheme(),
47+
Recorder: record.NewFakeRecorder(100),
4648
}
4749
return reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: nn})
4850
}

0 commit comments

Comments
 (0)