diff --git a/api/v1alpha1/conversion_test.go b/api/v1alpha1/conversion_test.go index e6698a98e..7ba0708fb 100644 --- a/api/v1alpha1/conversion_test.go +++ b/api/v1alpha1/conversion_test.go @@ -85,25 +85,30 @@ func TestFuzzyConversion(t *testing.T) { } func overrideVirtualMachineFieldsFuncs(codecs runtimeserializer.CodecFactory) []interface{} { - // TODO: The changes from v1a1 to v1a2 is quite large so several parts of the input objects are - // defaulted out until we start to marshall the object in the annotations for down conversions - // and back. return []interface{}{ func(vmSpec *v1alpha1.VirtualMachineSpec, c fuzz.Continue) { c.Fuzz(vmSpec) - for i := range vmSpec.Volumes { - // Not present in v1a2. - vmSpec.Volumes[i].VsphereVolume = nil + var volumes []v1alpha1.VirtualMachineVolume + for _, vol := range vmSpec.Volumes { + // vSphere volumes are gone in v1a2 so skip those. + if vol.VsphereVolume == nil { + volumes = append(volumes, vol) + } } + vmSpec.Volumes = volumes - if vmSpec.AdvancedOptions != nil { - if provOpts := vmSpec.AdvancedOptions.DefaultVolumeProvisioningOptions; provOpts != nil { + if opts := vmSpec.AdvancedOptions; opts != nil { + if provOpts := opts.DefaultVolumeProvisioningOptions; provOpts != nil { if provOpts.ThinProvisioned != nil { // Both cannot be set. provOpts.EagerZeroed = nil } } + + if opts.ChangeBlockTracking != nil && !*opts.ChangeBlockTracking { + opts.ChangeBlockTracking = nil + } } // This is effectively deprecated. @@ -226,3 +231,7 @@ func overrideConditionsObservedGeneration(conditions []metav1.Condition) { conditions[i].ObservedGeneration = 0 } } + +func ptrOf[T any](v T) *T { + return &v +} diff --git a/api/v1alpha1/virtualmachine_conversion.go b/api/v1alpha1/virtualmachine_conversion.go index a626dac8f..ff2426507 100644 --- a/api/v1alpha1/virtualmachine_conversion.go +++ b/api/v1alpha1/virtualmachine_conversion.go @@ -225,6 +225,9 @@ func convert_v1alpha2_BootstrapSpec_To_v1alpha1_VmMetadata( out.Transport = VirtualMachineMetadataExtraConfigTransport case "user-data": out.Transport = VirtualMachineMetadataCloudInitTransport + default: + // Best approx we can do. + out.Transport = VirtualMachineMetadataCloudInitTransport } } else if cloudInit.CloudConfig != nil { out.Transport = VirtualMachineMetadataCloudInitTransport @@ -358,34 +361,39 @@ func convert_v1alpha2_ReadinessProbeSpec_To_v1alpha1_Probe(in *v1alpha2.VirtualM } func convert_v1alpha1_VirtualMachineAdvancedOptions_To_v1alpha2_VirtualMachineAdvancedSpec( - in *VirtualMachineAdvancedOptions) *v1alpha2.VirtualMachineAdvancedSpec { + in *VirtualMachineAdvancedOptions, inVolumes []VirtualMachineVolume) *v1alpha2.VirtualMachineAdvancedSpec { + + bootDiskCapacity := convert_v1alpha1_VsphereVolumes_To_v1alpha2_BootDiskCapacity(inVolumes) - if in == nil || apiequality.Semantic.DeepEqual(*in, VirtualMachineAdvancedOptions{}) { + if (in == nil || apiequality.Semantic.DeepEqual(*in, VirtualMachineAdvancedOptions{})) && bootDiskCapacity == nil { return nil } out := v1alpha2.VirtualMachineAdvancedSpec{} - - if opts := in.DefaultVolumeProvisioningOptions; opts != nil { - if opts.ThinProvisioned != nil { - if *opts.ThinProvisioned { - out.DefaultVolumeProvisioningMode = v1alpha2.VirtualMachineVolumeProvisioningModeThin - } else { - out.DefaultVolumeProvisioningMode = v1alpha2.VirtualMachineVolumeProvisioningModeThick + out.BootDiskCapacity = bootDiskCapacity + + if in != nil { + if opts := in.DefaultVolumeProvisioningOptions; opts != nil { + if opts.ThinProvisioned != nil { + if *opts.ThinProvisioned { + out.DefaultVolumeProvisioningMode = v1alpha2.VirtualMachineVolumeProvisioningModeThin + } else { + out.DefaultVolumeProvisioningMode = v1alpha2.VirtualMachineVolumeProvisioningModeThick + } + } else if opts.EagerZeroed != nil && *opts.EagerZeroed { + out.DefaultVolumeProvisioningMode = v1alpha2.VirtualMachineVolumeProvisioningModeThickEagerZero } - } else if opts.EagerZeroed != nil && *opts.EagerZeroed { - out.DefaultVolumeProvisioningMode = v1alpha2.VirtualMachineVolumeProvisioningModeThickEagerZero } - } - if in.ChangeBlockTracking != nil { - out.ChangeBlockTracking = *in.ChangeBlockTracking + if in.ChangeBlockTracking != nil { + out.ChangeBlockTracking = *in.ChangeBlockTracking + } } return &out } -func convert_v1alpha1_VsphereVolumes_To_v1alpah2_BootDiskCapacity(volumes []VirtualMachineVolume) *resource.Quantity { +func convert_v1alpha1_VsphereVolumes_To_v1alpha2_BootDiskCapacity(volumes []VirtualMachineVolume) *resource.Quantity { // The v1a1 VsphereVolume was never a great API as you had to know the DeviceKey upfront; at the time our // API was private - only used by CAPW - and predates the "VM Service" VMs; In v1a2, we only support resizing // the boot disk via an explicit field. As good as we can here, map v1a1 volume into the v1a2 specific field. @@ -441,7 +449,7 @@ func convert_v1alpha2_VirtualMachineAdvancedSpec_To_v1alpha1_VirtualMachineAdvan } func convert_v1alpha2_BootDiskCapacity_To_v1alpha1_VirtualMachineVolume(capacity *resource.Quantity) *VirtualMachineVolume { - if capacity == nil || capacity.IsZero() { + if capacity == nil { return nil } @@ -577,10 +585,7 @@ func Convert_v1alpha1_VirtualMachineSpec_To_v1alpha2_VirtualMachineSpec( } out.ReadinessProbe = convert_v1alpha1_Probe_To_v1alpha2_ReadinessProbeSpec(in.ReadinessProbe) - out.Advanced = convert_v1alpha1_VirtualMachineAdvancedOptions_To_v1alpha2_VirtualMachineAdvancedSpec(in.AdvancedOptions) - if out.Advanced != nil { - out.Advanced.BootDiskCapacity = convert_v1alpha1_VsphereVolumes_To_v1alpah2_BootDiskCapacity(in.Volumes) - } + out.Advanced = convert_v1alpha1_VirtualMachineAdvancedOptions_To_v1alpha2_VirtualMachineAdvancedSpec(in.AdvancedOptions, in.Volumes) if in.ResourcePolicyName != "" { if out.Reserved == nil { @@ -821,16 +826,20 @@ func restore_v1alpha2_VirtualMachineBootstrapSpec( } } -func restore_v1alpha2_VirtualMachineNetwork( +func restore_v1alpha2_VirtualMachineNetworkSpec( dst, src *v1alpha2.VirtualMachine) { - dstNetwork := dst.Spec.Network srcNetwork := src.Spec.Network - - if dstNetwork == nil || srcNetwork == nil { + if srcNetwork == nil || (srcNetwork.HostName == "" && !srcNetwork.Disabled && len(srcNetwork.Interfaces) == 0) { + // Nothing to restore. return } + if dst.Spec.Network == nil { + dst.Spec.Network = &v1alpha2.VirtualMachineNetworkSpec{} + } + dstNetwork := dst.Spec.Network + dstNetwork.HostName = srcNetwork.HostName dstNetwork.Disabled = srcNetwork.Disabled @@ -859,6 +868,17 @@ func restore_v1alpha2_VirtualMachineNetwork( } } +func restore_v1alpha2_VirtualMachineReadinessProbeSpec( + dst, src *v1alpha2.VirtualMachine) { + + if src.Spec.ReadinessProbe != nil { + if dst.Spec.ReadinessProbe == nil { + dst.Spec.ReadinessProbe = &v1alpha2.VirtualMachineReadinessProbeSpec{} + } + dst.Spec.ReadinessProbe.GuestInfo = src.Spec.ReadinessProbe.GuestInfo + } +} + // ConvertTo converts this VirtualMachine to the Hub version. func (src *VirtualMachine) ConvertTo(dstRaw conversion.Hub) error { dst := dstRaw.(*v1alpha2.VirtualMachine) @@ -873,14 +893,10 @@ func (src *VirtualMachine) ConvertTo(dstRaw conversion.Hub) error { } restore_v1alpha2_VirtualMachineBootstrapSpec(dst, restored) - restore_v1alpha2_VirtualMachineNetwork(dst, restored) + restore_v1alpha2_VirtualMachineNetworkSpec(dst, restored) + restore_v1alpha2_VirtualMachineReadinessProbeSpec(dst, restored) - if restored.Spec.ReadinessProbe != nil { - if dst.Spec.ReadinessProbe == nil { - dst.Spec.ReadinessProbe = &v1alpha2.VirtualMachineReadinessProbeSpec{} - } - dst.Spec.ReadinessProbe.GuestInfo = restored.Spec.ReadinessProbe.GuestInfo - } + dst.Status = restored.Status return nil } diff --git a/api/v1alpha1/virtualmachine_conversion_test.go b/api/v1alpha1/virtualmachine_conversion_test.go new file mode 100644 index 000000000..3a9579699 --- /dev/null +++ b/api/v1alpha1/virtualmachine_conversion_test.go @@ -0,0 +1,266 @@ +// Copyright (c) 2023 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1_test + +import ( + "testing" + + . "github.com/onsi/gomega" + + "github.com/google/go-cmp/cmp" + corev1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/conversion" + + "github.com/vmware-tanzu/vm-operator/api/v1alpha1" + "github.com/vmware-tanzu/vm-operator/api/v1alpha2" + "github.com/vmware-tanzu/vm-operator/api/v1alpha2/cloudinit" + "github.com/vmware-tanzu/vm-operator/api/v1alpha2/common" + "github.com/vmware-tanzu/vm-operator/api/v1alpha2/sysprep" +) + +func TestVirtualMachineConversion(t *testing.T) { + g := NewWithT(t) + + hubSpokeHub := func(hub conversion.Hub, spoke conversion.Convertible) { + hubBefore := hub.DeepCopyObject().(conversion.Hub) + + // First convert hub to spoke + dstCopy := spoke.DeepCopyObject().(conversion.Convertible) + g.Expect(dstCopy.ConvertFrom(hubBefore)).To(Succeed()) + + // Convert spoke back to hub and check if the resulting hub is equal to the hub before the round trip + hubAfter := hub.DeepCopyObject().(conversion.Hub) + g.Expect(dstCopy.ConvertTo(hubAfter)).To(Succeed()) + + g.Expect(apiequality.Semantic.DeepEqual(hubBefore, hubAfter)).To(BeTrue(), cmp.Diff(hubBefore, hubAfter)) + } + + t.Run("VirtualMachine hub-spoke-hub", func(t *testing.T) { + hub := v1alpha2.VirtualMachine{ + Spec: v1alpha2.VirtualMachineSpec{ + ImageName: "my-name", + ClassName: "my-class", + StorageClass: "my-storage-class", + Bootstrap: &v1alpha2.VirtualMachineBootstrapSpec{ + CloudInit: &v1alpha2.VirtualMachineBootstrapCloudInitSpec{ + RawCloudConfig: &common.SecretKeySelector{ + Name: "cloud-init-secret", + Key: "user-data", + }, + SSHAuthorizedKeys: []string{"my-ssh-key"}, + }, + }, + Network: &v1alpha2.VirtualMachineNetworkSpec{ + HostName: "my-test-vm", + Disabled: true, + Interfaces: []v1alpha2.VirtualMachineNetworkInterfaceSpec{ + { + Name: "vds-interface", + Network: common.PartialObjectRef{ + TypeMeta: metav1.TypeMeta{ + Kind: "Network", + APIVersion: "netoperator.vmware.com/v1alpha1", + }, + Name: "primary", + }, + }, + { + Name: "ncp-interface", + Network: common.PartialObjectRef{ + TypeMeta: metav1.TypeMeta{ + Kind: "VirtualNetwork", + APIVersion: "vmware.com/v1alpha1", + }, + Name: "segment1", + }, + }, + { + Name: "my-interface", + Network: common.PartialObjectRef{ + TypeMeta: metav1.TypeMeta{ + Kind: "Network", + APIVersion: "netoperator.vmware.com/v1alpha1", + }, + Name: "secondary", + }, + Addresses: []string{"1.1.1.11", "2.2.2.22"}, + DHCP4: true, + DHCP6: true, + Gateway4: "1.1.1.1", + Gateway6: "2.2.2.2", + MTU: ptrOf[int64](9000), + Nameservers: []string{"9.9.9.9"}, + Routes: []v1alpha2.VirtualMachineNetworkRouteSpec{ + { + To: "3.3.3.3", + Via: "1.1.1.111", + Metric: 42, + }, + }, + SearchDomains: []string{"vmware.com"}, + }, + }, + }, + PowerState: v1alpha2.VirtualMachinePowerStateOff, + PowerOffMode: v1alpha2.VirtualMachinePowerOpModeHard, + SuspendMode: v1alpha2.VirtualMachinePowerOpModeTrySoft, + NextRestartTime: "tomorrow", + RestartMode: v1alpha2.VirtualMachinePowerOpModeSoft, + Volumes: []v1alpha2.VirtualMachineVolume{ + { + Name: "my-volume", + VirtualMachineVolumeSource: v1alpha2.VirtualMachineVolumeSource{ + PersistentVolumeClaim: ptrOf(v1alpha2.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "my-claim", + }, + }), + }, + }, + { + Name: "my-volume-2", + VirtualMachineVolumeSource: v1alpha2.VirtualMachineVolumeSource{ + PersistentVolumeClaim: ptrOf(v1alpha2.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "my-claim-2", + }, + InstanceVolumeClaim: &v1alpha2.InstanceVolumeClaimVolumeSource{ + StorageClass: "instance-storage-class", + Size: resource.MustParse("2048k"), + }, + }), + }, + }, + }, + ReadinessProbe: &v1alpha2.VirtualMachineReadinessProbeSpec{ + TCPSocket: &v1alpha2.TCPSocketAction{ + Host: "some-host", + Port: intstr.FromString("https"), + }, + GuestHeartbeat: &v1alpha2.GuestHeartbeatAction{ + ThresholdStatus: v1alpha2.RedHeartbeatStatus, + }, + GuestInfo: []v1alpha2.GuestInfoAction{ + { + Key: "guest-key", + Value: "guest-value", + }, + }, + TimeoutSeconds: 100, + PeriodSeconds: 200, + }, + Advanced: &v1alpha2.VirtualMachineAdvancedSpec{ + BootDiskCapacity: ptrOf(resource.MustParse("1024k")), + DefaultVolumeProvisioningMode: v1alpha2.VirtualMachineVolumeProvisioningModeThickEagerZero, + ChangeBlockTracking: true, + }, + Reserved: &v1alpha2.VirtualMachineReservedSpec{ + ResourcePolicyName: "my-resource-policy", + }, + MinHardwareVersion: 42, + }, + } + + hubSpokeHub(&hub, &v1alpha1.VirtualMachine{}) + }) + + t.Run("VirtualMachine hub-spoke-hub with inlined CloudInit", func(t *testing.T) { + hub := v1alpha2.VirtualMachine{ + Spec: v1alpha2.VirtualMachineSpec{ + Bootstrap: &v1alpha2.VirtualMachineBootstrapSpec{ + CloudInit: &v1alpha2.VirtualMachineBootstrapCloudInitSpec{ + CloudConfig: &cloudinit.CloudConfig{ + Timezone: "my-tz", + User: cloudinit.User{ + Name: "not-root", + }, + }, + SSHAuthorizedKeys: []string{"my-ssh-key"}, + }, + }, + }, + } + + hubSpokeHub(&hub, &v1alpha1.VirtualMachine{}) + }) + + t.Run("VirtualMachine hub-spoke-hub with inlined Sysprep", func(t *testing.T) { + hub := v1alpha2.VirtualMachine{ + Spec: v1alpha2.VirtualMachineSpec{ + Bootstrap: &v1alpha2.VirtualMachineBootstrapSpec{ + Sysprep: &v1alpha2.VirtualMachineBootstrapSysprepSpec{ + Sysprep: &sysprep.Sysprep{ + GUIRunOnce: sysprep.GUIRunOnce{ + Commands: []string{"echo", "hello"}, + }, + GUIUnattended: sysprep.GUIUnattended{ + AutoLogon: true, + }, + Identification: sysprep.Identification{ + DomainAdmin: "my-admin", + }, + UserData: sysprep.UserData{ + FullName: "vmware", + }, + }, + }, + }, + }, + } + + hubSpokeHub(&hub, &v1alpha1.VirtualMachine{}) + }) + + t.Run("VirtualMachine hub-spoke-hub with LinuxPrep and vAppConfig", func(t *testing.T) { + hub := v1alpha2.VirtualMachine{ + Spec: v1alpha2.VirtualMachineSpec{ + Bootstrap: &v1alpha2.VirtualMachineBootstrapSpec{ + LinuxPrep: &v1alpha2.VirtualMachineBootstrapLinuxPrepSpec{ + HardwareClockIsUTC: true, + TimeZone: "my-tz", + }, + VAppConfig: &v1alpha2.VirtualMachineBootstrapVAppConfigSpec{ + Properties: []common.KeyValueOrSecretKeySelectorPair{ + { + Key: "my-key", + Value: common.ValueOrSecretKeySelector{ + Value: ptrOf("my-value"), + }, + }, + }, + RawProperties: "my-secret", + }, + }, + }, + } + + hubSpokeHub(&hub, &v1alpha1.VirtualMachine{}) + }) + + t.Run("VirtualMachine hub-spoke-hub with vAppConfig", func(t *testing.T) { + hub := v1alpha2.VirtualMachine{ + Spec: v1alpha2.VirtualMachineSpec{ + Bootstrap: &v1alpha2.VirtualMachineBootstrapSpec{ + VAppConfig: &v1alpha2.VirtualMachineBootstrapVAppConfigSpec{ + Properties: []common.KeyValueOrSecretKeySelectorPair{ + { + Key: "my-key", + Value: common.ValueOrSecretKeySelector{ + Value: ptrOf("my-value"), + }, + }, + }, + RawProperties: "my-secret", + }, + }, + }, + } + + hubSpokeHub(&hub, &v1alpha1.VirtualMachine{}) + }) +}