sigs.k8s.io/cluster-api@v1.7.1/internal/controllers/topology/cluster/structuredmerge/twowayspatchhelper_test.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package structuredmerge
    18  
    19  import (
    20  	"fmt"
    21  	"testing"
    22  
    23  	. "github.com/onsi/gomega"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    26  
    27  	"sigs.k8s.io/cluster-api/internal/contract"
    28  	"sigs.k8s.io/cluster-api/internal/test/builder"
    29  )
    30  
    31  func TestNewHelper(t *testing.T) {
    32  	tests := []struct {
    33  		name               string
    34  		original           *unstructured.Unstructured // current
    35  		modified           *unstructured.Unstructured // desired
    36  		options            []HelperOption
    37  		wantHasChanges     bool
    38  		wantHasSpecChanges bool
    39  		wantPatch          []byte
    40  	}{
    41  		// Create
    42  
    43  		{
    44  			name:     "Create if original does not exists",
    45  			original: nil,
    46  			modified: &unstructured.Unstructured{ // desired
    47  				Object: map[string]interface{}{
    48  					"apiVersion": builder.BootstrapGroupVersion.String(),
    49  					"kind":       builder.GenericBootstrapConfigKind,
    50  					"metadata": map[string]interface{}{
    51  						"namespace": metav1.NamespaceDefault,
    52  						"name":      "foo",
    53  					},
    54  					"spec": map[string]interface{}{
    55  						"foo": "foo",
    56  					},
    57  				},
    58  			},
    59  			options:            []HelperOption{},
    60  			wantHasChanges:     true,
    61  			wantHasSpecChanges: true,
    62  			wantPatch:          []byte(fmt.Sprintf("{\"apiVersion\":%q,\"kind\":%q,\"metadata\":{\"name\":\"foo\",\"namespace\":%q},\"spec\":{\"foo\":\"foo\"}}", builder.BootstrapGroupVersion.String(), builder.GenericBootstrapConfigKind, metav1.NamespaceDefault)),
    63  		},
    64  
    65  		// Ignore fields
    66  
    67  		{
    68  			name: "Ignore fields are removed from the patch",
    69  			original: &unstructured.Unstructured{ // current
    70  				Object: map[string]interface{}{},
    71  			},
    72  			modified: &unstructured.Unstructured{ // desired
    73  				Object: map[string]interface{}{
    74  					"spec": map[string]interface{}{
    75  						"controlPlaneEndpoint": map[string]interface{}{
    76  							"host": "",
    77  							"port": int64(0),
    78  						},
    79  					},
    80  				},
    81  			},
    82  			options:            []HelperOption{IgnorePaths{contract.Path{"spec", "controlPlaneEndpoint"}}},
    83  			wantHasChanges:     false,
    84  			wantHasSpecChanges: false,
    85  			wantPatch:          []byte("{}"),
    86  		},
    87  
    88  		// Allowed Path fields
    89  
    90  		{
    91  			name: "Not allowed fields are removed from the patch",
    92  			original: &unstructured.Unstructured{ // current
    93  				Object: map[string]interface{}{},
    94  			},
    95  			modified: &unstructured.Unstructured{ // desired
    96  				Object: map[string]interface{}{
    97  					"status": map[string]interface{}{
    98  						"foo": "foo",
    99  					},
   100  				},
   101  			},
   102  			wantHasChanges:     false,
   103  			wantHasSpecChanges: false,
   104  			wantPatch:          []byte("{}"),
   105  		},
   106  
   107  		// Field both in original and in modified --> align to modified if different
   108  
   109  		{
   110  			name: "Field (spec.foo) both in original and in modified, no-op when equal",
   111  			original: &unstructured.Unstructured{ // current
   112  				Object: map[string]interface{}{
   113  					"spec": map[string]interface{}{
   114  						"foo": "foo",
   115  					},
   116  				},
   117  			},
   118  			modified: &unstructured.Unstructured{ // desired
   119  				Object: map[string]interface{}{
   120  					"spec": map[string]interface{}{
   121  						"foo": "foo",
   122  					},
   123  				},
   124  			},
   125  			wantHasChanges:     false,
   126  			wantHasSpecChanges: false,
   127  			wantPatch:          []byte("{}"),
   128  		},
   129  		{
   130  			name: "Field (metadata.label) both in original and in modified, align to modified when different",
   131  			original: &unstructured.Unstructured{ // current
   132  				Object: map[string]interface{}{
   133  					"metadata": map[string]interface{}{
   134  						"labels": map[string]interface{}{
   135  							"foo": "foo",
   136  						},
   137  					},
   138  				},
   139  			},
   140  			modified: &unstructured.Unstructured{ // desired
   141  				Object: map[string]interface{}{
   142  					"metadata": map[string]interface{}{
   143  						"labels": map[string]interface{}{
   144  							"foo": "foo-modified",
   145  						},
   146  					},
   147  				},
   148  			},
   149  			wantHasChanges:     true,
   150  			wantHasSpecChanges: false,
   151  			wantPatch:          []byte("{\"metadata\":{\"labels\":{\"foo\":\"foo-modified\"}}}"),
   152  		},
   153  		{
   154  			name: "Field (spec.template.spec.foo) both in original and in modified, no-op when equal",
   155  			original: &unstructured.Unstructured{ // current
   156  				Object: map[string]interface{}{
   157  					"spec": map[string]interface{}{
   158  						"template": map[string]interface{}{
   159  							"spec": map[string]interface{}{
   160  								"foo": "foo",
   161  							},
   162  						},
   163  					},
   164  				},
   165  			},
   166  			modified: &unstructured.Unstructured{ // desired
   167  				Object: map[string]interface{}{
   168  					"spec": map[string]interface{}{
   169  						"template": map[string]interface{}{
   170  							"spec": map[string]interface{}{
   171  								"foo": "foo",
   172  							},
   173  						},
   174  					},
   175  				},
   176  			},
   177  			wantHasChanges:     false,
   178  			wantHasSpecChanges: false,
   179  			wantPatch:          []byte("{}"),
   180  		},
   181  
   182  		{
   183  			name: "Field (spec.foo) both in original and in modified, align to modified when different",
   184  			original: &unstructured.Unstructured{ // current
   185  				Object: map[string]interface{}{
   186  					"spec": map[string]interface{}{
   187  						"foo": "foo",
   188  					},
   189  				},
   190  			},
   191  			modified: &unstructured.Unstructured{ // desired
   192  				Object: map[string]interface{}{
   193  					"spec": map[string]interface{}{
   194  						"foo": "foo-changed",
   195  					},
   196  				},
   197  			},
   198  			wantHasChanges:     true,
   199  			wantHasSpecChanges: true,
   200  			wantPatch:          []byte("{\"spec\":{\"foo\":\"foo-changed\"}}"),
   201  		},
   202  		{
   203  			name: "Field (metadata.label) both in original and in modified, align to modified when different",
   204  			original: &unstructured.Unstructured{ // current
   205  				Object: map[string]interface{}{
   206  					"metadata": map[string]interface{}{
   207  						"labels": map[string]interface{}{
   208  							"foo": "foo",
   209  						},
   210  					},
   211  				},
   212  			},
   213  			modified: &unstructured.Unstructured{ // desired
   214  				Object: map[string]interface{}{
   215  					"metadata": map[string]interface{}{
   216  						"labels": map[string]interface{}{
   217  							"foo": "foo-changed",
   218  						},
   219  					},
   220  				},
   221  			},
   222  			wantHasChanges:     true,
   223  			wantHasSpecChanges: false,
   224  			wantPatch:          []byte("{\"metadata\":{\"labels\":{\"foo\":\"foo-changed\"}}}"),
   225  		},
   226  		{
   227  			name: "Field (spec.template.spec.foo) both in original and in modified, align to modified when different",
   228  			original: &unstructured.Unstructured{ // current
   229  				Object: map[string]interface{}{
   230  					"spec": map[string]interface{}{
   231  						"template": map[string]interface{}{
   232  							"spec": map[string]interface{}{
   233  								"foo": "foo",
   234  							},
   235  						},
   236  					},
   237  				},
   238  			},
   239  			modified: &unstructured.Unstructured{ // desired
   240  				Object: map[string]interface{}{
   241  					"spec": map[string]interface{}{
   242  						"template": map[string]interface{}{
   243  							"spec": map[string]interface{}{
   244  								"foo": "foo-changed",
   245  							},
   246  						},
   247  					},
   248  				},
   249  			},
   250  			wantHasChanges:     true,
   251  			wantHasSpecChanges: true,
   252  			wantPatch:          []byte("{\"spec\":{\"template\":{\"spec\":{\"foo\":\"foo-changed\"}}}}"),
   253  		},
   254  
   255  		{
   256  			name: "Value of type Array or Slice both in original and in modified,, align to modified when different", // Note: fake treats all the slice as atomic (false positive)
   257  			original: &unstructured.Unstructured{
   258  				Object: map[string]interface{}{
   259  					"spec": map[string]interface{}{
   260  						"slice": []interface{}{
   261  							"D",
   262  							"C",
   263  							"B",
   264  						},
   265  					},
   266  				},
   267  			},
   268  			modified: &unstructured.Unstructured{
   269  				Object: map[string]interface{}{
   270  					"spec": map[string]interface{}{
   271  						"slice": []interface{}{
   272  							"A",
   273  							"B",
   274  							"C",
   275  						},
   276  					},
   277  				},
   278  			},
   279  			wantHasChanges:     true,
   280  			wantHasSpecChanges: true,
   281  			wantPatch:          []byte("{\"spec\":{\"slice\":[\"A\",\"B\",\"C\"]}}"),
   282  		},
   283  
   284  		// Field only in modified (not existing in original) --> align to modified
   285  
   286  		{
   287  			name: "Field (spec.foo) in modified only, align to modified",
   288  			original: &unstructured.Unstructured{ // current
   289  				Object: map[string]interface{}{},
   290  			},
   291  			modified: &unstructured.Unstructured{ // desired
   292  				Object: map[string]interface{}{
   293  					"spec": map[string]interface{}{
   294  						"foo": "foo-changed",
   295  					},
   296  				},
   297  			},
   298  			wantHasChanges:     true,
   299  			wantHasSpecChanges: true,
   300  			wantPatch:          []byte("{\"spec\":{\"foo\":\"foo-changed\"}}"),
   301  		},
   302  		{
   303  			name: "Field (metadata.label) in modified only, align to modified",
   304  			original: &unstructured.Unstructured{ // current
   305  				Object: map[string]interface{}{},
   306  			},
   307  			modified: &unstructured.Unstructured{ // desired
   308  				Object: map[string]interface{}{
   309  					"metadata": map[string]interface{}{
   310  						"labels": map[string]interface{}{
   311  							"foo": "foo-changed",
   312  						},
   313  					},
   314  				},
   315  			},
   316  			wantHasChanges:     true,
   317  			wantHasSpecChanges: false,
   318  			wantPatch:          []byte("{\"metadata\":{\"labels\":{\"foo\":\"foo-changed\"}}}"),
   319  		},
   320  		{
   321  			name: "Field (spec.template.spec.foo) in modified only, align to modified when different",
   322  			original: &unstructured.Unstructured{ // current
   323  				Object: map[string]interface{}{},
   324  			},
   325  			modified: &unstructured.Unstructured{ // desired
   326  				Object: map[string]interface{}{
   327  					"spec": map[string]interface{}{
   328  						"template": map[string]interface{}{
   329  							"spec": map[string]interface{}{
   330  								"foo": "foo-changed",
   331  							},
   332  						},
   333  					},
   334  				},
   335  			},
   336  			wantHasChanges:     true,
   337  			wantHasSpecChanges: true,
   338  			wantPatch:          []byte("{\"spec\":{\"template\":{\"spec\":{\"foo\":\"foo-changed\"}}}}"),
   339  		},
   340  
   341  		{
   342  			name: "Value of type Array or Slice in modified only, align to modified when different",
   343  			original: &unstructured.Unstructured{
   344  				Object: map[string]interface{}{},
   345  			},
   346  			modified: &unstructured.Unstructured{
   347  				Object: map[string]interface{}{
   348  					"spec": map[string]interface{}{
   349  						"slice": []interface{}{
   350  							"A",
   351  							"B",
   352  							"C",
   353  						},
   354  					},
   355  				},
   356  			},
   357  			wantHasChanges:     true,
   358  			wantHasSpecChanges: true,
   359  			wantPatch:          []byte("{\"spec\":{\"slice\":[\"A\",\"B\",\"C\"]}}"),
   360  		},
   361  
   362  		// Field only in original (not existing in modified) --> preserve original
   363  
   364  		{
   365  			name: "Field (spec.foo) in original only, preserve", // Note: fake can't detect if has been originated from templates or from external controllers, so it assumes  (false negative)
   366  			original: &unstructured.Unstructured{ // current
   367  				Object: map[string]interface{}{
   368  					"spec": map[string]interface{}{
   369  						"foo": "foo",
   370  					},
   371  				},
   372  			},
   373  			modified: &unstructured.Unstructured{ // desired
   374  				Object: map[string]interface{}{},
   375  			},
   376  			wantHasChanges:     false,
   377  			wantHasSpecChanges: false,
   378  			wantPatch:          []byte("{}"),
   379  		},
   380  		{
   381  			name: "Field (metadata.label) in original only, preserve", // Note: fake can't detect if has been originated from templates or from external controllers (false negative)
   382  			original: &unstructured.Unstructured{ // current
   383  				Object: map[string]interface{}{
   384  					"metadata": map[string]interface{}{
   385  						"labels": map[string]interface{}{
   386  							"foo": "foo",
   387  						},
   388  					},
   389  				},
   390  			},
   391  			modified: &unstructured.Unstructured{ // desired
   392  				Object: map[string]interface{}{},
   393  			},
   394  			wantHasChanges:     false,
   395  			wantHasSpecChanges: false,
   396  			wantPatch:          []byte("{}"),
   397  		},
   398  		{
   399  			name: "Field (spec.template.spec.foo) in original only, preserve", // Note: fake can't detect if has been originated from templates or from external controllers (false negative)
   400  			original: &unstructured.Unstructured{ // current
   401  				Object: map[string]interface{}{
   402  					"spec": map[string]interface{}{
   403  						"template": map[string]interface{}{
   404  							"spec": map[string]interface{}{
   405  								"foo": "foo",
   406  							},
   407  						},
   408  					},
   409  				},
   410  			},
   411  			modified: &unstructured.Unstructured{ // desired
   412  				Object: map[string]interface{}{},
   413  			},
   414  			wantHasChanges:     false,
   415  			wantHasSpecChanges: false,
   416  			wantPatch:          []byte("{}"),
   417  		},
   418  
   419  		{
   420  			name: "Value of type Array or Slice in original only, preserve", // Note: fake can't detect if has been originated from templates or from external controllers (false negative)
   421  			original: &unstructured.Unstructured{
   422  				Object: map[string]interface{}{
   423  					"spec": map[string]interface{}{
   424  						"slice": []interface{}{
   425  							"D",
   426  							"C",
   427  							"B",
   428  						},
   429  					},
   430  				},
   431  			},
   432  			modified: &unstructured.Unstructured{
   433  				Object: map[string]interface{}{},
   434  			},
   435  			wantHasChanges:     false,
   436  			wantHasSpecChanges: false,
   437  			wantPatch:          []byte("{}"),
   438  		},
   439  	}
   440  	for _, tt := range tests {
   441  		t.Run(tt.name, func(t *testing.T) {
   442  			g := NewWithT(t)
   443  
   444  			patch, err := NewTwoWaysPatchHelper(tt.original, tt.modified, env.GetClient(), tt.options...)
   445  			g.Expect(err).ToNot(HaveOccurred())
   446  
   447  			g.Expect(patch.patch).To(Equal(tt.wantPatch))
   448  			g.Expect(patch.HasChanges()).To(Equal(tt.wantHasChanges))
   449  			g.Expect(patch.HasSpecChanges()).To(Equal(tt.wantHasSpecChanges))
   450  		})
   451  	}
   452  }