Skip to content

Commit 134b45f

Browse files
chore: add unit tests, improve log and code readability (#839)
- Add 'namespace' structured field to the error log in Reconcile when a role reconciliation fails - Rename misleading local variable 'role' to 'roleBinding' in ensureRoleBinding to match the actual type - Add EnsureRole tests: transient Role creation error is propagated; pre-existing unrelated labels are preserved after patch - Add SetControllerReference test: returns an error when the owner does not implement runtime.Object - Add ObjectStoreReconciler tests: Role list failure and ObjectStore Get transient error both surface through the reconcile return value - Add scheme tests: AddCNPGToScheme with default and custom group/version Assisted-by: Claude Opus 4.6 Signed-off-by: Gabriele Quaresima <gabriele.quaresima@enterprisedb.com> Signed-off-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com> Co-authored-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com>
1 parent 4c7eb5e commit 134b45f

6 files changed

Lines changed: 264 additions & 2 deletions

File tree

internal/cnpgi/operator/rbac/ensure_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,21 @@ package rbac_test
2121

2222
import (
2323
"context"
24+
"fmt"
2425

2526
barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api"
2627
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
2728
machineryapi "github.com/cloudnative-pg/machinery/pkg/api"
2829
. "github.com/onsi/ginkgo/v2"
2930
. "github.com/onsi/gomega"
3031
rbacv1 "k8s.io/api/rbac/v1"
32+
apierrs "k8s.io/apimachinery/pkg/api/errors"
3133
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3234
"k8s.io/apimachinery/pkg/runtime"
3335
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
3436
"sigs.k8s.io/controller-runtime/pkg/client"
3537
"sigs.k8s.io/controller-runtime/pkg/client/fake"
38+
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
3639

3740
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
3841
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata"
@@ -179,6 +182,56 @@ var _ = Describe("EnsureRole", func() {
179182
})
180183
})
181184

185+
Context("when Role creation fails with a transient error", func() {
186+
BeforeEach(func() {
187+
internalErr := apierrs.NewInternalError(fmt.Errorf("etcd timeout"))
188+
fakeClient = fake.NewClientBuilder().
189+
WithScheme(newScheme()).
190+
WithInterceptorFuncs(interceptor.Funcs{
191+
Create: func(ctx context.Context, c client.WithWatch, obj client.Object, opts ...client.CreateOption) error {
192+
return internalErr
193+
},
194+
}).
195+
Build()
196+
})
197+
198+
It("should propagate the error", func() {
199+
err := rbac.EnsureRole(ctx, fakeClient, cluster, objects)
200+
Expect(err).To(HaveOccurred())
201+
Expect(apierrs.IsInternalError(err)).To(BeTrue())
202+
})
203+
})
204+
205+
Context("when the Role has pre-existing unrelated labels", func() {
206+
BeforeEach(func() {
207+
fakeClient = fake.NewClientBuilder().WithScheme(newScheme()).Build()
208+
existing := &rbacv1.Role{
209+
ObjectMeta: metav1.ObjectMeta{
210+
Name: "test-cluster-barman-cloud",
211+
Namespace: "default",
212+
Labels: map[string]string{
213+
"app.kubernetes.io/managed-by": "helm",
214+
},
215+
},
216+
}
217+
Expect(fakeClient.Create(ctx, existing)).To(Succeed())
218+
})
219+
220+
It("should preserve unrelated labels while adding the cluster label", func() {
221+
err := rbac.EnsureRole(ctx, fakeClient, cluster, objects)
222+
Expect(err).NotTo(HaveOccurred())
223+
224+
var role rbacv1.Role
225+
Expect(fakeClient.Get(ctx, client.ObjectKey{
226+
Namespace: "default",
227+
Name: "test-cluster-barman-cloud",
228+
}, &role)).To(Succeed())
229+
230+
Expect(role.Labels).To(HaveKeyWithValue("app.kubernetes.io/managed-by", "helm"))
231+
Expect(role.Labels).To(HaveKeyWithValue(metadata.ClusterLabelName, "test-cluster"))
232+
})
233+
})
234+
182235
Context("when the Role exists without the cluster label (upgrade scenario)", func() {
183236
BeforeEach(func() {
184237
fakeClient = fake.NewClientBuilder().WithScheme(newScheme()).Build()

internal/cnpgi/operator/reconciler.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,11 @@ func (r ReconcilerImplementation) ensureRoleBinding(
141141
ctx context.Context,
142142
cluster *cnpgv1.Cluster,
143143
) error {
144-
var role rbacv1.RoleBinding
144+
var roleBinding rbacv1.RoleBinding
145145
if err := r.Client.Get(ctx, client.ObjectKey{
146146
Namespace: cluster.Namespace,
147147
Name: specs.GetRBACName(cluster.Name),
148-
}, &role); err != nil {
148+
}, &roleBinding); err != nil {
149149
if apierrs.IsNotFound(err) {
150150
return r.createRoleBinding(ctx, cluster)
151151
}

internal/cnpgi/operator/specs/ownership_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,14 @@ var _ = Describe("SetControllerReference", func() {
8787
Expect(err).To(HaveOccurred())
8888
Expect(err.Error()).To(ContainSubstring("has no GVK set"))
8989
})
90+
91+
It("should fail when the owner does not implement runtime.Object", func() {
92+
// metav1.ObjectMeta satisfies metav1.Object but not runtime.Object.
93+
owner := &metav1.ObjectMeta{Name: "my-cluster"}
94+
controlled := &rbacv1.Role{}
95+
96+
err := SetControllerReference(owner, controlled)
97+
Expect(err).To(HaveOccurred())
98+
Expect(err.Error()).To(ContainSubstring("is not a runtime.Object"))
99+
})
90100
})

internal/controller/objectstore_controller_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,21 @@ package controller
2121

2222
import (
2323
"context"
24+
"fmt"
2425

2526
barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api"
2627
machineryapi "github.com/cloudnative-pg/machinery/pkg/api"
2728
. "github.com/onsi/ginkgo/v2"
2829
. "github.com/onsi/gomega"
2930
rbacv1 "k8s.io/api/rbac/v1"
31+
apierrs "k8s.io/apimachinery/pkg/api/errors"
3032
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3133
"k8s.io/apimachinery/pkg/runtime"
3234
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
3335
"k8s.io/apimachinery/pkg/types"
3436
"sigs.k8s.io/controller-runtime/pkg/client"
3537
"sigs.k8s.io/controller-runtime/pkg/client/fake"
38+
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
3639
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3740

3841
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
@@ -294,6 +297,54 @@ var _ = Describe("ObjectStoreReconciler", func() {
294297
Expect(updatedRole.Rules[2].ResourceNames).To(BeEmpty())
295298
})
296299

300+
It("should return an error when listing Roles fails", func() {
301+
internalErr := apierrs.NewInternalError(fmt.Errorf("etcd timeout"))
302+
fakeClient := fake.NewClientBuilder().
303+
WithScheme(scheme).
304+
WithInterceptorFuncs(interceptor.Funcs{
305+
List: func(ctx context.Context, c client.WithWatch, list client.ObjectList, opts ...client.ListOption) error {
306+
if _, ok := list.(*rbacv1.RoleList); ok {
307+
return internalErr
308+
}
309+
return c.List(ctx, list, opts...)
310+
},
311+
}).
312+
Build()
313+
314+
reconciler := &ObjectStoreReconciler{Client: fakeClient, Scheme: scheme}
315+
_, err := reconciler.Reconcile(ctx, reconcile.Request{
316+
NamespacedName: types.NamespacedName{Name: "my-store", Namespace: "default"},
317+
})
318+
Expect(err).To(HaveOccurred())
319+
Expect(err.Error()).To(ContainSubstring("while listing roles"))
320+
})
321+
322+
It("should return an error when fetching an ObjectStore fails with a transient error", func() {
323+
store := newTestObjectStore("my-store", "default", "aws-creds")
324+
role := newLabeledRole("my-cluster", "default", []barmancloudv1.ObjectStore{*store})
325+
326+
internalErr := apierrs.NewInternalError(fmt.Errorf("etcd timeout"))
327+
fakeClient := fake.NewClientBuilder().
328+
WithScheme(scheme).
329+
WithObjects(role).
330+
WithInterceptorFuncs(interceptor.Funcs{
331+
Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
332+
if _, ok := obj.(*barmancloudv1.ObjectStore); ok {
333+
return internalErr
334+
}
335+
return c.Get(ctx, key, obj, opts...)
336+
},
337+
}).
338+
Build()
339+
340+
reconciler := &ObjectStoreReconciler{Client: fakeClient, Scheme: scheme}
341+
_, err := reconciler.Reconcile(ctx, reconcile.Request{
342+
NamespacedName: types.NamespacedName{Name: "my-store", Namespace: "default"},
343+
})
344+
Expect(err).To(HaveOccurred())
345+
Expect(err.Error()).To(ContainSubstring("while reconciling role"))
346+
})
347+
297348
It("should reconcile multiple Roles referencing the same ObjectStore", func() {
298349
store := newTestObjectStore("shared-store", "default", "new-secret")
299350
oldStore := barmancloudv1.ObjectStore{

internal/scheme/cnpg_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
Copyright © contributors to CloudNativePG, established as
3+
CloudNativePG a Series of LF Projects, LLC.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
17+
SPDX-License-Identifier: Apache-2.0
18+
*/
19+
20+
package scheme
21+
22+
import (
23+
"context"
24+
25+
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
26+
. "github.com/onsi/ginkgo/v2"
27+
. "github.com/onsi/gomega"
28+
"github.com/spf13/viper"
29+
"k8s.io/apimachinery/pkg/runtime"
30+
"k8s.io/apimachinery/pkg/runtime/schema"
31+
)
32+
33+
var _ = Describe("AddCNPGToScheme", func() {
34+
var s *runtime.Scheme
35+
36+
BeforeEach(func() {
37+
s = runtime.NewScheme()
38+
})
39+
40+
AfterEach(func() {
41+
viper.Reset()
42+
})
43+
44+
It("should register CNPG types under the default group and version", func() {
45+
AddCNPGToScheme(context.Background(), s)
46+
47+
gvks, _, err := s.ObjectKinds(&cnpgv1.Cluster{})
48+
Expect(err).NotTo(HaveOccurred())
49+
Expect(gvks).To(ContainElement(schema.GroupVersionKind{
50+
Group: cnpgv1.SchemeGroupVersion.Group,
51+
Version: cnpgv1.SchemeGroupVersion.Version,
52+
Kind: "Cluster",
53+
}))
54+
})
55+
56+
It("should register Backup and ScheduledBackup under the default group", func() {
57+
AddCNPGToScheme(context.Background(), s)
58+
59+
gvks, _, err := s.ObjectKinds(&cnpgv1.Backup{})
60+
Expect(err).NotTo(HaveOccurred())
61+
Expect(gvks).To(ContainElement(HaveField("Group", cnpgv1.SchemeGroupVersion.Group)))
62+
63+
gvks, _, err = s.ObjectKinds(&cnpgv1.ScheduledBackup{})
64+
Expect(err).NotTo(HaveOccurred())
65+
Expect(gvks).To(ContainElement(HaveField("Group", cnpgv1.SchemeGroupVersion.Group)))
66+
})
67+
68+
It("should register CNPG types under a custom group", func() {
69+
viper.Set("custom-cnpg-group", "mycompany.io")
70+
71+
AddCNPGToScheme(context.Background(), s)
72+
73+
gvks, _, err := s.ObjectKinds(&cnpgv1.Cluster{})
74+
Expect(err).NotTo(HaveOccurred())
75+
Expect(gvks).To(ContainElement(schema.GroupVersionKind{
76+
Group: "mycompany.io",
77+
Version: cnpgv1.SchemeGroupVersion.Version,
78+
Kind: "Cluster",
79+
}))
80+
// The default group must not be registered
81+
Expect(s.Recognizes(schema.GroupVersionKind{
82+
Group: cnpgv1.SchemeGroupVersion.Group,
83+
Version: cnpgv1.SchemeGroupVersion.Version,
84+
Kind: "Cluster",
85+
})).To(BeFalse())
86+
})
87+
88+
It("should register CNPG types under a custom version", func() {
89+
viper.Set("custom-cnpg-version", "v2")
90+
91+
AddCNPGToScheme(context.Background(), s)
92+
93+
gvks, _, err := s.ObjectKinds(&cnpgv1.Cluster{})
94+
Expect(err).NotTo(HaveOccurred())
95+
Expect(gvks).To(ContainElement(schema.GroupVersionKind{
96+
Group: cnpgv1.SchemeGroupVersion.Group,
97+
Version: "v2",
98+
Kind: "Cluster",
99+
}))
100+
})
101+
102+
It("should register CNPG types under both a custom group and custom version", func() {
103+
viper.Set("custom-cnpg-group", "mycompany.io")
104+
viper.Set("custom-cnpg-version", "v2")
105+
106+
AddCNPGToScheme(context.Background(), s)
107+
108+
gvks, _, err := s.ObjectKinds(&cnpgv1.Cluster{})
109+
Expect(err).NotTo(HaveOccurred())
110+
Expect(gvks).To(ContainElement(schema.GroupVersionKind{
111+
Group: "mycompany.io",
112+
Version: "v2",
113+
Kind: "Cluster",
114+
}))
115+
})
116+
})

internal/scheme/suite_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
Copyright © contributors to CloudNativePG, established as
3+
CloudNativePG a Series of LF Projects, LLC.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
17+
SPDX-License-Identifier: Apache-2.0
18+
*/
19+
20+
package scheme
21+
22+
import (
23+
"testing"
24+
25+
. "github.com/onsi/ginkgo/v2"
26+
. "github.com/onsi/gomega"
27+
)
28+
29+
func TestScheme(t *testing.T) {
30+
RegisterFailHandler(Fail)
31+
RunSpecs(t, "Scheme Suite")
32+
}

0 commit comments

Comments
 (0)