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  }