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 }