Skip to content

Commit b5957b6

Browse files
authored
dryrun: add support for namespace filtering (#2142)
1 parent 0a8bce3 commit b5957b6

File tree

3 files changed

+158
-55
lines changed

3 files changed

+158
-55
lines changed

internal/dryrun/manager.go

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,22 @@ type Manager struct {
6060
logger *zap.Logger
6161
instanceUID string
6262
eventsClient corev1client.EventsGetter
63+
namespaces []string
6364
}
6465

65-
func NewManager(c cluster.Cluster, eventsClient corev1client.EventsGetter, logger *zap.Logger) (*Manager, error) {
66+
func NewManager(c cluster.Cluster, eventsClient corev1client.EventsGetter, logger *zap.Logger, namespaces []string) (*Manager, error) {
6667
mgr := &Manager{
6768
Cluster: c,
6869
logger: logger.Named("dry-run-manager"),
6970
instanceUID: uuid.New().String(),
7071
eventsClient: eventsClient,
72+
namespaces: []string{metav1.NamespaceAll},
7173
}
74+
75+
if len(namespaces) > 0 {
76+
mgr.namespaces = namespaces
77+
}
78+
7279
return mgr, nil
7380
}
7481

@@ -154,25 +161,27 @@ func (m *Manager) dryRunReconcilers(ctx context.Context) error {
154161
list := &unstructured.UnstructuredList{}
155162
list.SetGroupVersionKind(schema.GroupVersionKind{Group: gvk.Group, Version: gvk.Version, Kind: gvk.Kind + "List"})
156163

157-
if err := m.Cluster.GetClient().List(ctx, list); err != nil {
158-
return fmt.Errorf("unable to list resources: %w", err)
159-
}
160-
161-
for _, item := range list.Items {
162-
req := reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&item)}
163-
if err := m.Cluster.GetScheme().Convert(&item, resource, nil); err != nil {
164-
return fmt.Errorf("unable to convert item %T: %w", item, err)
164+
for _, namespace := range m.namespaces {
165+
if err := m.Cluster.GetClient().List(ctx, list, client.InNamespace(namespace)); err != nil {
166+
return fmt.Errorf("unable to list resources: %w", err)
165167
}
166168

167-
_, err := reconciler.Reconcile(ctx, req)
168-
if err != nil {
169-
if err := m.reportError(ctx, resource, err); err != nil {
169+
for _, item := range list.Items {
170+
req := reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&item)}
171+
if err := m.Cluster.GetScheme().Convert(&item, resource, nil); err != nil {
172+
return fmt.Errorf("unable to convert item %T: %w", item, err)
173+
}
174+
175+
_, err := reconciler.Reconcile(ctx, req)
176+
if err != nil {
177+
if err := m.reportError(ctx, resource, err); err != nil {
178+
return err
179+
}
180+
}
181+
if err := m.eventf(ctx, resource, corev1.EventTypeNormal, DryRunReason, "done"); err != nil {
170182
return err
171183
}
172184
}
173-
if err := m.eventf(ctx, resource, corev1.EventTypeNormal, DryRunReason, "done"); err != nil {
174-
return err
175-
}
176185
}
177186
}
178187
return nil

internal/dryrun/manager_test.go

Lines changed: 133 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,21 @@ import (
88
"testing"
99

1010
"github.com/stretchr/testify/require"
11-
"go.uber.org/zap"
1211
"go.uber.org/zap/zaptest"
12+
corev1 "k8s.io/api/core/v1"
1313
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1414
"k8s.io/apimachinery/pkg/runtime"
1515
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
1616
"k8s.io/client-go/kubernetes/fake"
1717
"k8s.io/client-go/kubernetes/scheme"
1818
"k8s.io/client-go/rest"
19+
client_go_testing "k8s.io/client-go/testing"
1920
"k8s.io/client-go/tools/record"
2021
ctrl "sigs.k8s.io/controller-runtime"
2122
"sigs.k8s.io/controller-runtime/pkg/builder"
2223
"sigs.k8s.io/controller-runtime/pkg/cache"
2324
"sigs.k8s.io/controller-runtime/pkg/client"
24-
crFake "sigs.k8s.io/controller-runtime/pkg/client/fake"
25+
client_fake "sigs.k8s.io/controller-runtime/pkg/client/fake"
2526
"sigs.k8s.io/controller-runtime/pkg/cluster"
2627
"sigs.k8s.io/controller-runtime/pkg/reconcile"
2728

@@ -113,7 +114,7 @@ func TestManagerStart(t *testing.T) {
113114
waitForCacheSyncResult: tc.waitForCacheSyncResult,
114115
}
115116
eventsGetter := fake.NewClientset().CoreV1()
116-
m, err := NewManager(&mckCluster, eventsGetter, zaptest.NewLogger(t))
117+
m, err := NewManager(&mckCluster, eventsGetter, zaptest.NewLogger(t), nil)
117118
require.NoError(t, err)
118119

119120
gotErr := ""
@@ -225,7 +226,7 @@ func TestDryRunReportError(t *testing.T) {
225226
} {
226227
t.Run(tc.name, func(t *testing.T) {
227228
eventsGetter := fake.NewClientset().CoreV1()
228-
m, err := NewManager(&mockCluster{}, eventsGetter, zaptest.NewLogger(t))
229+
m, err := NewManager(&mockCluster{}, eventsGetter, zaptest.NewLogger(t), nil)
229230
require.NoError(t, err)
230231
m.reportError(context.Background(), obj, tc.err)
231232

@@ -246,26 +247,25 @@ func TestDryRunReportError(t *testing.T) {
246247

247248
type mockReconciler struct {
248249
reconcile.Reconciler
250+
Resource client.Object
249251
ErrFail error
250-
Resource akov2.AtlasCustomResource
251252
}
252253

253254
func (m *mockReconciler) Reconcile(_ context.Context, _ ctrl.Request) (ctrl.Result, error) {
254255
return ctrl.Result{}, m.ErrFail
255256
}
256257

257258
func (m *mockReconciler) For() (client.Object, builder.Predicates) {
258-
return m.Resource.(client.Object), builder.Predicates{}
259+
return m.Resource, builder.Predicates{}
259260
}
260261

261262
func TestManager_dryRunReconcilers(t *testing.T) {
262263
tests := []struct {
263264
name string
264265
reconcilers []reconciler
265-
logger *zap.Logger
266-
instanceUID string
267-
ctx context.Context
268-
wantErr bool
266+
objects []client.Object
267+
wantEvents []*corev1.Event
268+
namespaces []string
269269
}{
270270
{
271271
name: "Should run dry run without errors for AtlasProject resource",
@@ -275,56 +275,150 @@ func TestManager_dryRunReconcilers(t *testing.T) {
275275
ErrFail: nil,
276276
},
277277
},
278-
wantErr: false,
279-
ctx: context.Background(),
280-
logger: zap.L(),
278+
objects: []client.Object{
279+
&akov2.AtlasProject{
280+
TypeMeta: metav1.TypeMeta{
281+
Kind: "AtlasProject",
282+
APIVersion: "atlas.mongodb.com/v1",
283+
},
284+
ObjectMeta: metav1.ObjectMeta{
285+
Name: "test",
286+
Namespace: "test",
287+
},
288+
Spec: akov2.AtlasProjectSpec{},
289+
Status: status.AtlasProjectStatus{},
290+
},
291+
},
292+
wantEvents: []*corev1.Event{
293+
{
294+
InvolvedObject: corev1.ObjectReference{Kind: "AtlasProject", APIVersion: "atlas.mongodb.com/v1", Namespace: "test", Name: "test"},
295+
Message: "done",
296+
},
297+
},
281298
},
282299
{
283-
name: "Should not fail when a reconciler fails",
300+
name: "Should emit an error when a reconciler fails",
284301
reconcilers: []reconciler{
285302
&mockReconciler{
286303
Resource: &akov2.AtlasProject{},
287304
ErrFail: fmt.Errorf("failed to reconcile"),
288305
},
289306
},
290-
wantErr: false,
291-
ctx: context.Background(),
292-
logger: zap.L(),
307+
objects: []client.Object{
308+
&akov2.AtlasProject{
309+
TypeMeta: metav1.TypeMeta{
310+
Kind: "AtlasProject",
311+
APIVersion: "atlas.mongodb.com/v1",
312+
},
313+
ObjectMeta: metav1.ObjectMeta{
314+
Name: "test",
315+
Namespace: "test",
316+
},
317+
Spec: akov2.AtlasProjectSpec{},
318+
Status: status.AtlasProjectStatus{},
319+
},
320+
},
321+
wantEvents: []*corev1.Event{
322+
{
323+
InvolvedObject: corev1.ObjectReference{Kind: "AtlasProject", APIVersion: "atlas.mongodb.com/v1", Namespace: "test", Name: "test"},
324+
Message: "failed to reconcile",
325+
},
326+
{
327+
InvolvedObject: corev1.ObjectReference{Kind: "AtlasProject", APIVersion: "atlas.mongodb.com/v1", Namespace: "test", Name: "test"},
328+
Message: "done",
329+
},
330+
},
331+
},
332+
{
333+
name: "Should ignore objects from a different namespace",
334+
reconcilers: []reconciler{
335+
&mockReconciler{
336+
Resource: &akov2.AtlasProject{},
337+
},
338+
},
339+
objects: []client.Object{
340+
&akov2.AtlasProject{
341+
TypeMeta: metav1.TypeMeta{
342+
Kind: "AtlasProject",
343+
APIVersion: "atlas.mongodb.com/v1",
344+
},
345+
ObjectMeta: metav1.ObjectMeta{
346+
Name: "test",
347+
Namespace: "test",
348+
},
349+
Spec: akov2.AtlasProjectSpec{},
350+
Status: status.AtlasProjectStatus{},
351+
},
352+
&akov2.AtlasProject{
353+
TypeMeta: metav1.TypeMeta{
354+
Kind: "AtlasProject",
355+
APIVersion: "atlas.mongodb.com/v1",
356+
},
357+
ObjectMeta: metav1.ObjectMeta{
358+
Name: "test2",
359+
Namespace: "ignored",
360+
},
361+
Spec: akov2.AtlasProjectSpec{},
362+
Status: status.AtlasProjectStatus{},
363+
},
364+
},
365+
namespaces: []string{"test"},
366+
wantEvents: []*corev1.Event{
367+
{
368+
InvolvedObject: corev1.ObjectReference{Kind: "AtlasProject", APIVersion: "atlas.mongodb.com/v1", Namespace: "test", Name: "test"},
369+
Message: "done",
370+
},
371+
},
293372
},
294373
}
374+
295375
for _, tt := range tests {
296376
t.Run(tt.name, func(t *testing.T) {
297-
eventsGetter := fake.NewClientset().CoreV1()
298377
schm := scheme.Scheme
299378
require.NoError(t, akov2.AddToScheme(schm))
300379

301380
clstr := &mockCluster{
302381
startErr: nil,
303382
waitForCacheSyncResult: true,
304-
client: crFake.NewClientBuilder().WithScheme(schm).WithObjects(
305-
&akov2.AtlasProject{
306-
TypeMeta: metav1.TypeMeta{
307-
Kind: "AtlasProject",
308-
APIVersion: "atlas.mongodb.com/v1",
309-
},
310-
ObjectMeta: metav1.ObjectMeta{
311-
Name: "test",
312-
Namespace: "test",
313-
},
314-
Spec: akov2.AtlasProjectSpec{},
315-
Status: status.AtlasProjectStatus{},
316-
}).Build(),
383+
client: client_fake.NewClientBuilder().WithScheme(schm).WithObjects(tt.objects...).Build(),
384+
}
385+
386+
eventsClient := fake.NewClientset()
387+
logger := zaptest.NewLogger(t)
388+
m, err := NewManager(clstr, eventsClient.CoreV1(), logger, tt.namespaces)
389+
if err != nil {
390+
t.Fatal(err)
391+
}
392+
393+
for _, r := range tt.reconcilers {
394+
m.SetupReconciler(r)
317395
}
318-
m := &Manager{
319-
Cluster: clstr,
320-
reconcilers: tt.reconcilers,
321-
logger: tt.logger,
322-
instanceUID: tt.instanceUID,
323-
eventsClient: eventsGetter,
396+
397+
if err := m.dryRunReconcilers(context.Background()); err != nil {
398+
t.Error(err)
399+
return
324400
}
325-
if err := m.dryRunReconcilers(tt.ctx); (err != nil) != tt.wantErr {
326-
t.Errorf("dryRunReconcilers() error = %v, wantErr %v", err, tt.wantErr)
401+
402+
gotEvents := []*corev1.Event{}
403+
for _, action := range eventsClient.Actions() {
404+
createAction, ok := action.(client_go_testing.CreateAction)
405+
if !ok {
406+
t.Errorf("Unexpected action: %v", action)
407+
continue
408+
}
409+
event, ok := createAction.GetObject().(*corev1.Event)
410+
if !ok {
411+
t.Errorf("Unexpected event: %v", event)
412+
continue
413+
}
414+
prunedEvent := &corev1.Event{
415+
InvolvedObject: event.InvolvedObject,
416+
Message: event.Message,
417+
}
418+
prunedEvent.InvolvedObject.ResourceVersion = ""
419+
gotEvents = append(gotEvents, prunedEvent)
327420
}
421+
require.Equal(t, tt.wantEvents, gotEvents)
328422
})
329423
}
330424
}

internal/operator/builder.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ func (b *Builder) Build(ctx context.Context) (cluster.Cluster, error) {
226226
return nil, fmt.Errorf("failed to initialize event client: %w", err)
227227
}
228228

229-
mgr, err := dryrun.NewManager(c, corev1Client, b.logger)
229+
mgr, err := dryrun.NewManager(c, corev1Client, b.logger, b.namespaces)
230230
if err != nil {
231231
return nil, fmt.Errorf("failed to create dry-run manager: %w", err)
232232
}

0 commit comments

Comments
 (0)