github.com/vmware/go-vcloud-director/v2@v2.24.0/govcd/cse_yaml_unit_test.go (about) 1 //go:build unit || ALL 2 3 package govcd 4 5 import ( 6 semver "github.com/hashicorp/go-version" 7 "github.com/vmware/go-vcloud-director/v2/types/v56" 8 "os" 9 "reflect" 10 "strings" 11 "testing" 12 ) 13 14 // Test_cseUpdateKubernetesTemplateInYaml tests the update process of the Kubernetes template OVA in a CAPI YAML. 15 func Test_cseUpdateKubernetesTemplateInYaml(t *testing.T) { 16 capiYaml, err := os.ReadFile("test-resources/capiYaml.yaml") 17 if err != nil { 18 t.Fatalf("could not read CAPI YAML test file: %s", err) 19 } 20 21 yamlDocs, err := unmarshalMultipleYamlDocuments(string(capiYaml)) 22 if err != nil { 23 t.Fatalf("could not unmarshal CAPI YAML test file: %s", err) 24 } 25 26 oldOvaVersion := "v1.25.7+vmware.2-tkg.1-8a74b9f12e488c54605b3537acb683bc" // Matches the version in capiYaml.yaml 27 if strings.Count(string(capiYaml), oldOvaVersion) < 2 { 28 t.Fatalf("the testing YAML doesn't contain the OVA to change") 29 } 30 31 oldTkgBundle, err := getTkgVersionBundleFromVAppTemplate(&types.VAppTemplate{ 32 Name: "dummy", 33 Children: &types.VAppTemplateChildren{VM: []*types.VAppTemplate{ 34 { 35 ProductSection: &types.ProductSection{ 36 Property: []*types.Property{ 37 { 38 Key: "VERSION", 39 DefaultValue: oldOvaVersion, 40 }, 41 }, 42 }, 43 }, 44 }}, 45 }) 46 if err != nil { 47 t.Fatal(err) 48 } 49 50 // We call the function to update the old OVA with the new one 51 newOva := &types.VAppTemplate{ 52 ID: "urn:vcloud:vapptemplate:e23b8a5c-e676-4d82-9050-c906a3ac2fea", 53 Name: "dummy", 54 Children: &types.VAppTemplateChildren{VM: []*types.VAppTemplate{ 55 { 56 ProductSection: &types.ProductSection{ 57 Property: []*types.Property{ 58 { 59 Key: "VERSION", 60 DefaultValue: "v1.19.16+vmware.1-tkg.2-fba68db15591c15fcd5f26b512663a42", 61 }, 62 }, 63 }, 64 }, 65 }}, 66 } 67 newTkgBundle, err := getTkgVersionBundleFromVAppTemplate(newOva) 68 if err != nil { 69 t.Fatal(err) 70 } 71 72 err = cseUpdateKubernetesTemplateInYaml(yamlDocs, newOva) 73 if err != nil { 74 t.Fatal(err) 75 } 76 77 updatedYaml, err := marshalMultipleYamlDocuments(yamlDocs) 78 if err != nil { 79 t.Fatalf("error marshaling %v: %s", yamlDocs, err) 80 } 81 82 // No document should have the old OVA 83 if strings.Count(updatedYaml, newOva.Name) < 2 || strings.Contains(updatedYaml, oldOvaVersion) { 84 t.Fatalf("failed updating the Kubernetes OVA template:\n%s", updatedYaml) 85 } 86 if !strings.Contains(updatedYaml, newTkgBundle.KubernetesVersion) || strings.Contains(updatedYaml, oldTkgBundle.KubernetesVersion) { 87 t.Fatalf("failed updating the Kubernetes version:\n%s", updatedYaml) 88 } 89 if !strings.Contains(updatedYaml, newTkgBundle.TkrVersion) || strings.Contains(updatedYaml, oldTkgBundle.TkrVersion) { 90 t.Fatalf("failed updating the Tanzu release version:\n%s", updatedYaml) 91 } 92 if !strings.Contains(updatedYaml, newTkgBundle.TkgVersion) || strings.Contains(updatedYaml, oldTkgBundle.TkgVersion) { 93 t.Fatalf("failed updating the Tanzu grid version:\n%s", updatedYaml) 94 } 95 if !strings.Contains(updatedYaml, newTkgBundle.CoreDnsVersion) || strings.Contains(updatedYaml, oldTkgBundle.CoreDnsVersion) { 96 t.Fatalf("failed updating the CoreDNS version:\n%s", updatedYaml) 97 } 98 if !strings.Contains(updatedYaml, newTkgBundle.EtcdVersion) || strings.Contains(updatedYaml, oldTkgBundle.EtcdVersion) { 99 t.Fatalf("failed updating the Etcd version:\n%s", updatedYaml) 100 } 101 } 102 103 // Test_cseUpdateWorkerPoolsInYaml tests the update process of the Worker pools in a CAPI YAML. 104 func Test_cseUpdateWorkerPoolsInYaml(t *testing.T) { 105 capiYaml, err := os.ReadFile("test-resources/capiYaml.yaml") 106 if err != nil { 107 t.Fatalf("could not read CAPI YAML test file: %s", err) 108 } 109 110 yamlDocs, err := unmarshalMultipleYamlDocuments(string(capiYaml)) 111 if err != nil { 112 t.Fatalf("could not unmarshal CAPI YAML test file: %s", err) 113 } 114 // We explore the YAML documents to get the current Worker pool 115 oldNodePools := map[string]CseWorkerPoolUpdateInput{} 116 for _, document := range yamlDocs { 117 if document["kind"] != "MachineDeployment" { 118 continue 119 } 120 121 workerPoolName := traverseMapAndGet[string](document, "metadata.name") 122 if workerPoolName == "" { 123 t.Fatalf("incorrect CAPI YAML: %s", err) 124 } 125 126 oldNodePools[workerPoolName] = CseWorkerPoolUpdateInput{ 127 MachineCount: int(traverseMapAndGet[float64](document, "spec.replicas")), 128 } 129 } 130 if len(oldNodePools) == 0 { 131 t.Fatalf("didn't get any valid worker node pool") 132 } 133 134 // We call the function to update the old pools with the new ones 135 newReplicas := 66 136 newNodePools := map[string]CseWorkerPoolUpdateInput{} 137 for name := range oldNodePools { 138 newNodePools[name] = CseWorkerPoolUpdateInput{ 139 MachineCount: newReplicas, 140 } 141 } 142 err = cseUpdateWorkerPoolsInYaml(yamlDocs, newNodePools) 143 if err != nil { 144 t.Fatal(err) 145 } 146 147 // The worker pools should have now the new details updated 148 for _, document := range yamlDocs { 149 if document["kind"] != "MachineDeployment" { 150 continue 151 } 152 153 retrievedReplicas := traverseMapAndGet[float64](document, "spec.replicas") 154 if traverseMapAndGet[float64](document, "spec.replicas") != float64(newReplicas) { 155 t.Fatalf("expected %d replicas but got %0.f", newReplicas, retrievedReplicas) 156 } 157 } 158 159 // Corner case: Wrong replicas 160 newReplicas = -1 161 newNodePools = map[string]CseWorkerPoolUpdateInput{} 162 for name := range oldNodePools { 163 newNodePools[name] = CseWorkerPoolUpdateInput{ 164 MachineCount: newReplicas, 165 } 166 } 167 err = cseUpdateWorkerPoolsInYaml(yamlDocs, newNodePools) 168 if err == nil { 169 t.Fatal("Expected an error, but got none") 170 } 171 172 // Corner case: No worker pool with that name exists 173 newNodePools = map[string]CseWorkerPoolUpdateInput{ 174 "not-exist": {}, 175 } 176 err = cseUpdateWorkerPoolsInYaml(yamlDocs, newNodePools) 177 if err == nil { 178 t.Fatal("Expected an error, but got none") 179 } 180 } 181 182 // Test_cseUpdateControlPlaneInYaml tests the update process of the Control Plane in a CAPI YAML. 183 func Test_cseUpdateControlPlaneInYaml(t *testing.T) { 184 capiYaml, err := os.ReadFile("test-resources/capiYaml.yaml") 185 if err != nil { 186 t.Fatalf("could not read CAPI YAML test file: %s", err) 187 } 188 189 yamlDocs, err := unmarshalMultipleYamlDocuments(string(capiYaml)) 190 if err != nil { 191 t.Fatalf("could not unmarshal CAPI YAML test file: %s", err) 192 } 193 // We explore the YAML documents to get the current Control plane 194 oldControlPlane := CseControlPlaneUpdateInput{} 195 for _, document := range yamlDocs { 196 if document["kind"] != "KubeadmControlPlane" { 197 continue 198 } 199 200 oldControlPlane = CseControlPlaneUpdateInput{ 201 MachineCount: int(traverseMapAndGet[float64](document, "spec.replicas")), 202 } 203 } 204 if reflect.DeepEqual(oldControlPlane, CseWorkerPoolUpdateInput{}) { 205 t.Fatalf("didn't get any valid Control Plane") 206 } 207 208 // We call the function to update the control plane 209 newReplicas := 67 210 newControlPlane := CseControlPlaneUpdateInput{ 211 MachineCount: newReplicas, 212 } 213 err = cseUpdateControlPlaneInYaml(yamlDocs, newControlPlane) 214 if err != nil { 215 t.Fatal(err) 216 } 217 218 // The control plane should have now the new details updated 219 for _, document := range yamlDocs { 220 if document["kind"] != "KubeadmControlPlane" { 221 continue 222 } 223 224 retrievedReplicas := traverseMapAndGet[float64](document, "spec.replicas") 225 if retrievedReplicas != float64(newReplicas) { 226 t.Fatalf("expected %d replicas but got %0.f", newReplicas, retrievedReplicas) 227 } 228 } 229 230 // Corner case: Wrong replicas 231 newReplicas = -1 232 newControlPlane = CseControlPlaneUpdateInput{ 233 MachineCount: newReplicas, 234 } 235 err = cseUpdateControlPlaneInYaml(yamlDocs, newControlPlane) 236 if err == nil { 237 t.Fatal("Expected an error, but got none") 238 } 239 240 newReplicas = 2 // Should be odd, hence fails 241 newControlPlane = CseControlPlaneUpdateInput{ 242 MachineCount: newReplicas, 243 } 244 err = cseUpdateControlPlaneInYaml(yamlDocs, newControlPlane) 245 if err == nil { 246 t.Fatal("Expected an error, but got none") 247 } 248 } 249 250 // Test_cseUpdateNodeHealthCheckInYaml tests the update process of the Machine Health Check capabilities in a CAPI YAML. 251 func Test_cseUpdateNodeHealthCheckInYaml(t *testing.T) { 252 capiYaml, err := os.ReadFile("test-resources/capiYaml.yaml") 253 if err != nil { 254 t.Fatalf("could not read CAPI YAML test file: %s", err) 255 } 256 257 yamlDocs, err := unmarshalMultipleYamlDocuments(string(capiYaml)) 258 if err != nil { 259 t.Fatalf("could not unmarshal CAPI YAML test file: %s", err) 260 } 261 262 clusterName := "" 263 for _, doc := range yamlDocs { 264 if doc["kind"] != "Cluster" { 265 continue 266 } 267 clusterName = traverseMapAndGet[string](doc, "metadata.name") 268 } 269 if clusterName == "" { 270 t.Fatal("could not find the cluster name in the CAPI YAML test file") 271 } 272 273 v, err := semver.NewVersion("4.1") 274 if err != nil { 275 t.Fatalf("incorrect version: %s", err) 276 } 277 278 // Deactivates Machine Health Check 279 yamlDocs, err = cseUpdateNodeHealthCheckInYaml(yamlDocs, clusterName, *v, vcdKeConfig{}) 280 if err != nil { 281 t.Fatal(err) 282 } 283 284 // The resulting documents should not have that document 285 for _, document := range yamlDocs { 286 if document["kind"] == "MachineHealthCheck" { 287 t.Fatal("Expected the MachineHealthCheck to be deleted, but it is there") 288 } 289 } 290 291 // Enables Machine Health Check 292 yamlDocs, err = cseUpdateNodeHealthCheckInYaml(yamlDocs, clusterName, *v, vcdKeConfig{ 293 MaxUnhealthyNodesPercentage: 12, 294 NodeStartupTimeout: "34", 295 NodeNotReadyTimeout: "56", 296 NodeUnknownTimeout: "78", 297 }) 298 if err != nil { 299 t.Fatal(err) 300 } 301 302 // The resulting documents should have a MachineHealthCheck 303 found := false 304 for _, document := range yamlDocs { 305 if document["kind"] != "MachineHealthCheck" { 306 continue 307 } 308 maxUnhealthy := traverseMapAndGet[string](document, "spec.maxUnhealthy") 309 if maxUnhealthy != "12%" { 310 t.Fatalf("expected a 'spec.maxUnhealthy' = 12%%, but got %s", maxUnhealthy) 311 } 312 nodeStartupTimeout := traverseMapAndGet[string](document, "spec.nodeStartupTimeout") 313 if nodeStartupTimeout != "34s" { 314 t.Fatalf("expected a 'spec.nodeStartupTimeout' = 34s, but got %s", nodeStartupTimeout) 315 } 316 found = true 317 } 318 if !found { 319 t.Fatalf("expected a MachineHealthCheck block but got nothing") 320 } 321 } 322 323 // Test_unmarshalMultplieYamlDocuments tests the unmarshalling of multiple YAML documents with unmarshalMultplieYamlDocuments 324 func Test_unmarshalMultplieYamlDocuments(t *testing.T) { 325 capiYaml, err := os.ReadFile("test-resources/capiYaml.yaml") 326 if err != nil { 327 t.Fatalf("could not read YAML test file: %s", err) 328 } 329 330 tests := []struct { 331 name string 332 yamlDocuments string 333 want int 334 wantErr bool 335 }{ 336 { 337 name: "unmarshal correct amount of documents", 338 yamlDocuments: string(capiYaml), 339 want: 8, 340 wantErr: false, 341 }, 342 { 343 name: "unmarshal single yaml document", 344 yamlDocuments: "test: foo", 345 want: 1, 346 wantErr: false, 347 }, 348 { 349 name: "unmarshal empty yaml document", 350 yamlDocuments: "", 351 want: 0, 352 }, 353 { 354 name: "unmarshal wrong yaml document", 355 yamlDocuments: "thisIsNotAYaml", 356 wantErr: true, 357 }, 358 } 359 for _, tt := range tests { 360 t.Run(tt.name, func(t *testing.T) { 361 got, err := unmarshalMultipleYamlDocuments(tt.yamlDocuments) 362 if (err != nil) != tt.wantErr { 363 t.Errorf("unmarshalMultplieYamlDocuments() error = %v, wantErr %v", err, tt.wantErr) 364 return 365 } 366 if len(got) != tt.want { 367 t.Errorf("unmarshalMultplieYamlDocuments() got %d documents, want %d", len(got), tt.want) 368 } 369 }) 370 } 371 } 372 373 // Test_marshalMultplieYamlDocuments tests the marshalling of multiple YAML documents with marshalMultplieYamlDocuments 374 func Test_marshalMultplieYamlDocuments(t *testing.T) { 375 capiYaml, err := os.ReadFile("test-resources/capiYaml.yaml") 376 if err != nil { 377 t.Fatalf("could not read YAML test file: %s", err) 378 } 379 380 unmarshaledCapiYaml, err := unmarshalMultipleYamlDocuments(string(capiYaml)) 381 if err != nil { 382 t.Fatalf("could not unmarshal the YAML test file: %s", err) 383 } 384 385 tests := []struct { 386 name string 387 yamlDocuments []map[string]interface{} 388 want []map[string]interface{} 389 wantErr bool 390 }{ 391 { 392 name: "marshal correct amount of documents", 393 yamlDocuments: unmarshaledCapiYaml, 394 want: unmarshaledCapiYaml, 395 wantErr: false, 396 }, 397 { 398 name: "marshal empty slice", 399 yamlDocuments: []map[string]interface{}{}, 400 want: []map[string]interface{}{}, 401 wantErr: false, 402 }, 403 } 404 for _, tt := range tests { 405 t.Run(tt.name, func(t *testing.T) { 406 got, err := marshalMultipleYamlDocuments(tt.yamlDocuments) 407 if (err != nil) != tt.wantErr { 408 t.Errorf("marshalMultipleYamlDocuments() error = %v, wantErr %v", err, tt.wantErr) 409 return 410 } 411 gotUnmarshaled, err := unmarshalMultipleYamlDocuments(got) // We unmarshal the result to compare it exactly with DeepEqual 412 if err != nil { 413 t.Errorf("unmarshalMultipleYamlDocuments() failed %s", err) 414 return 415 } 416 if !reflect.DeepEqual(gotUnmarshaled, tt.want) { 417 t.Errorf("marshalMultipleYamlDocuments() got =\n%v, want =\n%v", gotUnmarshaled, tt.want) 418 } 419 }) 420 } 421 } 422 423 // Test_traverseMapAndGet tests traverseMapAndGet function 424 func Test_traverseMapAndGet(t *testing.T) { 425 type args struct { 426 input interface{} 427 path string 428 } 429 tests := []struct { 430 name string 431 args args 432 wantType string 433 want interface{} 434 }{ 435 { 436 name: "input is nil", 437 args: args{ 438 input: nil, 439 }, 440 wantType: "string", 441 want: "", 442 }, 443 { 444 name: "input is not a map", 445 args: args{ 446 input: "error", 447 }, 448 wantType: "string", 449 want: "", 450 }, 451 { 452 name: "map is empty", 453 args: args{ 454 input: map[string]interface{}{}, 455 }, 456 wantType: "float64", 457 want: float64(0), 458 }, 459 { 460 name: "map does not have key", 461 args: args{ 462 input: map[string]interface{}{ 463 "keyA": "value", 464 }, 465 path: "keyB", 466 }, 467 wantType: "string", 468 want: "", 469 }, 470 { 471 name: "map has a single simple key", 472 args: args{ 473 input: map[string]interface{}{ 474 "keyA": "value", 475 }, 476 path: "keyA", 477 }, 478 wantType: "string", 479 want: "value", 480 }, 481 { 482 name: "map has a single complex key", 483 args: args{ 484 input: map[string]interface{}{ 485 "keyA": map[string]interface{}{ 486 "keyB": "value", 487 }, 488 }, 489 path: "keyA", 490 }, 491 wantType: "map", 492 want: map[string]interface{}{ 493 "keyB": "value", 494 }, 495 }, 496 { 497 name: "map has a complex structure", 498 args: args{ 499 input: map[string]interface{}{ 500 "keyA": map[string]interface{}{ 501 "keyB": map[string]interface{}{ 502 "keyC": "value", 503 }, 504 }, 505 }, 506 path: "keyA.keyB.keyC", 507 }, 508 wantType: "string", 509 want: "value", 510 }, 511 { 512 name: "requested path is deeper than the map structure", 513 args: args{ 514 input: map[string]interface{}{ 515 "keyA": map[string]interface{}{ 516 "keyB": map[string]interface{}{ 517 "keyC": "value", 518 }, 519 }, 520 }, 521 path: "keyA.keyB.keyC.keyD", 522 }, 523 wantType: "string", 524 want: "", 525 }, 526 { 527 name: "obtained value does not correspond to the desired type", 528 args: args{ 529 input: map[string]interface{}{ 530 "keyA": map[string]interface{}{ 531 "keyB": map[string]interface{}{ 532 "keyC": map[string]interface{}{}, 533 }, 534 }, 535 }, 536 path: "keyA.keyB.keyC", 537 }, 538 wantType: "string", 539 want: "", 540 }, 541 } 542 for _, tt := range tests { 543 t.Run(tt.name, func(t *testing.T) { 544 var got interface{} 545 if tt.wantType == "string" { 546 got = traverseMapAndGet[string](tt.args.input, tt.args.path) 547 } else if tt.wantType == "map" { 548 got = traverseMapAndGet[map[string]interface{}](tt.args.input, tt.args.path) 549 } else if tt.wantType == "float64" { 550 got = traverseMapAndGet[float64](tt.args.input, tt.args.path) 551 } else { 552 t.Fatalf("wantType type not used in this test") 553 } 554 555 if !reflect.DeepEqual(got, tt.want) { 556 t.Errorf("traverseMapAndGet() got = %v, want %v", got, tt.want) 557 } 558 }) 559 } 560 }