Skip to content

Commit

Permalink
dryrun: add support for namespace filtering (#2142)
Browse files Browse the repository at this point in the history
  • Loading branch information
s-urbaniak authored Feb 20, 2025
1 parent 0a8bce3 commit b5957b6
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 55 deletions.
39 changes: 24 additions & 15 deletions internal/dryrun/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,22 @@ type Manager struct {
logger *zap.Logger
instanceUID string
eventsClient corev1client.EventsGetter
namespaces []string
}

func NewManager(c cluster.Cluster, eventsClient corev1client.EventsGetter, logger *zap.Logger) (*Manager, error) {
func NewManager(c cluster.Cluster, eventsClient corev1client.EventsGetter, logger *zap.Logger, namespaces []string) (*Manager, error) {
mgr := &Manager{
Cluster: c,
logger: logger.Named("dry-run-manager"),
instanceUID: uuid.New().String(),
eventsClient: eventsClient,
namespaces: []string{metav1.NamespaceAll},
}

if len(namespaces) > 0 {
mgr.namespaces = namespaces
}

return mgr, nil
}

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

if err := m.Cluster.GetClient().List(ctx, list); err != nil {
return fmt.Errorf("unable to list resources: %w", err)
}

for _, item := range list.Items {
req := reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&item)}
if err := m.Cluster.GetScheme().Convert(&item, resource, nil); err != nil {
return fmt.Errorf("unable to convert item %T: %w", item, err)
for _, namespace := range m.namespaces {
if err := m.Cluster.GetClient().List(ctx, list, client.InNamespace(namespace)); err != nil {
return fmt.Errorf("unable to list resources: %w", err)
}

_, err := reconciler.Reconcile(ctx, req)
if err != nil {
if err := m.reportError(ctx, resource, err); err != nil {
for _, item := range list.Items {
req := reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&item)}
if err := m.Cluster.GetScheme().Convert(&item, resource, nil); err != nil {
return fmt.Errorf("unable to convert item %T: %w", item, err)
}

_, err := reconciler.Reconcile(ctx, req)
if err != nil {
if err := m.reportError(ctx, resource, err); err != nil {
return err
}
}
if err := m.eventf(ctx, resource, corev1.EventTypeNormal, DryRunReason, "done"); err != nil {
return err
}
}
if err := m.eventf(ctx, resource, corev1.EventTypeNormal, DryRunReason, "done"); err != nil {
return err
}
}
}
return nil
Expand Down
172 changes: 133 additions & 39 deletions internal/dryrun/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,21 @@ import (
"testing"

"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zaptest"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
client_go_testing "k8s.io/client-go/testing"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
crFake "sigs.k8s.io/controller-runtime/pkg/client/fake"
client_fake "sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/cluster"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

Expand Down Expand Up @@ -113,7 +114,7 @@ func TestManagerStart(t *testing.T) {
waitForCacheSyncResult: tc.waitForCacheSyncResult,
}
eventsGetter := fake.NewClientset().CoreV1()
m, err := NewManager(&mckCluster, eventsGetter, zaptest.NewLogger(t))
m, err := NewManager(&mckCluster, eventsGetter, zaptest.NewLogger(t), nil)
require.NoError(t, err)

gotErr := ""
Expand Down Expand Up @@ -225,7 +226,7 @@ func TestDryRunReportError(t *testing.T) {
} {
t.Run(tc.name, func(t *testing.T) {
eventsGetter := fake.NewClientset().CoreV1()
m, err := NewManager(&mockCluster{}, eventsGetter, zaptest.NewLogger(t))
m, err := NewManager(&mockCluster{}, eventsGetter, zaptest.NewLogger(t), nil)
require.NoError(t, err)
m.reportError(context.Background(), obj, tc.err)

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

type mockReconciler struct {
reconcile.Reconciler
Resource client.Object
ErrFail error
Resource akov2.AtlasCustomResource
}

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

func (m *mockReconciler) For() (client.Object, builder.Predicates) {
return m.Resource.(client.Object), builder.Predicates{}
return m.Resource, builder.Predicates{}
}

func TestManager_dryRunReconcilers(t *testing.T) {
tests := []struct {
name string
reconcilers []reconciler
logger *zap.Logger
instanceUID string
ctx context.Context
wantErr bool
objects []client.Object
wantEvents []*corev1.Event
namespaces []string
}{
{
name: "Should run dry run without errors for AtlasProject resource",
Expand All @@ -275,56 +275,150 @@ func TestManager_dryRunReconcilers(t *testing.T) {
ErrFail: nil,
},
},
wantErr: false,
ctx: context.Background(),
logger: zap.L(),
objects: []client.Object{
&akov2.AtlasProject{
TypeMeta: metav1.TypeMeta{
Kind: "AtlasProject",
APIVersion: "atlas.mongodb.com/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test",
},
Spec: akov2.AtlasProjectSpec{},
Status: status.AtlasProjectStatus{},
},
},
wantEvents: []*corev1.Event{
{
InvolvedObject: corev1.ObjectReference{Kind: "AtlasProject", APIVersion: "atlas.mongodb.com/v1", Namespace: "test", Name: "test"},
Message: "done",
},
},
},
{
name: "Should not fail when a reconciler fails",
name: "Should emit an error when a reconciler fails",
reconcilers: []reconciler{
&mockReconciler{
Resource: &akov2.AtlasProject{},
ErrFail: fmt.Errorf("failed to reconcile"),
},
},
wantErr: false,
ctx: context.Background(),
logger: zap.L(),
objects: []client.Object{
&akov2.AtlasProject{
TypeMeta: metav1.TypeMeta{
Kind: "AtlasProject",
APIVersion: "atlas.mongodb.com/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test",
},
Spec: akov2.AtlasProjectSpec{},
Status: status.AtlasProjectStatus{},
},
},
wantEvents: []*corev1.Event{
{
InvolvedObject: corev1.ObjectReference{Kind: "AtlasProject", APIVersion: "atlas.mongodb.com/v1", Namespace: "test", Name: "test"},
Message: "failed to reconcile",
},
{
InvolvedObject: corev1.ObjectReference{Kind: "AtlasProject", APIVersion: "atlas.mongodb.com/v1", Namespace: "test", Name: "test"},
Message: "done",
},
},
},
{
name: "Should ignore objects from a different namespace",
reconcilers: []reconciler{
&mockReconciler{
Resource: &akov2.AtlasProject{},
},
},
objects: []client.Object{
&akov2.AtlasProject{
TypeMeta: metav1.TypeMeta{
Kind: "AtlasProject",
APIVersion: "atlas.mongodb.com/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test",
},
Spec: akov2.AtlasProjectSpec{},
Status: status.AtlasProjectStatus{},
},
&akov2.AtlasProject{
TypeMeta: metav1.TypeMeta{
Kind: "AtlasProject",
APIVersion: "atlas.mongodb.com/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test2",
Namespace: "ignored",
},
Spec: akov2.AtlasProjectSpec{},
Status: status.AtlasProjectStatus{},
},
},
namespaces: []string{"test"},
wantEvents: []*corev1.Event{
{
InvolvedObject: corev1.ObjectReference{Kind: "AtlasProject", APIVersion: "atlas.mongodb.com/v1", Namespace: "test", Name: "test"},
Message: "done",
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
eventsGetter := fake.NewClientset().CoreV1()
schm := scheme.Scheme
require.NoError(t, akov2.AddToScheme(schm))

clstr := &mockCluster{
startErr: nil,
waitForCacheSyncResult: true,
client: crFake.NewClientBuilder().WithScheme(schm).WithObjects(
&akov2.AtlasProject{
TypeMeta: metav1.TypeMeta{
Kind: "AtlasProject",
APIVersion: "atlas.mongodb.com/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test",
},
Spec: akov2.AtlasProjectSpec{},
Status: status.AtlasProjectStatus{},
}).Build(),
client: client_fake.NewClientBuilder().WithScheme(schm).WithObjects(tt.objects...).Build(),
}

eventsClient := fake.NewClientset()
logger := zaptest.NewLogger(t)
m, err := NewManager(clstr, eventsClient.CoreV1(), logger, tt.namespaces)
if err != nil {
t.Fatal(err)
}

for _, r := range tt.reconcilers {
m.SetupReconciler(r)
}
m := &Manager{
Cluster: clstr,
reconcilers: tt.reconcilers,
logger: tt.logger,
instanceUID: tt.instanceUID,
eventsClient: eventsGetter,

if err := m.dryRunReconcilers(context.Background()); err != nil {
t.Error(err)
return
}
if err := m.dryRunReconcilers(tt.ctx); (err != nil) != tt.wantErr {
t.Errorf("dryRunReconcilers() error = %v, wantErr %v", err, tt.wantErr)

gotEvents := []*corev1.Event{}
for _, action := range eventsClient.Actions() {
createAction, ok := action.(client_go_testing.CreateAction)
if !ok {
t.Errorf("Unexpected action: %v", action)
continue
}
event, ok := createAction.GetObject().(*corev1.Event)
if !ok {
t.Errorf("Unexpected event: %v", event)
continue
}
prunedEvent := &corev1.Event{
InvolvedObject: event.InvolvedObject,
Message: event.Message,
}
prunedEvent.InvolvedObject.ResourceVersion = ""
gotEvents = append(gotEvents, prunedEvent)
}
require.Equal(t, tt.wantEvents, gotEvents)
})
}
}
2 changes: 1 addition & 1 deletion internal/operator/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ func (b *Builder) Build(ctx context.Context) (cluster.Cluster, error) {
return nil, fmt.Errorf("failed to initialize event client: %w", err)
}

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

0 comments on commit b5957b6

Please sign in to comment.