github.com/lablabs/operator-sdk@v0.8.2/pkg/ansible/controller/reconcile_test.go (about) 1 // Copyright 2018 The Operator-SDK Authors 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 controller_test 16 17 import ( 18 "context" 19 "reflect" 20 "testing" 21 "time" 22 23 "github.com/operator-framework/operator-sdk/pkg/ansible/controller" 24 ansiblestatus "github.com/operator-framework/operator-sdk/pkg/ansible/controller/status" 25 "github.com/operator-framework/operator-sdk/pkg/ansible/events" 26 "github.com/operator-framework/operator-sdk/pkg/ansible/runner" 27 "github.com/operator-framework/operator-sdk/pkg/ansible/runner/eventapi" 28 "github.com/operator-framework/operator-sdk/pkg/ansible/runner/fake" 29 30 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 31 "k8s.io/apimachinery/pkg/runtime/schema" 32 "k8s.io/apimachinery/pkg/types" 33 "sigs.k8s.io/controller-runtime/pkg/client" 34 fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" 35 "sigs.k8s.io/controller-runtime/pkg/reconcile" 36 ) 37 38 func TestReconcile(t *testing.T) { 39 gvk := schema.GroupVersionKind{ 40 Kind: "Testing", 41 Group: "operator-sdk", 42 Version: "v1beta1", 43 } 44 eventTime := time.Now() 45 testCases := []struct { 46 Name string 47 GVK schema.GroupVersionKind 48 ReconcilePeriod time.Duration 49 ManageStatus bool 50 Runner runner.Runner 51 EventHandlers []events.EventHandler 52 Client client.Client 53 ExpectedObject *unstructured.Unstructured 54 Result reconcile.Result 55 Request reconcile.Request 56 ShouldError bool 57 }{ 58 { 59 Name: "cr not found", 60 GVK: gvk, 61 ReconcilePeriod: 5 * time.Second, 62 Runner: &fake.Runner{ 63 JobEvents: []eventapi.JobEvent{}, 64 }, 65 Client: fakeclient.NewFakeClient(), 66 Result: reconcile.Result{}, 67 Request: reconcile.Request{ 68 NamespacedName: types.NamespacedName{ 69 Name: "not_found", 70 Namespace: "default", 71 }, 72 }, 73 }, 74 { 75 Name: "completed reconcile", 76 GVK: gvk, 77 ReconcilePeriod: 5 * time.Second, 78 ManageStatus: true, 79 Runner: &fake.Runner{ 80 JobEvents: []eventapi.JobEvent{ 81 eventapi.JobEvent{ 82 Event: eventapi.EventPlaybookOnStats, 83 Created: eventapi.EventTime{Time: eventTime}, 84 }, 85 }, 86 }, 87 Client: fakeclient.NewFakeClient(&unstructured.Unstructured{ 88 Object: map[string]interface{}{ 89 "metadata": map[string]interface{}{ 90 "name": "reconcile", 91 "namespace": "default", 92 }, 93 "apiVersion": "operator-sdk/v1beta1", 94 "kind": "Testing", 95 }, 96 }), 97 Result: reconcile.Result{ 98 RequeueAfter: 5 * time.Second, 99 }, 100 Request: reconcile.Request{ 101 NamespacedName: types.NamespacedName{ 102 Name: "reconcile", 103 Namespace: "default", 104 }, 105 }, 106 ExpectedObject: &unstructured.Unstructured{ 107 Object: map[string]interface{}{ 108 "metadata": map[string]interface{}{ 109 "name": "reconcile", 110 "namespace": "default", 111 }, 112 "apiVersion": "operator-sdk/v1beta1", 113 "kind": "Testing", 114 "spec": map[string]interface{}{}, 115 "status": map[string]interface{}{ 116 "conditions": []interface{}{ 117 map[string]interface{}{ 118 "status": "True", 119 "type": "Running", 120 "ansibleResult": map[string]interface{}{ 121 "changed": int64(0), 122 "failures": int64(0), 123 "ok": int64(0), 124 "skipped": int64(0), 125 "completion": eventTime.Format("2006-01-02T15:04:05.99999999"), 126 }, 127 "message": "Awaiting next reconciliation", 128 "reason": "Successful", 129 }, 130 }, 131 }, 132 }, 133 }, 134 }, 135 { 136 Name: "Failure message reconcile", 137 GVK: gvk, 138 ReconcilePeriod: 5 * time.Second, 139 ManageStatus: true, 140 Runner: &fake.Runner{ 141 JobEvents: []eventapi.JobEvent{ 142 eventapi.JobEvent{ 143 Event: eventapi.EventRunnerOnFailed, 144 Created: eventapi.EventTime{Time: eventTime}, 145 EventData: map[string]interface{}{ 146 "res": map[string]interface{}{ 147 "msg": "new failure message", 148 }, 149 }, 150 }, 151 eventapi.JobEvent{ 152 Event: eventapi.EventPlaybookOnStats, 153 Created: eventapi.EventTime{Time: eventTime}, 154 }, 155 }, 156 }, 157 Client: fakeclient.NewFakeClient(&unstructured.Unstructured{ 158 Object: map[string]interface{}{ 159 "metadata": map[string]interface{}{ 160 "name": "reconcile", 161 "namespace": "default", 162 }, 163 "apiVersion": "operator-sdk/v1beta1", 164 "kind": "Testing", 165 "spec": map[string]interface{}{}, 166 }, 167 }), 168 Result: reconcile.Result{ 169 RequeueAfter: 5 * time.Second, 170 }, 171 Request: reconcile.Request{ 172 NamespacedName: types.NamespacedName{ 173 Name: "reconcile", 174 Namespace: "default", 175 }, 176 }, 177 ExpectedObject: &unstructured.Unstructured{ 178 Object: map[string]interface{}{ 179 "metadata": map[string]interface{}{ 180 "name": "reconcile", 181 "namespace": "default", 182 }, 183 "apiVersion": "operator-sdk/v1beta1", 184 "kind": "Testing", 185 "spec": map[string]interface{}{}, 186 "status": map[string]interface{}{ 187 "conditions": []interface{}{ 188 map[string]interface{}{ 189 "status": "True", 190 "type": "Failure", 191 "ansibleResult": map[string]interface{}{ 192 "changed": int64(0), 193 "failures": int64(0), 194 "ok": int64(0), 195 "skipped": int64(0), 196 "completion": eventTime.Format("2006-01-02T15:04:05.99999999"), 197 }, 198 "message": "new failure message", 199 "reason": "Failed", 200 }, 201 map[string]interface{}{ 202 "status": "False", 203 "type": "Running", 204 "message": "Running reconciliation", 205 "reason": "Running", 206 }, 207 }, 208 }, 209 }, 210 }, 211 }, 212 { 213 Name: "Finalizer successful reconcile", 214 GVK: gvk, 215 ReconcilePeriod: 5 * time.Second, 216 ManageStatus: true, 217 Runner: &fake.Runner{ 218 JobEvents: []eventapi.JobEvent{ 219 eventapi.JobEvent{ 220 Event: eventapi.EventPlaybookOnStats, 221 Created: eventapi.EventTime{Time: eventTime}, 222 }, 223 }, 224 Finalizer: "testing.io", 225 }, 226 Client: fakeclient.NewFakeClient(&unstructured.Unstructured{ 227 Object: map[string]interface{}{ 228 "metadata": map[string]interface{}{ 229 "name": "reconcile", 230 "namespace": "default", 231 "annotations": map[string]interface{}{ 232 controller.ReconcilePeriodAnnotation: "3s", 233 }, 234 }, 235 "apiVersion": "operator-sdk/v1beta1", 236 "kind": "Testing", 237 "spec": map[string]interface{}{}, 238 }, 239 }), 240 Result: reconcile.Result{ 241 RequeueAfter: 3 * time.Second, 242 }, 243 Request: reconcile.Request{ 244 NamespacedName: types.NamespacedName{ 245 Name: "reconcile", 246 Namespace: "default", 247 }, 248 }, 249 ExpectedObject: &unstructured.Unstructured{ 250 Object: map[string]interface{}{ 251 "metadata": map[string]interface{}{ 252 "name": "reconcile", 253 "namespace": "default", 254 "annotations": map[string]interface{}{ 255 controller.ReconcilePeriodAnnotation: "3s", 256 }, 257 "finalizers": []interface{}{ 258 "testing.io", 259 }, 260 }, 261 "apiVersion": "operator-sdk/v1beta1", 262 "kind": "Testing", 263 "spec": map[string]interface{}{}, 264 "status": map[string]interface{}{ 265 "conditions": []interface{}{ 266 map[string]interface{}{ 267 "status": "True", 268 "type": "Running", 269 "ansibleResult": map[string]interface{}{ 270 "changed": int64(0), 271 "failures": int64(0), 272 "ok": int64(0), 273 "skipped": int64(0), 274 "completion": eventTime.Format("2006-01-02T15:04:05.99999999"), 275 }, 276 "message": "Awaiting next reconciliation", 277 "reason": "Successful", 278 }, 279 }, 280 }, 281 }, 282 }, 283 }, 284 { 285 Name: "reconcile deletetion", 286 GVK: gvk, 287 ReconcilePeriod: 5 * time.Second, 288 Runner: &fake.Runner{ 289 JobEvents: []eventapi.JobEvent{ 290 eventapi.JobEvent{ 291 Event: eventapi.EventPlaybookOnStats, 292 Created: eventapi.EventTime{Time: eventTime}, 293 }, 294 }, 295 Finalizer: "testing.io", 296 }, 297 Client: fakeclient.NewFakeClient(&unstructured.Unstructured{ 298 Object: map[string]interface{}{ 299 "metadata": map[string]interface{}{ 300 "name": "reconcile", 301 "namespace": "default", 302 "annotations": map[string]interface{}{ 303 controller.ReconcilePeriodAnnotation: "3s", 304 }, 305 "deletionTimestamp": eventTime.Format(time.RFC3339), 306 }, 307 "apiVersion": "operator-sdk/v1beta1", 308 "kind": "Testing", 309 "spec": map[string]interface{}{}, 310 }, 311 }), 312 Result: reconcile.Result{}, 313 Request: reconcile.Request{ 314 NamespacedName: types.NamespacedName{ 315 Name: "reconcile", 316 Namespace: "default", 317 }, 318 }, 319 }, 320 { 321 Name: "Finalizer successful deletion reconcile", 322 GVK: gvk, 323 ReconcilePeriod: 5 * time.Second, 324 ManageStatus: true, 325 Runner: &fake.Runner{ 326 JobEvents: []eventapi.JobEvent{ 327 eventapi.JobEvent{ 328 Event: eventapi.EventPlaybookOnStats, 329 Created: eventapi.EventTime{Time: eventTime}, 330 }, 331 }, 332 Finalizer: "testing.io", 333 }, 334 Client: fakeclient.NewFakeClient(&unstructured.Unstructured{ 335 Object: map[string]interface{}{ 336 "metadata": map[string]interface{}{ 337 "name": "reconcile", 338 "namespace": "default", 339 "finalizers": []interface{}{ 340 "testing.io", 341 }, 342 "deletionTimestamp": eventTime.Format(time.RFC3339), 343 }, 344 "apiVersion": "operator-sdk/v1beta1", 345 "kind": "Testing", 346 "spec": map[string]interface{}{}, 347 "status": map[string]interface{}{ 348 "conditions": []interface{}{ 349 map[string]interface{}{ 350 "status": "True", 351 "type": "Running", 352 "ansibleResult": map[string]interface{}{ 353 "changed": int64(0), 354 "failures": int64(0), 355 "ok": int64(0), 356 "skipped": int64(0), 357 "completion": eventTime.Format("2006-01-02T15:04:05.99999999"), 358 }, 359 "message": "Awaiting next reconciliation", 360 "reason": "Successful", 361 }, 362 }, 363 }, 364 }, 365 }), 366 Result: reconcile.Result{ 367 RequeueAfter: 5 * time.Second, 368 }, 369 Request: reconcile.Request{ 370 NamespacedName: types.NamespacedName{ 371 Name: "reconcile", 372 Namespace: "default", 373 }, 374 }, 375 ExpectedObject: &unstructured.Unstructured{ 376 Object: map[string]interface{}{ 377 "metadata": map[string]interface{}{ 378 "name": "reconcile", 379 "namespace": "default", 380 }, 381 "apiVersion": "operator-sdk/v1beta1", 382 "kind": "Testing", 383 "spec": map[string]interface{}{}, 384 "status": map[string]interface{}{ 385 "conditions": []interface{}{ 386 map[string]interface{}{ 387 "status": "True", 388 "type": "Running", 389 "ansibleResult": map[string]interface{}{ 390 "changed": int64(0), 391 "failures": int64(0), 392 "ok": int64(0), 393 "skipped": int64(0), 394 "completion": eventTime.Format("2006-01-02T15:04:05.99999999"), 395 }, 396 "message": "Awaiting next reconciliation", 397 "reason": "Successful", 398 }, 399 }, 400 }, 401 }, 402 }, 403 }, 404 { 405 Name: "No status event", 406 GVK: gvk, 407 ReconcilePeriod: 5 * time.Second, 408 Runner: &fake.Runner{ 409 JobEvents: []eventapi.JobEvent{ 410 eventapi.JobEvent{ 411 Created: eventapi.EventTime{Time: eventTime}, 412 }, 413 }, 414 }, 415 Client: fakeclient.NewFakeClient(&unstructured.Unstructured{ 416 Object: map[string]interface{}{ 417 "metadata": map[string]interface{}{ 418 "name": "reconcile", 419 "namespace": "default", 420 }, 421 "apiVersion": "operator-sdk/v1beta1", 422 "kind": "Testing", 423 "spec": map[string]interface{}{}, 424 }, 425 }), 426 Result: reconcile.Result{ 427 RequeueAfter: 5 * time.Second, 428 }, 429 Request: reconcile.Request{ 430 NamespacedName: types.NamespacedName{ 431 Name: "reconcile", 432 Namespace: "default", 433 }, 434 }, 435 ShouldError: true, 436 }, 437 { 438 Name: "no manage status", 439 GVK: gvk, 440 ReconcilePeriod: 5 * time.Second, 441 ManageStatus: false, 442 Runner: &fake.Runner{ 443 JobEvents: []eventapi.JobEvent{ 444 eventapi.JobEvent{ 445 Event: eventapi.EventPlaybookOnStats, 446 Created: eventapi.EventTime{Time: eventTime}, 447 }, 448 }, 449 }, 450 Client: fakeclient.NewFakeClient(&unstructured.Unstructured{ 451 Object: map[string]interface{}{ 452 "metadata": map[string]interface{}{ 453 "name": "reconcile", 454 "namespace": "default", 455 }, 456 "apiVersion": "operator-sdk/v1beta1", 457 "kind": "Testing", 458 }, 459 }), 460 Result: reconcile.Result{ 461 RequeueAfter: 5 * time.Second, 462 }, 463 Request: reconcile.Request{ 464 NamespacedName: types.NamespacedName{ 465 Name: "reconcile", 466 Namespace: "default", 467 }, 468 }, 469 ExpectedObject: &unstructured.Unstructured{ 470 Object: map[string]interface{}{ 471 "metadata": map[string]interface{}{ 472 "name": "reconcile", 473 "namespace": "default", 474 }, 475 "apiVersion": "operator-sdk/v1beta1", 476 "kind": "Testing", 477 "spec": map[string]interface{}{}, 478 "status": map[string]interface{}{}, 479 }, 480 }, 481 }, 482 } 483 484 for _, tc := range testCases { 485 t.Run(tc.Name, func(t *testing.T) { 486 var aor reconcile.Reconciler = &controller.AnsibleOperatorReconciler{ 487 GVK: tc.GVK, 488 Runner: tc.Runner, 489 Client: tc.Client, 490 EventHandlers: tc.EventHandlers, 491 ReconcilePeriod: tc.ReconcilePeriod, 492 ManageStatus: tc.ManageStatus, 493 } 494 result, err := aor.Reconcile(tc.Request) 495 if err != nil && !tc.ShouldError { 496 t.Fatalf("Unexpected error: %v", err) 497 } 498 if !reflect.DeepEqual(result, tc.Result) { 499 t.Fatalf("Reconcile result does not equal\nexpected: %#v\nactual: %#v", tc.Result, result) 500 } 501 if tc.ExpectedObject != nil { 502 actualObject := &unstructured.Unstructured{} 503 actualObject.SetGroupVersionKind(tc.ExpectedObject.GroupVersionKind()) 504 err := tc.Client.Get(context.TODO(), types.NamespacedName{ 505 Name: tc.ExpectedObject.GetName(), 506 Namespace: tc.ExpectedObject.GetNamespace(), 507 }, actualObject) 508 if err != nil { 509 t.Fatalf("Failed to get object: (%v)", err) 510 } 511 if !reflect.DeepEqual(actualObject.GetAnnotations(), tc.ExpectedObject.GetAnnotations()) { 512 t.Fatalf("Annotations are not the same\nexpected: %v\nactual: %v", tc.ExpectedObject.GetAnnotations(), actualObject.GetAnnotations()) 513 } 514 if !reflect.DeepEqual(actualObject.GetFinalizers(), tc.ExpectedObject.GetFinalizers()) && 515 len(actualObject.GetFinalizers()) != 0 && len(tc.ExpectedObject.GetFinalizers()) != 0 { 516 t.Fatalf("Finalizers are not the same\nexpected: %#v\nactual: %#v", tc.ExpectedObject.GetFinalizers(), actualObject.GetFinalizers()) 517 } 518 sMap, _ := tc.ExpectedObject.Object["status"].(map[string]interface{}) 519 expectedStatus := ansiblestatus.CreateFromMap(sMap) 520 sMap, _ = actualObject.Object["status"].(map[string]interface{}) 521 actualStatus := ansiblestatus.CreateFromMap(sMap) 522 if len(expectedStatus.Conditions) != len(actualStatus.Conditions) { 523 t.Fatalf("Status conditions not the same\nexpected: %v\nactual: %v", expectedStatus, actualStatus) 524 } 525 for _, c := range expectedStatus.Conditions { 526 actualCond := ansiblestatus.GetCondition(actualStatus, c.Type) 527 if c.Reason != actualCond.Reason || c.Message != actualCond.Message || c.Status != actualCond.Status { 528 t.Fatalf("Message or reason did not match\nexpected: %v\nactual: %v", c, actualCond) 529 } 530 if c.AnsibleResult == nil && actualCond.AnsibleResult != nil { 531 t.Fatalf("Ansible result did not match expected: %v\nactual: %v", c.AnsibleResult, actualCond.AnsibleResult) 532 } 533 if c.AnsibleResult != nil { 534 if !reflect.DeepEqual(c.AnsibleResult, actualCond.AnsibleResult) { 535 t.Fatalf("Ansible result did not match expected: %v\nactual: %v", c.AnsibleResult, actualCond.AnsibleResult) 536 } 537 } 538 } 539 } 540 }) 541 } 542 }