...

Source file src/sigs.k8s.io/controller-runtime/pkg/envtest/komega/equalobject_test.go

Documentation: sigs.k8s.io/controller-runtime/pkg/envtest/komega

     1  package komega
     2  
     3  import (
     4  	"testing"
     5  
     6  	. "github.com/onsi/gomega"
     7  	appsv1 "k8s.io/api/apps/v1"
     8  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
     9  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    10  	"sigs.k8s.io/controller-runtime/pkg/client"
    11  )
    12  
    13  func TestEqualObjectMatcher(t *testing.T) {
    14  	cases := []struct {
    15  		name     string
    16  		original client.Object
    17  		modified client.Object
    18  		options  []EqualObjectOption
    19  		want     bool
    20  	}{
    21  		{
    22  			name: "succeed with equal objects",
    23  			original: &appsv1.Deployment{
    24  				ObjectMeta: metav1.ObjectMeta{
    25  					Name: "test",
    26  				},
    27  			},
    28  			modified: &appsv1.Deployment{
    29  				ObjectMeta: metav1.ObjectMeta{
    30  					Name: "test",
    31  				},
    32  			},
    33  			want: true,
    34  		},
    35  		{
    36  			name: "fail with non equal objects",
    37  			original: &appsv1.Deployment{
    38  				ObjectMeta: metav1.ObjectMeta{
    39  					Name: "test",
    40  				},
    41  			},
    42  			modified: &appsv1.Deployment{
    43  				ObjectMeta: metav1.ObjectMeta{
    44  					Name: "somethingelse",
    45  				},
    46  			},
    47  			want: false,
    48  		},
    49  		{
    50  			name: "succeeds if ignored fields do not match",
    51  			original: &appsv1.Deployment{
    52  				ObjectMeta: metav1.ObjectMeta{
    53  					Name:   "test",
    54  					Labels: map[string]string{"somelabel": "somevalue"},
    55  					OwnerReferences: []metav1.OwnerReference{{
    56  						Name: "controller",
    57  					}},
    58  				},
    59  			},
    60  			modified: &appsv1.Deployment{
    61  				ObjectMeta: metav1.ObjectMeta{
    62  					Name:   "somethingelse",
    63  					Labels: map[string]string{"somelabel": "anothervalue"},
    64  					OwnerReferences: []metav1.OwnerReference{{
    65  						Name: "another",
    66  					}},
    67  				},
    68  			},
    69  			want: true,
    70  			options: []EqualObjectOption{
    71  				IgnorePaths{
    72  					"ObjectMeta.Name",
    73  					"ObjectMeta.CreationTimestamp",
    74  					"ObjectMeta.Labels.somelabel",
    75  					"ObjectMeta.OwnerReferences[0].Name",
    76  					"Spec.Template.ObjectMeta",
    77  				},
    78  			},
    79  		},
    80  		{
    81  			name: "succeeds if ignored fields in json notation do not match",
    82  			original: &appsv1.Deployment{
    83  				ObjectMeta: metav1.ObjectMeta{
    84  					Name:   "test",
    85  					Labels: map[string]string{"somelabel": "somevalue"},
    86  					OwnerReferences: []metav1.OwnerReference{{
    87  						Name: "controller",
    88  					}},
    89  				},
    90  			},
    91  			modified: &appsv1.Deployment{
    92  				ObjectMeta: metav1.ObjectMeta{
    93  					Name:   "somethingelse",
    94  					Labels: map[string]string{"somelabel": "anothervalue"},
    95  					OwnerReferences: []metav1.OwnerReference{{
    96  						Name: "another",
    97  					}},
    98  				},
    99  			},
   100  			want: true,
   101  			options: []EqualObjectOption{
   102  				IgnorePaths{
   103  					"metadata.name",
   104  					"metadata.creationTimestamp",
   105  					"metadata.labels.somelabel",
   106  					"metadata.ownerReferences[0].name",
   107  					"spec.template.metadata",
   108  				},
   109  			},
   110  		},
   111  		{
   112  			name: "succeeds if all allowed fields match, and some others do not",
   113  			original: &appsv1.Deployment{
   114  				ObjectMeta: metav1.ObjectMeta{
   115  					Name:      "test",
   116  					Namespace: "default",
   117  				},
   118  			},
   119  			modified: &appsv1.Deployment{
   120  				ObjectMeta: metav1.ObjectMeta{
   121  					Name:      "test",
   122  					Namespace: "special",
   123  				},
   124  			},
   125  			want: true,
   126  			options: []EqualObjectOption{
   127  				MatchPaths{
   128  					"ObjectMeta.Name",
   129  				},
   130  			},
   131  		},
   132  		{
   133  			name: "works with unstructured.Unstructured",
   134  			original: &unstructured.Unstructured{
   135  				Object: map[string]interface{}{
   136  					"metadata": map[string]interface{}{
   137  						"name":      "something",
   138  						"namespace": "test",
   139  					},
   140  				},
   141  			},
   142  			modified: &unstructured.Unstructured{
   143  				Object: map[string]interface{}{
   144  					"metadata": map[string]interface{}{
   145  						"name":      "somethingelse",
   146  						"namespace": "test",
   147  					},
   148  				},
   149  			},
   150  			want: true,
   151  			options: []EqualObjectOption{
   152  				IgnorePaths{
   153  					"metadata.name",
   154  				},
   155  			},
   156  		},
   157  
   158  		// Test when objects are equal.
   159  		{
   160  			name: "Equal field (spec) both in original and in modified",
   161  			original: &unstructured.Unstructured{
   162  				Object: map[string]interface{}{
   163  					"spec": map[string]interface{}{
   164  						"foo": "bar",
   165  					},
   166  				},
   167  			},
   168  			modified: &unstructured.Unstructured{
   169  				Object: map[string]interface{}{
   170  					"spec": map[string]interface{}{
   171  						"foo": "bar",
   172  					},
   173  				},
   174  			},
   175  			want: true,
   176  		},
   177  
   178  		{
   179  			name: "Equal nested field both in original and in modified",
   180  			original: &unstructured.Unstructured{
   181  				Object: map[string]interface{}{
   182  					"spec": map[string]interface{}{
   183  						"template": map[string]interface{}{
   184  							"spec": map[string]interface{}{
   185  								"A": "A",
   186  							},
   187  						},
   188  					},
   189  				},
   190  			},
   191  			modified: &unstructured.Unstructured{
   192  				Object: map[string]interface{}{
   193  					"spec": map[string]interface{}{
   194  						"template": map[string]interface{}{
   195  							"spec": map[string]interface{}{
   196  								"A": "A",
   197  							},
   198  						},
   199  					},
   200  				},
   201  			},
   202  			want: true,
   203  		},
   204  
   205  		// Test when there is a difference between the objects.
   206  		{
   207  			name: "Unequal field both in original and in modified",
   208  			original: &unstructured.Unstructured{
   209  				Object: map[string]interface{}{
   210  					"spec": map[string]interface{}{
   211  						"foo": "bar-changed",
   212  					},
   213  				},
   214  			},
   215  			modified: &unstructured.Unstructured{
   216  				Object: map[string]interface{}{
   217  					"spec": map[string]interface{}{
   218  						"foo": "bar",
   219  					},
   220  				},
   221  			},
   222  			want: false,
   223  		},
   224  		{
   225  			name: "Unequal nested field both in original and modified",
   226  			original: &unstructured.Unstructured{
   227  				Object: map[string]interface{}{
   228  					"spec": map[string]interface{}{
   229  						"template": map[string]interface{}{
   230  							"spec": map[string]interface{}{
   231  								"A": "A-Changed",
   232  							},
   233  						},
   234  					},
   235  				},
   236  			},
   237  			modified: &unstructured.Unstructured{
   238  				Object: map[string]interface{}{
   239  					"spec": map[string]interface{}{
   240  						"template": map[string]interface{}{
   241  							"spec": map[string]interface{}{
   242  								"A": "A",
   243  							},
   244  						},
   245  					},
   246  				},
   247  			},
   248  			want: false,
   249  		},
   250  
   251  		{
   252  			name: "Value of type map with different values",
   253  			original: &unstructured.Unstructured{
   254  				Object: map[string]interface{}{
   255  					"spec": map[string]interface{}{
   256  						"map": map[string]string{
   257  							"A": "A-changed",
   258  							"B": "B",
   259  							// C missing
   260  						},
   261  					},
   262  				},
   263  			},
   264  			modified: &unstructured.Unstructured{
   265  				Object: map[string]interface{}{
   266  					"spec": map[string]interface{}{
   267  						"map": map[string]string{
   268  							"A": "A",
   269  							// B missing
   270  							"C": "C",
   271  						},
   272  					},
   273  				},
   274  			},
   275  			want: false,
   276  		},
   277  
   278  		{
   279  			name: "Value of type Array or Slice with same length but different values",
   280  			original: &unstructured.Unstructured{
   281  				Object: map[string]interface{}{
   282  					"spec": map[string]interface{}{
   283  						"slice": []string{
   284  							"D",
   285  							"C",
   286  							"B",
   287  						},
   288  					},
   289  				},
   290  			},
   291  			modified: &unstructured.Unstructured{
   292  				Object: map[string]interface{}{
   293  					"spec": map[string]interface{}{
   294  						"slice": []string{
   295  							"A",
   296  							"B",
   297  							"C",
   298  						},
   299  					},
   300  				},
   301  			},
   302  			want: false,
   303  		},
   304  
   305  		// This tests specific behaviour in how Kubernetes marshals the zero value of metav1.Time{}.
   306  		{
   307  			name: "Creation timestamp set to empty value on both original and modified",
   308  			original: &unstructured.Unstructured{
   309  				Object: map[string]interface{}{
   310  					"spec": map[string]interface{}{
   311  						"A": "A",
   312  					},
   313  					"metadata": map[string]interface{}{
   314  						"selfLink":          "foo",
   315  						"creationTimestamp": metav1.Time{},
   316  					},
   317  				},
   318  			},
   319  			modified: &unstructured.Unstructured{
   320  				Object: map[string]interface{}{
   321  					"spec": map[string]interface{}{
   322  						"A": "A",
   323  					},
   324  					"metadata": map[string]interface{}{
   325  						"selfLink":          "foo",
   326  						"creationTimestamp": metav1.Time{},
   327  					},
   328  				},
   329  			},
   330  			want: true,
   331  		},
   332  
   333  		// Cases to test diff when fields exist only in modified object.
   334  		{
   335  			name: "Field only in modified",
   336  			original: &unstructured.Unstructured{
   337  				Object: map[string]interface{}{},
   338  			},
   339  			modified: &unstructured.Unstructured{
   340  				Object: map[string]interface{}{
   341  					"spec": map[string]interface{}{
   342  						"foo": "bar",
   343  					},
   344  				},
   345  			},
   346  			want: false,
   347  		},
   348  		{
   349  			name: "Nested field only in modified",
   350  			original: &unstructured.Unstructured{
   351  				Object: map[string]interface{}{},
   352  			},
   353  			modified: &unstructured.Unstructured{
   354  				Object: map[string]interface{}{
   355  					"spec": map[string]interface{}{
   356  						"template": map[string]interface{}{
   357  							"spec": map[string]interface{}{
   358  								"A": "A",
   359  							},
   360  						},
   361  					},
   362  				},
   363  			},
   364  			want: false,
   365  		},
   366  		{
   367  			name: "Creation timestamp exists on modified but not on original",
   368  			original: &unstructured.Unstructured{
   369  				Object: map[string]interface{}{
   370  					"spec": map[string]interface{}{
   371  						"A": "A",
   372  					},
   373  				},
   374  			},
   375  			modified: &unstructured.Unstructured{
   376  				Object: map[string]interface{}{
   377  					"spec": map[string]interface{}{
   378  						"A": "A",
   379  					},
   380  					"metadata": map[string]interface{}{
   381  						"selfLink":          "foo",
   382  						"creationTimestamp": "2021-11-03T11:05:17Z",
   383  					},
   384  				},
   385  			},
   386  			want: false,
   387  		},
   388  
   389  		// Test when fields exists only in the original object.
   390  		{
   391  			name: "Field only in original",
   392  			original: &unstructured.Unstructured{
   393  				Object: map[string]interface{}{
   394  					"spec": map[string]interface{}{
   395  						"foo": "bar",
   396  					},
   397  				},
   398  			},
   399  			modified: &unstructured.Unstructured{
   400  				Object: map[string]interface{}{},
   401  			},
   402  			want: false,
   403  		},
   404  		{
   405  			name: "Nested field only in original",
   406  			original: &unstructured.Unstructured{
   407  				Object: map[string]interface{}{
   408  					"spec": map[string]interface{}{
   409  						"template": map[string]interface{}{
   410  							"spec": map[string]interface{}{
   411  								"A": "A",
   412  							},
   413  						},
   414  					},
   415  				},
   416  			},
   417  			modified: &unstructured.Unstructured{
   418  				Object: map[string]interface{}{},
   419  			},
   420  			want: false,
   421  		},
   422  		{
   423  			name: "Creation timestamp exists on original but not on modified",
   424  			original: &unstructured.Unstructured{
   425  				Object: map[string]interface{}{
   426  					"spec": map[string]interface{}{
   427  						"A": "A",
   428  					},
   429  					"metadata": map[string]interface{}{
   430  						"selfLink":          "foo",
   431  						"creationTimestamp": "2021-11-03T11:05:17Z",
   432  					},
   433  				},
   434  			},
   435  			modified: &unstructured.Unstructured{
   436  				Object: map[string]interface{}{
   437  					"spec": map[string]interface{}{
   438  						"A": "A",
   439  					},
   440  				},
   441  			},
   442  
   443  			want: false,
   444  		},
   445  
   446  		// Test metadata fields computed by the system or in status are compared.
   447  		{
   448  			name: "Unequal Metadata fields computed by the system or in status",
   449  			original: &unstructured.Unstructured{
   450  				Object: map[string]interface{}{},
   451  			},
   452  			modified: &unstructured.Unstructured{
   453  				Object: map[string]interface{}{
   454  					"metadata": map[string]interface{}{
   455  						"selfLink":        "foo",
   456  						"uid":             "foo",
   457  						"resourceVersion": "foo",
   458  						"generation":      "foo",
   459  						"managedFields":   "foo",
   460  					},
   461  					"status": map[string]interface{}{
   462  						"foo": "bar",
   463  					},
   464  				},
   465  			},
   466  			want: false,
   467  		},
   468  		{
   469  			name: "Unequal labels and annotations",
   470  			original: &unstructured.Unstructured{
   471  				Object: map[string]interface{}{},
   472  			},
   473  			modified: &unstructured.Unstructured{
   474  				Object: map[string]interface{}{
   475  					"metadata": map[string]interface{}{
   476  						"labels": map[string]interface{}{
   477  							"foo": "bar",
   478  						},
   479  						"annotations": map[string]interface{}{
   480  							"foo": "bar",
   481  						},
   482  					},
   483  				},
   484  			},
   485  			want: false,
   486  		},
   487  
   488  		// Ignore fields MatchOption
   489  		{
   490  			name: "Unequal metadata fields ignored by IgnorePaths MatchOption",
   491  			original: &unstructured.Unstructured{
   492  				Object: map[string]interface{}{
   493  					"metadata": map[string]interface{}{
   494  						"name": "test",
   495  					},
   496  				},
   497  			},
   498  			modified: &unstructured.Unstructured{
   499  				Object: map[string]interface{}{
   500  					"metadata": map[string]interface{}{
   501  						"name":            "test",
   502  						"selfLink":        "foo",
   503  						"uid":             "foo",
   504  						"resourceVersion": "foo",
   505  						"generation":      "foo",
   506  						"managedFields":   "foo",
   507  					},
   508  				},
   509  			},
   510  			options: []EqualObjectOption{IgnoreAutogeneratedMetadata},
   511  			want:    true,
   512  		},
   513  		{
   514  			name: "Unequal labels and annotations ignored by IgnorePaths MatchOption",
   515  			original: &unstructured.Unstructured{
   516  				Object: map[string]interface{}{
   517  					"metadata": map[string]interface{}{
   518  						"name": "test",
   519  					},
   520  				},
   521  			},
   522  			modified: &unstructured.Unstructured{
   523  				Object: map[string]interface{}{
   524  					"metadata": map[string]interface{}{
   525  						"name": "test",
   526  						"labels": map[string]interface{}{
   527  							"foo": "bar",
   528  						},
   529  						"annotations": map[string]interface{}{
   530  							"foo": "bar",
   531  						},
   532  					},
   533  				},
   534  			},
   535  			options: []EqualObjectOption{IgnorePaths{"metadata.labels", "metadata.annotations"}},
   536  			want:    true,
   537  		},
   538  		{
   539  			name: "Ignore fields are not compared",
   540  			original: &unstructured.Unstructured{
   541  				Object: map[string]interface{}{
   542  					"spec": map[string]interface{}{},
   543  				},
   544  			},
   545  			modified: &unstructured.Unstructured{
   546  				Object: map[string]interface{}{
   547  					"spec": map[string]interface{}{
   548  						"controlPlaneEndpoint": map[string]interface{}{
   549  							"host": "",
   550  							"port": 0,
   551  						},
   552  					},
   553  				},
   554  			},
   555  			options: []EqualObjectOption{IgnorePaths{"spec.controlPlaneEndpoint"}},
   556  			want:    true,
   557  		},
   558  		{
   559  			name: "Not-ignored fields are still compared",
   560  			original: &unstructured.Unstructured{
   561  				Object: map[string]interface{}{
   562  					"metadata": map[string]interface{}{
   563  						"annotations": map[string]interface{}{},
   564  					},
   565  				},
   566  			},
   567  			modified: &unstructured.Unstructured{
   568  				Object: map[string]interface{}{
   569  					"metadata": map[string]interface{}{
   570  						"annotations": map[string]interface{}{
   571  							"ignored":    "somevalue",
   572  							"superflous": "shouldcausefailure",
   573  						},
   574  					},
   575  				},
   576  			},
   577  			options: []EqualObjectOption{IgnorePaths{"metadata.annotations.ignored"}},
   578  			want:    false,
   579  		},
   580  
   581  		// MatchPaths MatchOption
   582  		{
   583  			name: "Unequal metadata fields not compared by setting MatchPaths MatchOption",
   584  			original: &unstructured.Unstructured{
   585  				Object: map[string]interface{}{
   586  					"spec": map[string]interface{}{
   587  						"A": "A",
   588  					},
   589  				},
   590  			},
   591  			modified: &unstructured.Unstructured{
   592  				Object: map[string]interface{}{
   593  					"spec": map[string]interface{}{
   594  						"A": "A",
   595  					},
   596  					"metadata": map[string]interface{}{
   597  						"selfLink": "foo",
   598  						"uid":      "foo",
   599  					},
   600  				},
   601  			},
   602  			options: []EqualObjectOption{MatchPaths{"spec"}},
   603  			want:    true,
   604  		},
   605  
   606  		// More tests
   607  		{
   608  			name: "No changes",
   609  			original: &unstructured.Unstructured{
   610  				Object: map[string]interface{}{
   611  					"spec": map[string]interface{}{
   612  						"A": "A",
   613  						"B": "B",
   614  						"C": "C", // C only in original
   615  					},
   616  				},
   617  			},
   618  			modified: &unstructured.Unstructured{
   619  				Object: map[string]interface{}{
   620  					"spec": map[string]interface{}{
   621  						"A": "A",
   622  						"B": "B",
   623  					},
   624  				},
   625  			},
   626  			want: false,
   627  		},
   628  		{
   629  			name: "Many changes",
   630  			original: &unstructured.Unstructured{
   631  				Object: map[string]interface{}{
   632  					"spec": map[string]interface{}{
   633  						"A": "A",
   634  						// B missing
   635  						"C": "C", // C only in original
   636  					},
   637  				},
   638  			},
   639  			modified: &unstructured.Unstructured{
   640  				Object: map[string]interface{}{
   641  					"spec": map[string]interface{}{
   642  						"A": "A",
   643  						"B": "B",
   644  					},
   645  				},
   646  			},
   647  			want: false,
   648  		},
   649  	}
   650  
   651  	for _, c := range cases {
   652  		t.Run(c.name, func(t *testing.T) {
   653  			g := NewWithT(t)
   654  			m := EqualObject(c.original, c.options...)
   655  			success, _ := m.Match(c.modified)
   656  			if !success {
   657  				t.Log(m.FailureMessage(c.modified))
   658  			}
   659  			g.Expect(success).To(Equal(c.want))
   660  		})
   661  	}
   662  }
   663  

View as plain text