github.com/banzaicloud/operator-tools@v0.28.10/pkg/reconciler/native_test.go (about)

     1  // Copyright © 2020 Banzai Cloud
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package reconciler_test
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"testing"
    21  
    22  	"github.com/go-logr/logr"
    23  	"github.com/spf13/cast"
    24  	"github.com/stretchr/testify/assert"
    25  	corev1 "k8s.io/api/core/v1"
    26  	"k8s.io/apimachinery/pkg/api/meta"
    27  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/apimachinery/pkg/runtime/schema"
    31  	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
    32  	"sigs.k8s.io/controller-runtime/pkg/builder"
    33  
    34  	"github.com/banzaicloud/operator-tools/pkg/reconciler"
    35  	ottypes "github.com/banzaicloud/operator-tools/pkg/types"
    36  	"github.com/banzaicloud/operator-tools/pkg/utils"
    37  )
    38  
    39  // FakeResourceOwner object implements the ResourceOwner interface by piggybacking a ConfigMap (oink-oink)
    40  type FakeResourceOwner struct {
    41  	*corev1.ConfigMap
    42  }
    43  
    44  func (e *FakeResourceOwner) GetControlNamespace() string {
    45  	return controlNamespace
    46  }
    47  
    48  // Assert that a Secret reconciled together with a purged type (ConfigMap) will not get hurt (not even gets dirty)
    49  func TestNativeReconcilerKeepsTheSecret(t *testing.T) {
    50  	nativeReconciler := reconciler.NewNativeReconciler(
    51  		"testcomponent",
    52  		reconciler.NewGenericReconciler(k8sClient, log, reconciler.ReconcilerOpts{}),
    53  		k8sClient,
    54  		reconciler.NewReconciledComponent(
    55  			func(parent reconciler.ResourceOwner, object interface{}) []reconciler.ResourceBuilder {
    56  				parentWithControlNamespace := parent.(reconciler.ResourceOwnerWithControlNamespace)
    57  				rb := []reconciler.ResourceBuilder{}
    58  				// depending on the incoming config we return 0 or more items
    59  				count := cast.ToInt(object)
    60  				for i := 0; i < count; i++ {
    61  					name := fmt.Sprintf("asd-%d", i)
    62  					rb = append(rb, func() (object runtime.Object, state reconciler.DesiredState, e error) {
    63  						return &corev1.ConfigMap{
    64  							ObjectMeta: v1.ObjectMeta{
    65  								Name:      name,
    66  								Namespace: parentWithControlNamespace.GetControlNamespace(),
    67  							},
    68  						}, reconciler.StatePresent, nil
    69  					})
    70  				}
    71  				// this is returned with every call, so it shouldn't change
    72  				rb = append(rb, func() (object runtime.Object, state reconciler.DesiredState, e error) {
    73  					return &corev1.Secret{
    74  						ObjectMeta: v1.ObjectMeta{
    75  							Name:      "keep-the-secret",
    76  							Namespace: parentWithControlNamespace.GetControlNamespace(),
    77  						},
    78  					}, reconciler.StatePresent, nil
    79  				})
    80  				return rb
    81  			},
    82  			func(b *builder.Builder) {},
    83  			func() []schema.GroupVersionKind {
    84  				return []schema.GroupVersionKind{
    85  					{
    86  						Group:   "",
    87  						Version: "v1",
    88  						Kind:    "ConfigMap",
    89  					},
    90  				}
    91  			},
    92  		),
    93  		func(object runtime.Object) (reconciler.ResourceOwner, interface{}) {
    94  			return &FakeResourceOwner{ConfigMap: object.(*corev1.ConfigMap)}, object.(*corev1.ConfigMap).Data["count"]
    95  		},
    96  	)
    97  
    98  	fakeOwnerObject := &corev1.ConfigMap{
    99  		ObjectMeta: v1.ObjectMeta{
   100  			Name:      "example",
   101  			Namespace: controlNamespace,
   102  		},
   103  	}
   104  
   105  	setCount := func(c *corev1.ConfigMap, count int) *corev1.ConfigMap {
   106  		if c.Data == nil {
   107  			c.Data = map[string]string{}
   108  		}
   109  		c.Data["count"] = cast.ToString(count)
   110  		return c
   111  	}
   112  
   113  	// in the first iteration we create a single configmap and a secret (keep the secret!)
   114  
   115  	_, err := nativeReconciler.Reconcile(setCount(fakeOwnerObject, 1))
   116  	if err != nil {
   117  		t.Fatalf("Expected nil, got: %+v", err)
   118  	}
   119  
   120  	assertConfigMapList(t, func(l *corev1.ConfigMapList) {
   121  		assert.Len(t, l.Items, 1)
   122  		assert.Equal(t, l.Items[0].Name, "asd-0")
   123  	})
   124  	assertSecretList(t, func(l *corev1.SecretList) {
   125  		assert.Len(t, l.Items, 1)
   126  		assert.Equal(t, l.Items[0].Name, "keep-the-secret")
   127  	})
   128  
   129  	assert.Len(t, nativeReconciler.GetReconciledObjectWithState(reconciler.ReconciledObjectStatePurged), 0)
   130  
   131  	// next round, the count of configmaps increase to 2, keep the secret!
   132  
   133  	_, err = nativeReconciler.Reconcile(setCount(fakeOwnerObject, 2))
   134  	if err != nil {
   135  		t.Fatalf("%+v", err)
   136  	}
   137  
   138  	assertConfigMapList(t, func(l *corev1.ConfigMapList) {
   139  		assert.Len(t, l.Items, 2)
   140  		assert.Equal(t, l.Items[0].Name, "asd-0")
   141  		assert.Equal(t, l.Items[1].Name, "asd-1")
   142  	})
   143  	assertSecretList(t, func(l *corev1.SecretList) {
   144  		assert.Len(t, l.Items, 1)
   145  		assert.Equal(t, l.Items[0].Name, "keep-the-secret")
   146  	})
   147  
   148  	assert.Len(t, nativeReconciler.GetReconciledObjectWithState(reconciler.ReconciledObjectStatePurged), 0)
   149  
   150  	// next round, the count shrinks back to 1, the second configmap should be removed, keep the secret!
   151  
   152  	_, err = nativeReconciler.Reconcile(setCount(fakeOwnerObject, 1))
   153  	if err != nil {
   154  		t.Fatalf("Expected nil, got: %+v", err)
   155  	}
   156  
   157  	assertConfigMapList(t, func(l *corev1.ConfigMapList) {
   158  		assert.Len(t, l.Items, 1)
   159  		assert.Equal(t, l.Items[0].Name, "asd-0")
   160  	})
   161  	assertSecretList(t, func(l *corev1.SecretList) {
   162  		assert.Len(t, l.Items, 1)
   163  		assert.Equal(t, l.Items[0].Name, "keep-the-secret")
   164  	})
   165  
   166  	purged := nativeReconciler.GetReconciledObjectWithState(reconciler.ReconciledObjectStatePurged)
   167  	assert.Len(t, purged, 1)
   168  	assert.Equal(t, purged[0].(*unstructured.Unstructured).GetName(), "asd-1")
   169  
   170  	// next round, scale back the configmaps to 0, keep the secret!
   171  
   172  	_, err = nativeReconciler.Reconcile(setCount(fakeOwnerObject, 0))
   173  	if err != nil {
   174  		t.Fatalf("Expected nil, got: %+v", err)
   175  	}
   176  
   177  	assertConfigMapList(t, func(l *corev1.ConfigMapList) {
   178  		assert.Len(t, l.Items, 0)
   179  	})
   180  	assertSecretList(t, func(l *corev1.SecretList) {
   181  		assert.Len(t, l.Items, 1)
   182  		assert.Equal(t, l.Items[0].Name, "keep-the-secret")
   183  	})
   184  
   185  	purged = nativeReconciler.GetReconciledObjectWithState(reconciler.ReconciledObjectStatePurged)
   186  	assert.Len(t, purged, 2)
   187  	assert.Equal(t, purged[0].(*unstructured.Unstructured).GetName(), "asd-1")
   188  	assert.Equal(t, purged[1].(*unstructured.Unstructured).GetName(), "asd-0")
   189  }
   190  
   191  func TestNativeReconcilerObjectModifier(t *testing.T) {
   192  	nativeReconciler := createReconcilerForRefTests(
   193  		reconciler.NativeReconcilerWithModifier(func(o, p runtime.Object) (runtime.Object, error) {
   194  			om, _ := meta.Accessor(o)
   195  			pm, _ := meta.Accessor(p)
   196  			om.SetAnnotations(pm.GetAnnotations())
   197  			return o, nil
   198  		}))
   199  
   200  	fakeOwnerObject := &corev1.ConfigMap{
   201  		ObjectMeta: v1.ObjectMeta{
   202  			Name:      "example",
   203  			Namespace: controlNamespace,
   204  			UID:       "something",
   205  			Annotations: map[string]string{
   206  				"parentKey": "parentValue",
   207  			},
   208  		},
   209  	}
   210  
   211  	_, err := nativeReconciler.Reconcile(fakeOwnerObject)
   212  	if err != nil {
   213  		t.Fatalf("got error: %s", err.Error())
   214  	}
   215  
   216  	assertConfigMapList(t, func(l *corev1.ConfigMapList) {
   217  		assert.Len(t, l.Items, 1)
   218  		assert.Contains(t, l.Items[0].Annotations, "parentKey")
   219  		assert.Equal(t, "parentValue", l.Items[0].Annotations["parentKey"])
   220  	})
   221  }
   222  
   223  func TestNativeReconcilerSetNoControllerRefByDefault(t *testing.T) {
   224  	nativeReconciler := createReconcilerForRefTests(
   225  	// without this, controller refs are not going to be applied:
   226  	// reconciler.NativeReconcilerSetControllerRef()
   227  	)
   228  
   229  	fakeOwnerObject := &corev1.ConfigMap{
   230  		ObjectMeta: v1.ObjectMeta{
   231  			Name:      "example",
   232  			Namespace: controlNamespace,
   233  			UID:       "something-fashionable",
   234  		},
   235  	}
   236  
   237  	_, err := nativeReconciler.Reconcile(fakeOwnerObject)
   238  	if err != nil {
   239  		t.Fatalf("%+v", err)
   240  	}
   241  	assertConfigMapList(t, func(l *corev1.ConfigMapList) {
   242  		assert.Len(t, l.Items, 1)
   243  		assert.Len(t, l.Items[0].OwnerReferences, 0)
   244  	})
   245  }
   246  
   247  func TestNativeReconcilerSetControllerRef(t *testing.T) {
   248  	nativeReconciler := createReconcilerForRefTests(
   249  		// without this, controller refs are not going to be applied:
   250  		reconciler.NativeReconcilerSetControllerRef(),
   251  	)
   252  
   253  	fakeOwnerObject := &corev1.ConfigMap{
   254  		ObjectMeta: v1.ObjectMeta{
   255  			Name:      "example",
   256  			Namespace: controlNamespace,
   257  			UID:       "something-fashionable",
   258  		},
   259  	}
   260  
   261  	_, err := nativeReconciler.Reconcile(fakeOwnerObject)
   262  	if err != nil {
   263  		t.Fatalf("%+v", err)
   264  	}
   265  	assertConfigMapList(t, func(l *corev1.ConfigMapList) {
   266  		assert.Len(t, l.Items, 1)
   267  		assert.Len(t, l.Items[0].OwnerReferences, 1)
   268  	})
   269  }
   270  
   271  func TestNativeReconcilerSetControllerRefMultipleTimes(t *testing.T) {
   272  	nativeReconciler := createReconcilerForRefTests(reconciler.NativeReconcilerSetControllerRef())
   273  
   274  	fakeOwnerObject := &corev1.ConfigMap{
   275  		ObjectMeta: v1.ObjectMeta{
   276  			Name:      "example",
   277  			Namespace: controlNamespace,
   278  			UID:       "something-fashionable",
   279  		},
   280  	}
   281  
   282  	for i := 0; i < 2; i++ {
   283  		_, err := nativeReconciler.Reconcile(fakeOwnerObject)
   284  		if err != nil {
   285  			t.Fatalf("%+v", err)
   286  		}
   287  		assertConfigMapList(t, func(l *corev1.ConfigMapList) {
   288  			assert.Len(t, l.Items, 1)
   289  			assert.Len(t, l.Items[0].OwnerReferences, 1)
   290  			assert.Equal(t, fakeOwnerObject.UID, l.Items[0].OwnerReferences[0].UID)
   291  		})
   292  	}
   293  }
   294  
   295  func TestNativeReconcilerFailToSetCrossNamespaceControllerRef(t *testing.T) {
   296  	nativeReconciler := createReconcilerForRefTests(reconciler.NativeReconcilerSetControllerRef())
   297  
   298  	fakeOwnerObject := &corev1.ConfigMap{
   299  		ObjectMeta: v1.ObjectMeta{
   300  			Name:      "example",
   301  			Namespace: "another-such-wow-namespace",
   302  			UID:       "something-fashionable",
   303  		},
   304  	}
   305  
   306  	_, err := nativeReconciler.Reconcile(fakeOwnerObject)
   307  	if err != nil {
   308  		t.Fatalf("got error: %s", err.Error())
   309  	}
   310  }
   311  
   312  func TestCreatedDesiredStateAnnotationWithStaticStatePresent(t *testing.T) {
   313  	desired := &corev1.ConfigMap{
   314  		ObjectMeta: v1.ObjectMeta{
   315  			Name:      "test-desired-state-with-static-present",
   316  			Namespace: controlNamespace,
   317  			Annotations: map[string]string{
   318  				ottypes.BanzaiCloudDesiredStateCreated: "true",
   319  			},
   320  		},
   321  		Data: map[string]string{
   322  			"a": "b",
   323  		},
   324  	}
   325  
   326  	r := reconciler.NewReconcilerWith(k8sClient)
   327  	result, err := r.ReconcileResource(desired, reconciler.StatePresent)
   328  	if result != nil {
   329  		t.Fatalf("result expected to be nil if everything went smooth")
   330  	}
   331  	if err != nil {
   332  		t.Fatalf("%+v", err)
   333  	}
   334  
   335  	desiredMutated := desired.DeepCopy()
   336  	desiredMutated.Data["a"] = "c"
   337  
   338  	nr := reconciler.NewNativeReconcilerWithDefaults("test", k8sClient, clientgoscheme.Scheme, logr.Discard(), func(parent reconciler.ResourceOwner, object interface{}) []reconciler.ResourceBuilder {
   339  		return []reconciler.ResourceBuilder{
   340  			func() (runtime.Object, reconciler.DesiredState, error) {
   341  				return desiredMutated, reconciler.StatePresent, nil
   342  			},
   343  		}
   344  	}, func() []schema.GroupVersionKind {
   345  		return nil
   346  	}, func(_ runtime.Object) (reconciler.ResourceOwner, interface{}) {
   347  		return nil, nil
   348  	})
   349  
   350  	_, err = nr.Reconcile(desired)
   351  	if err != nil {
   352  		t.Fatalf("%+v", err)
   353  	}
   354  
   355  	created := &corev1.ConfigMap{}
   356  	if err := k8sClient.Get(context.TODO(), utils.ObjectKeyFromObjectMeta(desired), created); err != nil {
   357  		t.Fatalf("%+v", err)
   358  	}
   359  
   360  	assert.Equal(t, created.Name, desired.Name)
   361  	assert.Equal(t, created.Namespace, desired.Namespace)
   362  	assert.Equal(t, created.Data["a"], desired.Data["a"])
   363  }
   364  
   365  func TestCreatedDesiredStateAnnotationWithDynamicStatePresent(t *testing.T) {
   366  	desired := &corev1.ConfigMap{
   367  		ObjectMeta: v1.ObjectMeta{
   368  			Name:      "test-desired-state-with-dynamic-present",
   369  			Namespace: controlNamespace,
   370  			Annotations: map[string]string{
   371  				ottypes.BanzaiCloudDesiredStateCreated: "true",
   372  			},
   373  		},
   374  		Data: map[string]string{
   375  			"a": "b",
   376  		},
   377  	}
   378  
   379  	r := reconciler.NewReconcilerWith(k8sClient)
   380  	result, err := r.ReconcileResource(desired, reconciler.StatePresent)
   381  	if result != nil {
   382  		t.Fatalf("result expected to be nil if everything went smooth")
   383  	}
   384  	if err != nil {
   385  		t.Fatalf("%+v", err)
   386  	}
   387  
   388  	desiredMutated := desired.DeepCopy()
   389  	desiredMutated.Data["a"] = "c"
   390  
   391  	nr := reconciler.NewNativeReconcilerWithDefaults("test", k8sClient, clientgoscheme.Scheme, logr.Discard(), func(parent reconciler.ResourceOwner, object interface{}) []reconciler.ResourceBuilder {
   392  		return []reconciler.ResourceBuilder{
   393  			func() (runtime.Object, reconciler.DesiredState, error) {
   394  				return desiredMutated, reconciler.DynamicDesiredState{
   395  					DesiredState: reconciler.StatePresent,
   396  				}, nil
   397  			},
   398  		}
   399  	}, func() []schema.GroupVersionKind {
   400  		return nil
   401  	}, func(_ runtime.Object) (reconciler.ResourceOwner, interface{}) {
   402  		return nil, nil
   403  	})
   404  
   405  	_, err = nr.Reconcile(desired)
   406  	if err != nil {
   407  		t.Fatalf("%+v", err)
   408  	}
   409  
   410  	created := &corev1.ConfigMap{}
   411  	if err := k8sClient.Get(context.TODO(), utils.ObjectKeyFromObjectMeta(desired), created); err != nil {
   412  		t.Fatalf("%+v", err)
   413  	}
   414  
   415  	assert.Equal(t, created.Name, desired.Name)
   416  	assert.Equal(t, created.Namespace, desired.Namespace)
   417  	assert.Equal(t, created.Data["a"], desired.Data["a"])
   418  }
   419  
   420  func createReconcilerForRefTests(opts ...reconciler.NativeReconcilerOpt) *reconciler.NativeReconciler {
   421  	return reconciler.NewNativeReconciler(
   422  		"test",
   423  		reconciler.NewGenericReconciler(k8sClient, log, reconciler.ReconcilerOpts{}),
   424  		k8sClient,
   425  		reconciler.NewReconciledComponent(
   426  			func(parent reconciler.ResourceOwner, object interface{}) []reconciler.ResourceBuilder {
   427  				parentWithControlNamespace := parent.(reconciler.ResourceOwnerWithControlNamespace)
   428  				var rb []reconciler.ResourceBuilder
   429  				rb = append(rb, func() (object runtime.Object, state reconciler.DesiredState, e error) {
   430  					return &corev1.ConfigMap{
   431  						ObjectMeta: v1.ObjectMeta{
   432  							Name:      "test-cm",
   433  							Namespace: parentWithControlNamespace.GetControlNamespace(),
   434  						},
   435  					}, reconciler.StatePresent, nil
   436  				})
   437  				return rb
   438  			},
   439  			func(b *builder.Builder) {},
   440  			func() []schema.GroupVersionKind { return []schema.GroupVersionKind{} },
   441  		),
   442  		func(object runtime.Object) (reconciler.ResourceOwner, interface{}) {
   443  			return &FakeResourceOwner{ConfigMap: object.(*corev1.ConfigMap)}, nil
   444  		},
   445  		opts...,
   446  	)
   447  }