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 }