diff --git a/pkg/cloudprovider/vsphere/nodemanager.go b/pkg/cloudprovider/vsphere/nodemanager.go index 8e7c226f7..3f9c09497 100644 --- a/pkg/cloudprovider/vsphere/nodemanager.go +++ b/pkg/cloudprovider/vsphere/nodemanager.go @@ -41,6 +41,11 @@ import ( "github.com/vmware/govmomi/vim25/types" ) +const ( + // instanceType field key in the annotation YAML field of VirtualMachineConfigInfo(vim.vm.ConfigInfo) Data Object + instanceTypeFieldKey = "instanceType" +) + // Errors var ( // ErrVCenterNotFound is returned when the configured vCenter cannot be @@ -358,17 +363,7 @@ func (nm *NodeManager) DiscoverNode(nodeID string, searchBy cm.FindVM) error { nodeID, vmDI.VM, vmDI.VcServer, vmDI.DataCenter.Name()) klog.V(2).Info("Hostname: ", oVM.Guest.HostName, " UUID: ", vmDI.UUID) - os := "unknown" - if g, ok := GuestOSLookup[oVM.Summary.Config.GuestId]; ok { - os = g - } - - // store instance type in nodeinfo map - instanceType := fmt.Sprintf("vsphere-vm.cpu-%d.mem-%dgb.os-%s", - oVM.Summary.Config.NumCpu, - (oVM.Summary.Config.MemorySizeMB / 1024), - os, - ) + instanceType := getInstanceType(oVM.Config.Annotation, oVM.Summary.Config) nodeInfo := &NodeInfo{ tenantRef: tenantRef, dataCenter: vmDI.DataCenter, vm: vmDI.VM, vcServer: vmDI.VcServer, @@ -379,6 +374,35 @@ func (nm *NodeManager) DiscoverNode(nodeID string, searchBy cm.FindVM) error { return nil } +func getInstanceType(annotation string, config types.VirtualMachineConfigSummary) string { + annotation = strings.TrimSpace(annotation) + if annotation == "" { + return getInstanceTypeFallback(config) + } + + var yamlData map[string]any + if err := yaml.Unmarshal([]byte(annotation), &yamlData); err == nil { + if instanceTypeFieldValue, ok := yamlData[instanceTypeFieldKey].(string); ok && instanceTypeFieldValue != "" { + return instanceTypeFieldValue + } + } + return getInstanceTypeFallback(config) +} + +func getInstanceTypeFallback(config types.VirtualMachineConfigSummary) string { + os := "unknown" + if g, ok := GuestOSLookup[config.GuestId]; ok { + os = g + } + + // store instance type in nodeinfo map + return fmt.Sprintf("vsphere-vm.cpu-%d.mem-%dgb.os-%s", + config.NumCpu, + (config.MemorySizeMB / 1024), + os, + ) +} + // discoverIPs returns a pair of *ipAddrNetworkNames. The first representing // the internal network IP and the second being the external network IP. // diff --git a/pkg/cloudprovider/vsphere/nodemanager_test.go b/pkg/cloudprovider/vsphere/nodemanager_test.go index 002b54729..b63142366 100644 --- a/pkg/cloudprovider/vsphere/nodemanager_test.go +++ b/pkg/cloudprovider/vsphere/nodemanager_test.go @@ -2254,3 +2254,205 @@ network.encoding: %s network: %s`, encoding, encodedNetconfig) } + +func TestGetInstanceType(t *testing.T) { + tests := []struct { + name string + annotation string + config vimtypes.VirtualMachineConfigSummary + expected string + }{ + { + name: "camel-case YAML field", + annotation: "instanceType: c-2x", + config: vimtypes.VirtualMachineConfigSummary{ + NumCpu: 2, + MemorySizeMB: 4096, + }, + expected: "c-2x", + }, + { + name: "camel-case multiline YAML", + annotation: `owner: platform-team +instanceType: c-4x +environment: production`, + config: vimtypes.VirtualMachineConfigSummary{ + NumCpu: 4, + MemorySizeMB: 8192, + }, + expected: "c-4x", + }, + { + name: "camel-case JSON is valid YAML", + annotation: `{"instanceType":"m-8x"}`, + config: vimtypes.VirtualMachineConfigSummary{ + NumCpu: 8, + MemorySizeMB: 16384, + }, + expected: "m-8x", + }, + { + name: "camel-case human-readable key-value format", + annotation: `owner: platform-team +instanceType: r-16x`, + config: vimtypes.VirtualMachineConfigSummary{ + NumCpu: 16, + MemorySizeMB: 32768, + }, + expected: "r-16x", + }, + { + name: "snake-case YAML field is ignored", + annotation: "instance_type: c-2x", + config: vimtypes.VirtualMachineConfigSummary{ + NumCpu: 2, + MemorySizeMB: 4096, + }, + expected: "vsphere-vm.cpu-2.mem-4gb.os-unknown", + }, + { + name: "snake-case JSON field is ignored", + annotation: `{"instance_type":"c-4x"}`, + config: vimtypes.VirtualMachineConfigSummary{ + NumCpu: 4, + MemorySizeMB: 8192, + }, + expected: "vsphere-vm.cpu-4.mem-8gb.os-unknown", + }, + { + name: "snake-case multiline field is ignored", + annotation: `owner: platform-team +instance_type: c-8x`, + config: vimtypes.VirtualMachineConfigSummary{ + NumCpu: 8, + MemorySizeMB: 16384, + }, + expected: "vsphere-vm.cpu-8.mem-16gb.os-unknown", + }, + { + name: "missing instanceType uses fallback", + annotation: "owner: platform-team", + config: vimtypes.VirtualMachineConfigSummary{ + NumCpu: 4, + MemorySizeMB: 8192, + }, + expected: "vsphere-vm.cpu-4.mem-8gb.os-unknown", + }, + { + name: "empty instanceType uses fallback", + annotation: `instanceType: ""`, + config: vimtypes.VirtualMachineConfigSummary{ + NumCpu: 8, + MemorySizeMB: 32768, + }, + expected: "vsphere-vm.cpu-8.mem-32gb.os-unknown", + }, + { + name: "non-string instanceType uses fallback", + annotation: "instanceType: 123", + config: vimtypes.VirtualMachineConfigSummary{ + NumCpu: 2, + MemorySizeMB: 2048, + }, + expected: "vsphere-vm.cpu-2.mem-2gb.os-unknown", + }, + { + name: "boolean instanceType uses fallback", + annotation: "instanceType: true", + config: vimtypes.VirtualMachineConfigSummary{ + NumCpu: 2, + MemorySizeMB: 4096, + }, + expected: "vsphere-vm.cpu-2.mem-4gb.os-unknown", + }, + { + name: "malformed YAML uses fallback", + annotation: "instanceType: [invalid", + config: vimtypes.VirtualMachineConfigSummary{ + NumCpu: 4, + MemorySizeMB: 16384, + }, + expected: "vsphere-vm.cpu-4.mem-16gb.os-unknown", + }, + { + name: "unknown guest OS uses unknown", + annotation: "owner: platform-team", + config: vimtypes.VirtualMachineConfigSummary{ + GuestId: "unsupportedGuest", + NumCpu: 4, + MemorySizeMB: 8192, + }, + expected: "vsphere-vm.cpu-4.mem-8gb.os-unknown", + }, + { + name: "memory is converted from MB to whole GB", + annotation: "owner: platform-team", + config: vimtypes.VirtualMachineConfigSummary{ + NumCpu: 2, + MemorySizeMB: 2560, + }, + expected: "vsphere-vm.cpu-2.mem-2gb.os-unknown", + }, + + // This documents the current implementation. It returns before + // constructing the default instance type. + { + name: "empty annotation returns empty string", + annotation: "", + config: vimtypes.VirtualMachineConfigSummary{ + NumCpu: 2, + MemorySizeMB: 4096, + }, + expected: "vsphere-vm.cpu-2.mem-4gb.os-unknown", + }, + { + name: "whitespace-only annotation returns empty string", + annotation: " \n\t ", + config: vimtypes.VirtualMachineConfigSummary{ + NumCpu: 2, + MemorySizeMB: 4096, + }, + expected: "vsphere-vm.cpu-2.mem-4gb.os-unknown", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := getInstanceType(test.annotation, test.config) + if actual != test.expected { + t.Errorf( + "getInstanceType(%q, %+v) returned %q, expected %q", + test.annotation, + test.config, + actual, + test.expected, + ) + } + }) + } +} + +func TestGetInstanceTypeUsesGuestOSLookup(t *testing.T) { + const guestID = "sles12_64Guest" + + expectedOS, ok := GuestOSLookup[guestID] + if !ok { + t.Fatalf("GuestOSLookup does not contain %q", guestID) + } + + config := vimtypes.VirtualMachineConfigSummary{ + GuestId: guestID, + NumCpu: 4, + MemorySizeMB: 8192, + } + + actual := getInstanceType("owner: platform-team", config) + expected := fmt.Sprintf( + "vsphere-vm.cpu-4.mem-8gb.os-%s", + expectedOS, + ) + + if actual != expected { + t.Errorf("getInstanceType() returned %q, expected %q", actual, expected) + } +}