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  }