Repository: vmware-tanzu/vm-operator
Pull request #949: ✨ FastDeploy: Add DiskPromotion API and reconciler
Author: login: dougm
Labels: name: size/XL
description: Denotes a PR that changes 500-999 lines.
Created: 2025-05-01T16:27:01Z | Merged: 2025-05-06T14:52:06Z

Depends on https://github.com/vmware/govmomi/pull/3778


Diff:
diff --git a/api/v1alpha1/virtualmachine_conversion.go b/api/v1alpha1/virtualmachine_conversion.go
index 0dbe7e138..bfd8d44e3 100644
--- a/api/v1alpha1/virtualmachine_conversion.go
+++ b/api/v1alpha1/virtualmachine_conversion.go
@@ -998,6 +998,10 @@ func restore_v1alpha4_VirtualMachineCdrom(dst, src *vmopv1.VirtualMachine) {
 	dst.Spec.Cdrom = src.Spec.Cdrom
 }
 
+func restore_v1alpha4_VirtualMachinePromoteDisksMode(dst, src *vmopv1.VirtualMachine) {
+	dst.Spec.PromoteDisksMode = src.Spec.PromoteDisksMode
+}
+
 func convert_v1alpha1_PreReqsReadyCondition_to_v1alpha4_Conditions(
 	dst *vmopv1.VirtualMachine) []metav1.Condition {
 
@@ -1250,6 +1254,7 @@ func (src *VirtualMachine) ConvertTo(dstRaw ctrlconversion.Hub) error {
 	restore_v1alpha4_VirtualMachineGuestID(dst, restored)
 	restore_v1alpha4_VirtualMachineCdrom(dst, restored)
 	restore_v1alpha4_VirtualMachineCryptoSpec(dst, restored)
+	restore_v1alpha4_VirtualMachinePromoteDisksMode(dst, restored)
 
 	// END RESTORE
 
diff --git a/api/v1alpha1/virtualmachine_conversion_test.go b/api/v1alpha1/virtualmachine_conversion_test.go
index 7d3f19d60..8a686c960 100644
--- a/api/v1alpha1/virtualmachine_conversion_test.go
+++ b/api/v1alpha1/virtualmachine_conversion_test.go
@@ -246,6 +246,7 @@ func TestVirtualMachineConversion(t *testing.T) {
 						Connected:         ptrOf(true),
 					},
 				},
+				PromoteDisksMode: vmopv1.VirtualMachinePromoteDisksModeOffline,
 			},
 		}
 
diff --git a/api/v1alpha1/zz_generated.conversion.go b/api/v1alpha1/zz_generated.conversion.go
index ed797feca..0ba7db26a 100644
--- a/api/v1alpha1/zz_generated.conversion.go
+++ b/api/v1alpha1/zz_generated.conversion.go
@@ -2097,6 +2097,7 @@ func autoConvert_v1alpha4_VirtualMachineSpec_To_v1alpha1_VirtualMachineSpec(in *
 	// WARNING: in.InstanceUUID requires manual conversion: does not exist in peer-type
 	// WARNING: in.BiosUUID requires manual conversion: does not exist in peer-type
 	// WARNING: in.GuestID requires manual conversion: does not exist in peer-type
+	// WARNING: in.PromoteDisksMode requires manual conversion: does not exist in peer-type
 	return nil
 }
 
@@ -2174,6 +2175,7 @@ func autoConvert_v1alpha4_VirtualMachineStatus_To_v1alpha1_VirtualMachineStatus(
 	out.LastRestartTime = (*v1.Time)(unsafe.Pointer(in.LastRestartTime))
 	out.HardwareVersion = in.HardwareVersion
 	// WARNING: in.Storage requires manual conversion: does not exist in peer-type
+	// WARNING: in.TaskID requires manual conversion: does not exist in peer-type
 	return nil
 }
 
diff --git a/api/v1alpha2/virtualmachine_conversion.go b/api/v1alpha2/virtualmachine_conversion.go
index 1b4fb5e28..6fadde1f7 100644
--- a/api/v1alpha2/virtualmachine_conversion.go
+++ b/api/v1alpha2/virtualmachine_conversion.go
@@ -291,6 +291,10 @@ func restore_v1alpha4_VirtualMachineCdrom(dst, src *vmopv1.VirtualMachine) {
 	dst.Spec.Cdrom = src.Spec.Cdrom
 }
 
+func restore_v1alpha4_VirtualMachinePromoteDisksMode(dst, src *vmopv1.VirtualMachine) {
+	dst.Spec.PromoteDisksMode = src.Spec.PromoteDisksMode
+}
+
 // ConvertTo converts this VirtualMachine to the Hub version.
 func (src *VirtualMachine) ConvertTo(dstRaw ctrlconversion.Hub) error {
 	dst := dstRaw.(*vmopv1.VirtualMachine)
@@ -315,6 +319,7 @@ func (src *VirtualMachine) ConvertTo(dstRaw ctrlconversion.Hub) error {
 	restore_v1alpha4_VirtualMachineGuestID(dst, restored)
 	restore_v1alpha4_VirtualMachineCdrom(dst, restored)
 	restore_v1alpha4_VirtualMachineCryptoSpec(dst, restored)
+	restore_v1alpha4_VirtualMachinePromoteDisksMode(dst, restored)
 
 	// END RESTORE
 
diff --git a/api/v1alpha2/virtualmachine_conversion_test.go b/api/v1alpha2/virtualmachine_conversion_test.go
index acd5176b4..762db97fc 100644
--- a/api/v1alpha2/virtualmachine_conversion_test.go
+++ b/api/v1alpha2/virtualmachine_conversion_test.go
@@ -256,6 +256,7 @@ func TestVirtualMachineConversion(t *testing.T) {
 						Connected:         ptrOf(true),
 					},
 				},
+				PromoteDisksMode: vmopv1.VirtualMachinePromoteDisksModeOffline,
 			},
 		}
 
diff --git a/api/v1alpha2/zz_generated.conversion.go b/api/v1alpha2/zz_generated.conversion.go
index 3cf7e0b6b..e355d3f44 100644
--- a/api/v1alpha2/zz_generated.conversion.go
+++ b/api/v1alpha2/zz_generated.conversion.go
@@ -3219,6 +3219,7 @@ func autoConvert_v1alpha4_VirtualMachineSpec_To_v1alpha2_VirtualMachineSpec(in *
 	// WARNING: in.InstanceUUID requires manual conversion: does not exist in peer-type
 	// WARNING: in.BiosUUID requires manual conversion: does not exist in peer-type
 	// WARNING: in.GuestID requires manual conversion: does not exist in peer-type
+	// WARNING: in.PromoteDisksMode requires manual conversion: does not exist in peer-type
 	return nil
 }
 
@@ -3292,6 +3293,7 @@ func autoConvert_v1alpha4_VirtualMachineStatus_To_v1alpha2_VirtualMachineStatus(
 	out.LastRestartTime = (*v1.Time)(unsafe.Pointer(in.LastRestartTime))
 	out.HardwareVersion = in.HardwareVersion
 	// WARNING: in.Storage requires manual conversion: does not exist in peer-type
+	// WARNING: in.TaskID requires manual conversion: does not exist in peer-type
 	return nil
 }
 
diff --git a/api/v1alpha3/virtualmachine_conversion.go b/api/v1alpha3/virtualmachine_conversion.go
index cbca8f753..4865bca79 100644
--- a/api/v1alpha3/virtualmachine_conversion.go
+++ b/api/v1alpha3/virtualmachine_conversion.go
@@ -18,6 +18,30 @@ func Convert_v1alpha4_VirtualMachineBootstrapCloudInitSpec_To_v1alpha3_VirtualMa
 	return autoConvert_v1alpha4_VirtualMachineBootstrapCloudInitSpec_To_v1alpha3_VirtualMachineBootstrapCloudInitSpec(in, out, s)
 }
 
+func Convert_v1alpha4_VirtualMachineSpec_To_v1alpha3_VirtualMachineSpec(
+	in *vmopv1.VirtualMachineSpec, out *VirtualMachineSpec, s apiconversion.Scope) error {
+
+	if err := autoConvert_v1alpha4_VirtualMachineSpec_To_v1alpha3_VirtualMachineSpec(in, out, s); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func Convert_v1alpha4_VirtualMachineStatus_To_v1alpha3_VirtualMachineStatus(
+	in *vmopv1.VirtualMachineStatus, out *VirtualMachineStatus, s apiconversion.Scope) error {
+
+	if err := autoConvert_v1alpha4_VirtualMachineStatus_To_v1alpha3_VirtualMachineStatus(in, out, s); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func restore_v1alpha4_VirtualMachinePromoteDisksMode(dst, src *vmopv1.VirtualMachine) {
+	dst.Spec.PromoteDisksMode = src.Spec.PromoteDisksMode
+}
+
 // ConvertTo converts this VirtualMachine to the Hub version.
 func (src *VirtualMachine) ConvertTo(dstRaw ctrlconversion.Hub) error {
 	dst := dstRaw.(*vmopv1.VirtualMachine)
@@ -34,6 +58,7 @@ func (src *VirtualMachine) ConvertTo(dstRaw ctrlconversion.Hub) error {
 	// BEGIN RESTORE
 
 	restore_v1alpha4_VirtualMachineBootstrapCloudInitWaitOnNetwork(dst, restored)
+	restore_v1alpha4_VirtualMachinePromoteDisksMode(dst, restored)
 
 	// END RESTORE
 
diff --git a/api/v1alpha3/virtualmachine_conversion_test.go b/api/v1alpha3/virtualmachine_conversion_test.go
index b1aafc4f5..f7f3ecdcf 100644
--- a/api/v1alpha3/virtualmachine_conversion_test.go
+++ b/api/v1alpha3/virtualmachine_conversion_test.go
@@ -253,6 +253,7 @@ func TestVirtualMachineConversion(t *testing.T) {
 						Connected:         ptrOf(true),
 					},
 				},
+				PromoteDisksMode: vmopv1.VirtualMachinePromoteDisksModeOffline,
 			},
 		}
 
diff --git a/api/v1alpha3/zz_generated.conversion.go b/api/v1alpha3/zz_generated.conversion.go
index cedd4a81f..118821a11 100644
--- a/api/v1alpha3/zz_generated.conversion.go
+++ b/api/v1alpha3/zz_generated.conversion.go
@@ -1005,21 +1005,11 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
-	if err := s.AddGeneratedConversionFunc((*v1alpha4.VirtualMachineSpec)(nil), (*VirtualMachineSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_v1alpha4_VirtualMachineSpec_To_v1alpha3_VirtualMachineSpec(a.(*v1alpha4.VirtualMachineSpec), b.(*VirtualMachineSpec), scope)
-	}); err != nil {
-		return err
-	}
 	if err := s.AddGeneratedConversionFunc((*VirtualMachineStatus)(nil), (*v1alpha4.VirtualMachineStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1alpha3_VirtualMachineStatus_To_v1alpha4_VirtualMachineStatus(a.(*VirtualMachineStatus), b.(*v1alpha4.VirtualMachineStatus), scope)
 	}); err != nil {
 		return err
 	}
-	if err := s.AddGeneratedConversionFunc((*v1alpha4.VirtualMachineStatus)(nil), (*VirtualMachineStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_v1alpha4_VirtualMachineStatus_To_v1alpha3_VirtualMachineStatus(a.(*v1alpha4.VirtualMachineStatus), b.(*VirtualMachineStatus), scope)
-	}); err != nil {
-		return err
-	}
 	if err := s.AddGeneratedConversionFunc((*VirtualMachineStorageStatus)(nil), (*v1alpha4.VirtualMachineStorageStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1alpha3_VirtualMachineStorageStatus_To_v1alpha4_VirtualMachineStorageStatus(a.(*VirtualMachineStorageStatus), b.(*v1alpha4.VirtualMachineStorageStatus), scope)
 	}); err != nil {
@@ -1145,6 +1135,16 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddConversionFunc((*v1alpha4.VirtualMachineSpec)(nil), (*VirtualMachineSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1alpha4_VirtualMachineSpec_To_v1alpha3_VirtualMachineSpec(a.(*v1alpha4.VirtualMachineSpec), b.(*VirtualMachineSpec), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddConversionFunc((*v1alpha4.VirtualMachineStatus)(nil), (*VirtualMachineStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1alpha4_VirtualMachineStatus_To_v1alpha3_VirtualMachineStatus(a.(*v1alpha4.VirtualMachineStatus), b.(*VirtualMachineStatus), scope)
+	}); err != nil {
+		return err
+	}
 	return nil
 }
 
@@ -3755,14 +3755,10 @@ func autoConvert_v1alpha4_VirtualMachineSpec_To_v1alpha3_VirtualMachineSpec(in *
 	out.InstanceUUID = in.InstanceUUID
 	out.BiosUUID = in.BiosUUID
 	out.GuestID = in.GuestID
+	// WARNING: in.PromoteDisksMode requires manual conversion: does not exist in peer-type
 	return nil
 }
 
-// Convert_v1alpha4_VirtualMachineSpec_To_v1alpha3_VirtualMachineSpec is an autogenerated conversion function.
-func Convert_v1alpha4_VirtualMachineSpec_To_v1alpha3_VirtualMachineSpec(in *v1alpha4.VirtualMachineSpec, out *VirtualMachineSpec, s conversion.Scope) error {
-	return autoConvert_v1alpha4_VirtualMachineSpec_To_v1alpha3_VirtualMachineSpec(in, out, s)
-}
-
 func autoConvert_v1alpha3_VirtualMachineStatus_To_v1alpha4_VirtualMachineStatus(in *VirtualMachineStatus, out *v1alpha4.VirtualMachineStatus, s conversion.Scope) error {
 	out.Class = (*common.LocalObjectRef)(unsafe.Pointer(in.Class))
 	out.Host = in.Host
@@ -3803,14 +3799,10 @@ func autoConvert_v1alpha4_VirtualMachineStatus_To_v1alpha3_VirtualMachineStatus(
 	out.LastRestartTime = (*v1.Time)(unsafe.Pointer(in.LastRestartTime))
 	out.HardwareVersion = in.HardwareVersion
 	out.Storage = (*VirtualMachineStorageStatus)(unsafe.Pointer(in.Storage))
+	// WARNING: in.TaskID requires manual conversion: does not exist in peer-type
 	return nil
 }
 
-// Convert_v1alpha4_VirtualMachineStatus_To_v1alpha3_VirtualMachineStatus is an autogenerated conversion function.
-func Convert_v1alpha4_VirtualMachineStatus_To_v1alpha3_VirtualMachineStatus(in *v1alpha4.VirtualMachineStatus, out *VirtualMachineStatus, s conversion.Scope) error {
-	return autoConvert_v1alpha4_VirtualMachineStatus_To_v1alpha3_VirtualMachineStatus(in, out, s)
-}
-
 func autoConvert_v1alpha3_VirtualMachineStorageStatus_To_v1alpha4_VirtualMachineStorageStatus(in *VirtualMachineStorageStatus, out *v1alpha4.VirtualMachineStorageStatus, s conversion.Scope) error {
 	out.Usage = (*v1alpha4.VirtualMachineStorageStatusUsage)(unsafe.Pointer(in.Usage))
 	return nil
diff --git a/api/v1alpha4/virtualmachine_types.go b/api/v1alpha4/virtualmachine_types.go
index ee67ca5d9..8bc410607 100644
--- a/api/v1alpha4/virtualmachine_types.go
+++ b/api/v1alpha4/virtualmachine_types.go
@@ -46,6 +46,10 @@ const (
 	// encryption state is synced to the desired encryption state.
 	VirtualMachineEncryptionSynced = "VirtualMachineEncryptionSynced"
 
+	// VirtualMachineDiskPromotionSynced indicates that the VirtualMachine's
+	// disk promotion state is synced to the desired promotion state.
+	VirtualMachineDiskPromotionSynced = "VirtualMachineDiskPromotionSynced"
+
 	// VirtualMachineConditionCreated indicates that the VM has been created.
 	VirtualMachineConditionCreated = "VirtualMachineCreated"
 
@@ -453,6 +457,29 @@ type VirtualMachineCryptoSpec struct {
 	UseDefaultKeyProvider *bool `json:"useDefaultKeyProvider,omitempty"`
 }
 
+// +kubebuilder:validation:Enum=Direct;Linked
+
+// VirtualMachineDeployMode represents the available modes in which a VM may be
+// deployed.
+type VirtualMachineDeployMode string
+
+const (
+	VirtualMachineDeployModeDirect VirtualMachineDeployMode = "Direct"
+	VirtualMachineDeployModeLinked VirtualMachineDeployMode = "Linked"
+)
+
+// +kubebuilder:validation:Enum=Online;Offline
+
+// VirtualMachinePromoteDisksMode represents the available modes for promoting
+// child disks to full clones.
+type VirtualMachinePromoteDisksMode string
+
+const (
+	VirtualMachinePromoteDisksModeDisabled VirtualMachinePromoteDisksMode = "Disabled"
+	VirtualMachinePromoteDisksModeOnline   VirtualMachinePromoteDisksMode = "Online"
+	VirtualMachinePromoteDisksModeOffline  VirtualMachinePromoteDisksMode = "Offline"
+)
+
 // VirtualMachineSpec defines the desired state of a VirtualMachine.
 type VirtualMachineSpec struct {
 	// +optional
@@ -752,6 +779,20 @@ type VirtualMachineSpec struct {
 	//
 	// This field is required when the VM has any CD-ROM devices attached.
 	GuestID string `json:"guestID,omitempty"`
+
+	// +optional
+	// +kubebuilder:default=Online
+
+	// PromoteDisksMode describes the mode used to promote a VM's delta disks to
+	// full disks. The available modes are:
+	//
+	// - Disabled -- Do not promote disks.
+	// - Online   -- Promote disks while the VM is powered on. VMs with
+	//               snapshots do not support online promotion.
+	// - Offline  -- Promote disks while the VM is powered off.
+	//
+	// Defaults to Online.
+	PromoteDisksMode VirtualMachinePromoteDisksMode `json:"promoteDisksMode,omitempty"`
 }
 
 // VirtualMachineReservedSpec describes a set of VM configuration options
@@ -930,6 +971,12 @@ type VirtualMachineStatus struct {
 
 	// Storage describes the observed state of the VirtualMachine's storage.
 	Storage *VirtualMachineStorageStatus `json:"storage,omitempty"`
+
+	// +optional
+
+	// TaskID describes the observed ID of the task created by VM Operator to
+	// perform some long-running operation on the VM.
+	TaskID string `json:"taskID,omitempty"`
 }
 
 // +kubebuilder:object:root=true
diff --git a/config/crd/bases/vmoperator.vmware.com_virtualmachinereplicasets.yaml b/config/crd/bases/vmoperator.vmware.com_virtualmachinereplicasets.yaml
index 3d924db8a..b393cd551 100644
--- a/config/crd/bases/vmoperator.vmware.com_virtualmachinereplicasets.yaml
+++ b/config/crd/bases/vmoperator.vmware.com_virtualmachinereplicasets.yaml
@@ -3532,6 +3532,22 @@ spec:
                         - PoweredOn
                         - Suspended
                         type: string
+                      promoteDisksMode:
+                        default: Online
+                        description: |-
+                          PromoteDisksMode describes the mode used to promote a VM's delta disks to
+                          full disks. The available modes are:
+
+                          - Disabled -- Do not promote disks.
+                          - Online   -- Promote disks while the VM is powered on. VMs with
+                                        snapshots do not support online promotion.
+                          - Offline  -- Promote disks while the VM is powered off.
+
+                          Defaults to Online.
+                        enum:
+                        - Online
+                        - Offline
+                        type: string
                       readinessProbe:
                         description: ReadinessProbe describes a probe used to determine
                           the VM's ready state.
diff --git a/config/crd/bases/vmoperator.vmware.com_virtualmachines.yaml b/config/crd/bases/vmoperator.vmware.com_virtualmachines.yaml
index e25d8fcae..c8cd98207 100644
--- a/config/crd/bases/vmoperator.vmware.com_virtualmachines.yaml
+++ b/config/crd/bases/vmoperator.vmware.com_virtualmachines.yaml
@@ -7061,6 +7061,22 @@ spec:
                 - PoweredOn
                 - Suspended
                 type: string
+              promoteDisksMode:
+                default: Online
+                description: |-
+                  PromoteDisksMode describes the mode used to promote a VM's delta disks to
+                  full disks. The available modes are:
+
+                  - Disabled -- Do not promote disks.
+                  - Online   -- Promote disks while the VM is powered on. VMs with
+                                snapshots do not support online promotion.
+                  - Offline  -- Promote disks while the VM is powered off.
+
+                  Defaults to Online.
+                enum:
+                - Online
+                - Offline
+                type: string
               readinessProbe:
                 description: ReadinessProbe describes a probe used to determine the
                   VM's ready state.
@@ -8106,6 +8122,11 @@ spec:
                         x-kubernetes-int-or-string: true
                     type: object
                 type: object
+              taskID:
+                description: |-
+                  TaskID describes the observed ID of the task created by VM Operator to
+                  perform some long-running operation on the VM.
+                type: string
               uniqueID:
                 description: |-
                   UniqueID describes a unique identifier that is provided by the underlying
diff --git a/controllers/virtualmachine/virtualmachine/virtualmachine_controller.go b/controllers/virtualmachine/virtualmachine/virtualmachine_controller.go
index b178e2540..ff703a6ae 100644
--- a/controllers/virtualmachine/virtualmachine/virtualmachine_controller.go
+++ b/controllers/virtualmachine/virtualmachine/virtualmachine_controller.go
@@ -45,6 +45,7 @@ import (
 	vmopv1util "github.com/vmware-tanzu/vm-operator/pkg/util/vmopv1"
 	"github.com/vmware-tanzu/vm-operator/pkg/vmconfig"
 	"github.com/vmware-tanzu/vm-operator/pkg/vmconfig/crypto"
+	"github.com/vmware-tanzu/vm-operator/pkg/vmconfig/diskpromo"
 )
 
 const (
@@ -287,12 +288,16 @@ type Reconciler struct {
 func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) {
 	ctx = pkgcfg.JoinContext(ctx, r.Context)
 	ctx = cource.JoinContext(ctx, r.Context)
+	ctx = vmconfig.WithContext(ctx)
 
 	if pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey {
-		ctx = vmconfig.WithContext(ctx)
 		ctx = vmconfig.Register(ctx, crypto.New())
 	}
 
+	if pkgcfg.FromContext(ctx).Features.FastDeploy {
+		ctx = vmconfig.Register(ctx, diskpromo.New())
+	}
+
 	ctx = ctxop.WithContext(ctx)
 	ctx = ovfcache.JoinContext(ctx, r.Context)
 	ctx = record.WithContext(ctx, r.Recorder)
diff --git a/go.mod b/go.mod
index e9a4472bd..86ff61921 100644
--- a/go.mod
+++ b/go.mod
@@ -34,10 +34,10 @@ require (
 	github.com/vmware-tanzu/vm-operator/external/tanzu-topology v0.0.0-00010101000000-000000000000
 	github.com/vmware-tanzu/vm-operator/pkg/backup/api v0.0.0-00010101000000-000000000000
 	github.com/vmware-tanzu/vm-operator/pkg/constants/testlabels v0.0.0-00010101000000-000000000000
-	github.com/vmware/govmomi v0.49.0-alpha.0.0.20250304190849-a640504a20f9
+	github.com/vmware/govmomi v0.51.0-alpha.0.0.20250503025045-95320a7de48d
 	golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc
 	// * https://github.com/vmware-tanzu/vm-operator/security/dependabot/24
-	golang.org/x/text v0.22.0
+	golang.org/x/text v0.24.0
 	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d
 	k8s.io/api v0.31.0
 	k8s.io/apiextensions-apiserver v0.31.0
diff --git a/go.sum b/go.sum
index 1e0f96fc7..924358b66 100644
--- a/go.sum
+++ b/go.sum
@@ -116,8 +116,8 @@ github.com/vmware-tanzu/net-operator-api v0.0.0-20240523152550-862e2c4eb0e0 h1:y
 github.com/vmware-tanzu/net-operator-api v0.0.0-20240523152550-862e2c4eb0e0/go.mod h1:w6QJGm3crIA16ZIz1FVQXD2NVeJhOgGXxW05RbVTSTo=
 github.com/vmware-tanzu/nsx-operator/pkg/apis v0.0.0-20241112044858-9da8637c1b0d h1:z9lrzKVtNlujduv9BilzPxuge/LE2F0N1ms3TP4JZvw=
 github.com/vmware-tanzu/nsx-operator/pkg/apis v0.0.0-20241112044858-9da8637c1b0d/go.mod h1:Q4JzNkNMvjo7pXtlB5/R3oME4Nhah7fAObWgghVmtxk=
-github.com/vmware/govmomi v0.49.0-alpha.0.0.20250304190849-a640504a20f9 h1:sJEx0HYXDcOMS+RUNHMRJkwfrMcp+IN6ReV+zuthk6w=
-github.com/vmware/govmomi v0.49.0-alpha.0.0.20250304190849-a640504a20f9/go.mod h1:bkmhpq8SlVxJo8BAmMLEB0TsX26+RqhuU/qDZgseYgU=
+github.com/vmware/govmomi v0.51.0-alpha.0.0.20250503025045-95320a7de48d h1:HbYf2xu61QZ7pXh6FPst6egp+5NeHkGN9GNRjd/HkcI=
+github.com/vmware/govmomi v0.51.0-alpha.0.0.20250503025045-95320a7de48d/go.mod h1:Z5uo7z0kRhVV00E4gfbUGwUaXIKTgqngsT+t/mIDpcI=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -155,8 +155,8 @@ golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
 golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
 golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
 golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/pkg/providers/vsphere/session/session_vm_update.go b/pkg/providers/vsphere/session/session_vm_update.go
index 0c30eca65..103906813 100644
--- a/pkg/providers/vsphere/session/session_vm_update.go
+++ b/pkg/providers/vsphere/session/session_vm_update.go
@@ -1079,10 +1079,14 @@ func (s *Session) UpdateVirtualMachine(
 	var (
 		refetchProps bool
 		updateErr    error
+		isPaused     = isVMPaused(vmCtx)
+		hasTask      = vmCtx.VM.Status.TaskID != ""
 	)
 
-	// Only update VM's power state when VM is not paused.
-	if !isVMPaused(vmCtx) {
+	// Only update VM's power state when VM:
+	// - is not paused
+	// - does not have an outstanding task
+	if !isPaused && !hasTask {
 		// Translate the VM's current power state into the VM Op power state value.
 		var existingPowerState vmopv1.VirtualMachinePowerState
 		switch vmCtx.MoVM.Summary.Runtime.PowerState {
@@ -1116,7 +1120,8 @@ func (s *Session) UpdateVirtualMachine(
 				existingPowerState)
 		}
 	} else {
-		vmCtx.Logger.Info("VirtualMachine is paused. PowerState is not updated.")
+		vmCtx.Logger.Info("PowerState is not updated.",
+			"isPaused", isPaused, "hasTask", hasTask)
 		refetchProps, updateErr = defaultReconfigure(vmCtx, s.K8sClient, vcVM)
 	}
 
diff --git a/pkg/vmconfig/diskpromo/diskpromo_reconciler.go b/pkg/vmconfig/diskpromo/diskpromo_reconciler.go
new file mode 100644
index 000000000..41e618327
--- /dev/null
+++ b/pkg/vmconfig/diskpromo/diskpromo_reconciler.go
@@ -0,0 +1,229 @@
+// © Broadcom. All Rights Reserved.
+// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
+// SPDX-License-Identifier: Apache-2.0
+
+package diskpromo
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/go-logr/logr"
+	"github.com/vmware/govmomi/fault"
+	"github.com/vmware/govmomi/object"
+	"github.com/vmware/govmomi/property"
+	"github.com/vmware/govmomi/vim25"
+	"github.com/vmware/govmomi/vim25/mo"
+	vimtypes "github.com/vmware/govmomi/vim25/types"
+	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha4"
+	pkgcond "github.com/vmware-tanzu/vm-operator/pkg/conditions"
+	pkgerr "github.com/vmware-tanzu/vm-operator/pkg/errors"
+	"github.com/vmware-tanzu/vm-operator/pkg/vmconfig"
+)
+
+type reconciler struct{}
+
+var _ vmconfig.Reconciler = reconciler{}
+
+const (
+	ReasonTaskError = "DiskPromotionTaskError"
+	ReasonPending   = "DiskPromotionPending"
+
+	promoteDisksTaskKey = "VirtualMachine.promoteDisks"
+)
+
+// New returns a new Reconciler for a VM's disk promotion state.
+func New() vmconfig.Reconciler {
+	return reconciler{}
+}
+
+// Name returns the unique name used to identify the reconciler.
+func (r reconciler) Name() string {
+	return "diskpromo"
+}
+
+func (r reconciler) OnResult(
+	_ context.Context,
+	_ *vmopv1.VirtualMachine,
+	_ mo.VirtualMachine,
+	_ error) error {
+
+	return nil
+}
+
+// Reconcile performs an online promotion of any of a VM's linked-clone disks to
+// full clones. This reconciler is a no-op for VMs that do not yet exists and/or
+// are not powered on.
+func (r reconciler) Reconcile(
+	ctx context.Context,
+	_ ctrlclient.Client,
+	vimClient *vim25.Client,
+	vm *vmopv1.VirtualMachine,
+	moVM mo.VirtualMachine,
+	_ *vimtypes.VirtualMachineConfigSpec) error {
+
+	if ctx == nil {
+		panic("context is nil")
+	}
+	if vimClient == nil {
+		panic("vimClient is nil")
+	}
+	if vm == nil {
+		panic("vm is nil")
+	}
+
+	if moVM.Config == nil {
+		// Skip VMs that are not yet created.
+		return nil
+	}
+
+	logger := logr.FromContextOrDiscard(ctx)
+
+	if vm.Status.TaskID != "" {
+		ref := vimtypes.ManagedObjectReference{
+			Type:  "Task",
+			Value: vm.Status.TaskID,
+		}
+
+		var task mo.Task
+		pc := property.DefaultCollector(vimClient)
+
+		if err := pc.RetrieveOne(ctx, ref, []string{"info"}, &task); err != nil {
+			if fault.Is(err, &vimtypes.ManagedObjectNotFound{}) {
+				// Tasks go away after 10m of completion
+				vm.Status.TaskID = ""
+			}
+			return err
+		}
+
+		logger.Info("Pending task",
+			"ID", vm.Status.TaskID,
+			"description", task.Info.DescriptionId,
+			"state", task.Info.State)
+
+		// From the API doc:
+		//   An identifier for this operation. This includes publicly visible internal tasks and
+		//   is a lookup in the TaskDescription methodInfo data object.
+		// See also:
+		//   govc collect -s -json TaskManager:TaskManager description.methodInfo | \
+		//     jq '.[] | select(.key == "VirtualMachine.promoteDisks") | .'
+
+		if task.Info.DescriptionId != promoteDisksTaskKey {
+			return nil
+		}
+
+		switch task.Info.State {
+		case vimtypes.TaskInfoStateSuccess:
+			vm.Status.TaskID = ""
+
+			pkgcond.MarkTrue(
+				vm,
+				vmopv1.VirtualMachineDiskPromotionSynced)
+
+			return nil
+		case vimtypes.TaskInfoStateError:
+			vm.Status.TaskID = ""
+
+			pkgcond.MarkFalse(
+				vm,
+				vmopv1.VirtualMachineDiskPromotionSynced,
+				ReasonTaskError,
+				"%s",
+				task.Info.Error.LocalizedMessage)
+
+			return nil
+		default:
+			// Skip VMs with outstanding tasks.
+			return nil
+		}
+	}
+
+	logger.Info("Promote disks", "mode", vm.Spec.PromoteDisksMode)
+
+	if vm.Spec.PromoteDisksMode == vmopv1.VirtualMachinePromoteDisksModeDisabled {
+		// Skip VMs that do not request promotion.
+		pkgcond.Delete(vm, vmopv1.VirtualMachineDiskPromotionSynced)
+		return nil
+	}
+
+	// Find the VirtualDisks for this VM.
+	var (
+		childDisks []vimtypes.VirtualDisk
+		devices    = object.VirtualDeviceList(moVM.Config.Hardware.Device)
+		allDisks   = devices.SelectByType(&vimtypes.VirtualDisk{})
+	)
+
+	// Find any classic, file-based disks that have parent backings.
+	for i := range allDisks {
+		d := allDisks[i].(*vimtypes.VirtualDisk)
+		if d.VDiskId == nil { // Skip FCDs
+			switch tBack := d.Backing.(type) {
+			case *vimtypes.VirtualDiskFlatVer2BackingInfo:
+				if tBack.Parent != nil {
+					childDisks = append(childDisks, *d)
+				}
+			case *vimtypes.VirtualDiskSeSparseBackingInfo:
+				if tBack.Parent != nil {
+					childDisks = append(childDisks, *d)
+				}
+			case *vimtypes.VirtualDiskSparseVer2BackingInfo:
+				if tBack.Parent != nil {
+					childDisks = append(childDisks, *d)
+				}
+			}
+		}
+	}
+
+	logger.Info("Promote disks", "total", len(allDisks), "child", len(childDisks))
+
+	if len(childDisks) == 0 {
+		// Skip VMs that do not have any child disks to promote.
+		return nil
+	}
+
+	switch vm.Spec.PromoteDisksMode {
+	case vmopv1.VirtualMachinePromoteDisksModeOnline:
+		if moVM.Snapshot != nil && moVM.Snapshot.CurrentSnapshot != nil {
+			// Skip VMs that have snapshots.
+			pkgcond.MarkFalse(
+				vm,
+				vmopv1.VirtualMachineDiskPromotionSynced,
+				ReasonPending,
+				"Cannot online promote disks when VM has snapshot")
+			return nil
+		}
+		if moVM.Runtime.PowerState != vimtypes.VirtualMachinePowerStatePoweredOn {
+			pkgcond.MarkFalse(
+				vm,
+				vmopv1.VirtualMachineDiskPromotionSynced,
+				ReasonPending,
+				"Pending VM powered on")
+			return nil
+		}
+	case vmopv1.VirtualMachinePromoteDisksModeOffline:
+		if moVM.Runtime.PowerState != vimtypes.VirtualMachinePowerStatePoweredOff {
+			// Skip VMs that are not powered off.
+			pkgcond.MarkFalse(
+				vm,
+				vmopv1.VirtualMachineDiskPromotionSynced,
+				ReasonPending,
+				"Pending VM powered off")
+			return nil
+		}
+	}
+
+	obj := object.NewVirtualMachine(vimClient, moVM.Self)
+	task, err := obj.PromoteDisks(ctx, true, childDisks)
+	if err != nil {
+		return fmt.Errorf("failed to call promote disks task: %w", err)
+	}
+
+	// Track the task ID.
+	vm.Status.TaskID = task.Reference().Value
+
+	return pkgerr.NoRequeueError{
+		Message: "doing online disk promotion",
+	}
+}
diff --git a/pkg/vmconfig/diskpromo/diskpromo_reconciler_suite_test.go b/pkg/vmconfig/diskpromo/diskpromo_reconciler_suite_test.go
new file mode 100644
index 000000000..f2cdfcede
--- /dev/null
+++ b/pkg/vmconfig/diskpromo/diskpromo_reconciler_suite_test.go
@@ -0,0 +1,25 @@
+// © Broadcom. All Rights Reserved.
+// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
+// SPDX-License-Identifier: Apache-2.0
+
+package diskpromo_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	"k8s.io/klog/v2"
+	logf "sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+func init() {
+	klog.SetOutput(GinkgoWriter)
+	logf.SetLogger(klog.Background())
+}
+
+func TestSuite(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "DiskPromo Reconciler Test Suite")
+}
diff --git a/pkg/vmconfig/diskpromo/diskpromo_reconciler_test.go b/pkg/vmconfig/diskpromo/diskpromo_reconciler_test.go
new file mode 100644
index 000000000..650f725d7
--- /dev/null
+++ b/pkg/vmconfig/diskpromo/diskpromo_reconciler_test.go
@@ -0,0 +1,390 @@
+// © Broadcom. All Rights Reserved.
+// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
+// SPDX-License-Identifier: Apache-2.0
+
+package diskpromo_test
+
+import (
+	"context"
+
+	"github.com/go-logr/logr"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"github.com/vmware/govmomi/object"
+	"github.com/vmware/govmomi/vim25"
+	"github.com/vmware/govmomi/vim25/mo"
+	vimtypes "github.com/vmware/govmomi/vim25/types"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/klog/v2/textlogger"
+	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
+
+	vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha4"
+	"github.com/vmware-tanzu/vm-operator/pkg/conditions"
+	pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config"
+	"github.com/vmware-tanzu/vm-operator/pkg/constants/testlabels"
+	ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation"
+	pkgerr "github.com/vmware-tanzu/vm-operator/pkg/errors"
+	"github.com/vmware-tanzu/vm-operator/pkg/vmconfig"
+	"github.com/vmware-tanzu/vm-operator/pkg/vmconfig/diskpromo"
+	"github.com/vmware-tanzu/vm-operator/test/builder"
+)
+
+var _ = Describe("New", Label(testlabels.V1Alpha4), func() {
+	It("should return a reconciler", func() {
+		Expect(diskpromo.New()).ToNot(BeNil())
+	})
+})
+
+var _ = Describe("Name", Label(testlabels.V1Alpha4), func() {
+	It("should return diskpromo", func() {
+		Expect(diskpromo.New().Name()).To(Equal("diskpromo"))
+	})
+})
+
+var _ = Describe("Reconcile", Label(testlabels.V1Alpha4), func() {
+	var (
+		r          vmconfig.Reconciler
+		ctx        context.Context
+		k8sClient  ctrlclient.Client
+		vimClient  *vim25.Client
+		moVM       mo.VirtualMachine
+		vcVM       *object.VirtualMachine
+		vm         *vmopv1.VirtualMachine
+		withObjs   []ctrlclient.Object
+		withFuncs  interceptor.Funcs
+		configSpec *vimtypes.VirtualMachineConfigSpec
+		noRequeue  = pkgerr.NoRequeueError{Message: "doing online disk promotion"}
+
+		// Used for testing that only tasks with promoteDisksTaskKey are reconciled
+		createDiskTask string
+	)
+
+	BeforeEach(func() {
+		r = diskpromo.New()
+
+		vcsimCtx := builder.NewTestContextForVCSim(
+			ctxop.WithContext(pkgcfg.NewContextWithDefaultConfig()), builder.VCSimTestConfig{})
+		ctx = vcsimCtx
+		ctx = vmconfig.WithContext(ctx)
+		ctx = logr.NewContext(
+			ctx,
+			textlogger.NewLogger(textlogger.NewConfig(
+				textlogger.Verbosity(5),
+				textlogger.Output(GinkgoWriter),
+			)))
+		vimClient = vcsimCtx.VCClient.Client
+
+		var err error
+		vcVM, err = vcsimCtx.Finder.VirtualMachine(ctx, "DC0_C0_RP0_VM0")
+		Expect(err).NotTo(HaveOccurred())
+
+		dskName := "[LocalDS_0] base-" + vcVM.Name() + ".vmdk"
+		dskMgr := object.NewVirtualDiskManager(vimClient)
+		task, err := dskMgr.CreateVirtualDisk(ctx, dskName, vcsimCtx.Datacenter, &vimtypes.FileBackedVirtualDiskSpec{
+			CapacityKb: 10 * 1024,
+		})
+		Expect(err).NotTo(HaveOccurred())
+		Expect(task.Wait(ctx)).NotTo(HaveOccurred())
+		createDiskTask = task.Reference().Value
+
+		device, err := vcVM.Device(ctx)
+		Expect(err).NotTo(HaveOccurred())
+
+		controller, err := device.FindDiskController("")
+		Expect(err).NotTo(HaveOccurred())
+
+		disk := device.CreateDisk(controller, vimtypes.ManagedObjectReference{}, dskName)
+		disk = device.ChildDisk(disk)
+		err = vcVM.AddDevice(ctx, disk)
+		Expect(err).NotTo(HaveOccurred())
+		// Refresh with child disk added so VirtualDisk.Key is valid
+		device, err = vcVM.Device(ctx)
+		Expect(err).NotTo(HaveOccurred())
+
+		moVM = mo.VirtualMachine{
+			Config: &vimtypes.VirtualMachineConfigInfo{
+				Hardware: vimtypes.VirtualHardware{
+					Device: device,
+				},
+			},
+			Runtime: vimtypes.VirtualMachineRuntimeInfo{
+				PowerState: vimtypes.VirtualMachinePowerStatePoweredOn,
+			},
+			Summary: vimtypes.VirtualMachineSummary{
+				Runtime: vimtypes.VirtualMachineRuntimeInfo{
+					PowerState: vimtypes.VirtualMachinePowerStatePoweredOn,
+				},
+			},
+		}
+
+		moVM.Self = vcVM.Reference()
+
+		configSpec = &vimtypes.VirtualMachineConfigSpec{}
+
+		vm = &vmopv1.VirtualMachine{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "my-namespace",
+				Name:      "my-vm",
+			},
+			Spec: vmopv1.VirtualMachineSpec{},
+			Status: vmopv1.VirtualMachineStatus{
+				UniqueID: moVM.Self.Value,
+			},
+		}
+
+		withFuncs = interceptor.Funcs{}
+		withObjs = []ctrlclient.Object{vm}
+	})
+	JustBeforeEach(func() {
+		k8sClient = builder.NewFakeClientWithInterceptors(withFuncs, withObjs...)
+	})
+
+	When("it should panic", func() {
+		When("ctx is nil", func() {
+			BeforeEach(func() {
+				ctx = nil
+			})
+			It("should panic", func() {
+				fn := func() {
+					_ = r.Reconcile(ctx, k8sClient, vimClient, vm, moVM, configSpec)
+				}
+				Expect(fn).To(PanicWith("context is nil"))
+			})
+		})
+
+		When("vimClient is nil", func() {
+			JustBeforeEach(func() {
+				vimClient = nil
+			})
+			It("should panic", func() {
+				fn := func() {
+					_ = r.Reconcile(ctx, k8sClient, vimClient, vm, moVM, configSpec)
+				}
+				Expect(fn).To(PanicWith("vimClient is nil"))
+			})
+		})
+
+		When("vm is nil", func() {
+			BeforeEach(func() {
+				vm = nil
+			})
+			It("should panic", func() {
+				fn := func() {
+					_ = r.Reconcile(ctx, k8sClient, vimClient, vm, moVM, configSpec)
+				}
+				Expect(fn).To(PanicWith("vm is nil"))
+			})
+		})
+
+	})
+
+	When("it should not panic", func() {
+		var (
+			err error
+		)
+
+		JustBeforeEach(func() {
+			err = r.Reconcile(ctx, k8sClient, vimClient, vm, moVM, configSpec)
+		})
+
+		When("creating a new vm", func() {
+
+			When("spec.powerState is PoweredOn", func() {
+				When("promoteDisksMode is Disabled", func() {
+					BeforeEach(func() {
+						vm.Spec.PromoteDisksMode = vmopv1.VirtualMachinePromoteDisksModeDisabled
+					})
+					It("should not promote disks", func() {
+						Expect(err).ToNot(HaveOccurred())
+						c := conditions.Get(vm, vmopv1.VirtualMachineDiskPromotionSynced)
+						Expect(c).To(BeNil())
+
+						childDisks := 0
+
+						device, err := vcVM.Device(ctx)
+						Expect(err).NotTo(HaveOccurred())
+
+						for _, d := range device.SelectByType(&vimtypes.VirtualDisk{}) {
+							disk := d.(*vimtypes.VirtualDisk)
+							b, ok := disk.Backing.(*vimtypes.VirtualDiskFlatVer2BackingInfo)
+							Expect(ok).To(BeTrue())
+							if b.Parent != nil {
+								childDisks++
+							}
+						}
+
+						Expect(childDisks).To(Equal(1))
+					})
+				})
+
+				When("promoteDisksMode is Offline", func() {
+					BeforeEach(func() {
+						vm.Spec.PromoteDisksMode = vmopv1.VirtualMachinePromoteDisksModeOffline
+					})
+					It("should not promote disks", func() {
+						Expect(err).ToNot(HaveOccurred())
+						c := conditions.Get(vm, vmopv1.VirtualMachineDiskPromotionSynced)
+						Expect(c).ToNot(BeNil())
+						Expect(c.Status).To(Equal(metav1.ConditionFalse))
+						Expect(c.Reason).To(Equal(diskpromo.ReasonPending))
+					})
+				})
+
+				When("promoteDiskMode is Online", func() {
+					BeforeEach(func() {
+						vm.Spec.PromoteDisksMode = vmopv1.VirtualMachinePromoteDisksModeOnline
+					})
+
+					It("should promote disks", func() {
+						Expect(err).To(HaveOccurred())
+						Expect(err).To(Equal(noRequeue))
+						Expect(vm.Status.TaskID).ToNot(BeEmpty())
+
+						// simulator promoteDisks will delay the task based on disk capacity,
+						// so we need > 1 Reconcile here.
+						Eventually(func(g Gomega) {
+							err = r.Reconcile(ctx, k8sClient, vimClient, vm, moVM, configSpec)
+							g.Expect(err).ToNot(HaveOccurred())
+							g.Expect(vm.Status.TaskID).To(BeEmpty())
+							c := conditions.Get(vm, vmopv1.VirtualMachineDiskPromotionSynced)
+							g.Expect(c).ToNot(BeNil())
+							g.Expect(c.Status).To(Equal(metav1.ConditionTrue))
+						}).Should(Succeed(), "waiting for promoteDisks task to complete")
+
+						// VM should not have any child disks after disk promotion
+						device, err := vcVM.Device(ctx)
+						Expect(err).NotTo(HaveOccurred())
+						for _, d := range device.SelectByType(&vimtypes.VirtualDisk{}) {
+							disk := d.(*vimtypes.VirtualDisk)
+							b, ok := disk.Backing.(*vimtypes.VirtualDiskFlatVer2BackingInfo)
+							Expect(ok).To(BeTrue())
+							Expect(b.Parent).To(BeNil())
+						}
+
+						// Expect no task when no child disks
+						moVM.Config.Hardware.Device = device
+						err = r.Reconcile(ctx, k8sClient, vimClient, vm, moVM, configSpec)
+						Expect(err).ToNot(HaveOccurred())
+						Expect(vm.Status.TaskID).To(BeEmpty())
+					})
+				})
+
+				When("vm is not created", func() {
+					BeforeEach(func() {
+						moVM.Config = nil
+					})
+					It("should not fail", func() {
+						Expect(err).ToNot(HaveOccurred())
+						c := conditions.Get(vm, vmopv1.VirtualMachineDiskPromotionSynced)
+						Expect(c).To(BeNil())
+					})
+				})
+
+				When("status.task is not promoteDisks", func() {
+					BeforeEach(func() {
+						vm.Status.TaskID = createDiskTask
+					})
+					It("should not fail", func() {
+						Expect(err).ToNot(HaveOccurred())
+						c := conditions.Get(vm, vmopv1.VirtualMachineDiskPromotionSynced)
+						Expect(c).To(BeNil())
+					})
+				})
+
+				When("status.task is not found", func() {
+					BeforeEach(func() {
+						vm.Status.TaskID = "task-invalid"
+					})
+					It("should fail and clear taskID", func() {
+						Expect(err).To(HaveOccurred())
+						c := conditions.Get(vm, vmopv1.VirtualMachineDiskPromotionSynced)
+						Expect(c).To(BeNil())
+						Expect(vm.Status.TaskID).To(BeEmpty())
+					})
+				})
+
+				When("vm has snapshots", func() {
+					BeforeEach(func() {
+						vm.Spec.PromoteDisksMode = vmopv1.VirtualMachinePromoteDisksModeOnline
+						moVM.Snapshot = getSnapshotInfoWithLinearChain()
+					})
+					It("should not sync", func() {
+						Expect(err).ToNot(HaveOccurred())
+						c := conditions.Get(vm, vmopv1.VirtualMachineDiskPromotionSynced)
+						Expect(c).ToNot(BeNil())
+						Expect(c.Status).To(Equal(metav1.ConditionFalse))
+						Expect(c.Reason).To(Equal(diskpromo.ReasonPending))
+					})
+				})
+
+				When("invalid VirtualDisk has invalid Key", func() {
+					BeforeEach(func() {
+						vm.Spec.PromoteDisksMode = vmopv1.VirtualMachinePromoteDisksModeOnline
+
+						device := object.VirtualDeviceList(moVM.Config.Hardware.Device)
+						for _, d := range device.SelectByType(&vimtypes.VirtualDisk{}) {
+							disk := d.(*vimtypes.VirtualDisk)
+							if b, ok := disk.Backing.(*vimtypes.VirtualDiskFlatVer2BackingInfo); ok {
+								if b.Parent != nil {
+									d.GetVirtualDevice().Key = -d.GetVirtualDevice().Key
+								}
+							}
+						}
+					})
+
+					It("should fail the task", func() {
+						Expect(err).To(HaveOccurred())
+						Expect(err).To(Equal(noRequeue))
+						Expect(vm.Status.TaskID).ToNot(BeEmpty())
+
+						err = r.Reconcile(ctx, k8sClient, vimClient, vm, moVM, configSpec)
+						Expect(err).ToNot(HaveOccurred())
+						Expect(vm.Status.TaskID).To(BeEmpty())
+						c := conditions.Get(vm, vmopv1.VirtualMachineDiskPromotionSynced)
+						Expect(c).ToNot(BeNil())
+						Expect(c.Status).To(Equal(metav1.ConditionFalse))
+						Expect(c.Reason).To(Equal(diskpromo.ReasonTaskError))
+					})
+				})
+			})
+		})
+
+		When("spec.powerState is PoweredOn", func() {
+			BeforeEach(func() {
+				moVM.Runtime.PowerState = vimtypes.VirtualMachinePowerStatePoweredOff
+			})
+			When("promoteDiskMode is Online", func() {
+				BeforeEach(func() {
+					vm.Spec.PromoteDisksMode = vmopv1.VirtualMachinePromoteDisksModeOnline
+				})
+
+				It("should not sync", func() {
+					Expect(err).ToNot(HaveOccurred())
+					Expect(vm.Status.TaskID).To(BeEmpty())
+
+					c := conditions.Get(vm, vmopv1.VirtualMachineDiskPromotionSynced)
+					Expect(c).ToNot(BeNil())
+					Expect(c.Status).To(Equal(metav1.ConditionFalse))
+					Expect(c.Reason).To(Equal(diskpromo.ReasonPending))
+				})
+			})
+		})
+	})
+})
+
+func getSnapshotInfoWithLinearChain() *vimtypes.VirtualMachineSnapshotInfo {
+	return &vimtypes.VirtualMachineSnapshotInfo{
+		CurrentSnapshot: &vimtypes.ManagedObjectReference{},
+		RootSnapshotList: []vimtypes.VirtualMachineSnapshotTree{
+			{
+				Name: "1",
+				ChildSnapshotList: []vimtypes.VirtualMachineSnapshotTree{
+					{
+						Name: "1a",
+					},
+				},
+			},
+		},
+	}
+}


Reviews:
user: login: akutz
body: Thanks @dougm . I will look more in-depth later, this was just a first-pass of the API, thanks!
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-01T17:30:02Z
user: login: bryanv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T18:41:45Z
user: login: dougm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T19:33:47Z
user: login: dougm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T20:43:35Z
user: login: bryanv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T21:58:59Z
user: login: bryanv
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T21:59:03Z
user: login: dougm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T22:12:57Z

Review comments:
diff_hunk: @@ -705,6 +732,32 @@ type VirtualMachineSpec struct {
 	//
 	// This field is required when the VM has any CD-ROM devices attached.
 	GuestID string `json:"guestID,omitempty"`
+
+	// +optional
+	// +kubebuilder:default=Linked
+
+	// DeployMode describes the mode in which the VM is deployed. The available
+	// modes are:
+	//
+	// - Direct              -- a full clone of the VM image
+	// - Linked              -- a linked clone of the VM image
+	//
+	// Defaults to Linked.
+	// DeployMode VirtualMachineDeployMode `json:"deployMode,omitempty"`
path: api/v1alpha4/virtualmachine_types.go
user: login: akutz
body: This can be removed.
created_at: 2025-05-01T17:27:56Z
author_association: COLLABORATOR
diff_hunk: @@ -883,6 +936,12 @@ type VirtualMachineStatus struct {
 
 	// Storage describes the observed state of the VirtualMachine's storage.
 	Storage *VirtualMachineStorageStatus `json:"storage,omitempty"`
+
+	// +optional
+
+	// TaskID describes the ID of a task created by VM Operator to modify this
path: api/v1alpha4/virtualmachine_types.go
user: login: akutz
body: Suggest:

```
// TaskID describes the observed ID of the task created by VM Operator to
// perform some long-running operation on the VM.
```

I do not want to say "modify" because this ID might also surface the ID of a snapshot task for instance.
created_at: 2025-05-01T17:29:11Z
author_association: COLLABORATOR
diff_hunk: @@ -5,21 +5,70 @@
 package v1alpha3
 
 import (
+	apiconversion "k8s.io/apimachinery/pkg/conversion"
 	ctrlconversion "sigs.k8s.io/controller-runtime/pkg/conversion"
 
+	"github.com/vmware-tanzu/vm-operator/api/utilconversion"
 	vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha4"
 )
 
+func Convert_v1alpha4_VirtualMachineSpec_To_v1alpha3_VirtualMachineSpec(
+	in *vmopv1.VirtualMachineSpec, out *VirtualMachineSpec, s apiconversion.Scope) error {
+
+	if err := autoConvert_v1alpha4_VirtualMachineSpec_To_v1alpha3_VirtualMachineSpec(in, out, s); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func Convert_v1alpha4_VirtualMachineStatus_To_v1alpha3_VirtualMachineStatus(
+	in *vmopv1.VirtualMachineStatus, out *VirtualMachineStatus, s apiconversion.Scope) error {
+
+	if err := autoConvert_v1alpha4_VirtualMachineStatus_To_v1alpha3_VirtualMachineStatus(in, out, s); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func restore_v1alpha4_VirtualMachinePromoteDisksMode(dst, src *vmopv1.VirtualMachine) {
+	dst.Spec.PromoteDisksMode = src.Spec.PromoteDisksMode
+}
+
 // ConvertTo converts this VirtualMachine to the Hub version.
 func (src *VirtualMachine) ConvertTo(dstRaw ctrlconversion.Hub) error {
 	dst := dstRaw.(*vmopv1.VirtualMachine)
-	return Convert_v1alpha3_VirtualMachine_To_v1alpha4_VirtualMachine(src, dst, nil)
+	if err := Convert_v1alpha3_VirtualMachine_To_v1alpha4_VirtualMachine(src, dst, nil); err != nil {
+		return err
+	}
+
+	// Manually restore data.
+	restored := &vmopv1.VirtualMachine{}
+	if ok, err := utilconversion.UnmarshalData(src, restored); err != nil || !ok {
+		return err
+	}
+
+	// BEGIN RESTORE
+
+	restore_v1alpha4_VirtualMachinePromoteDisksMode(dst, restored)
path: api/v1alpha3/virtualmachine_conversion.go
user: login: bryanv
body: Don't we need to do this restore v1a1 and v1a2 as well? 

And v1a3 should probably have a `VirtualMachine hub-spoke-hub` test that v1a{1,2} have that asserts that `PromoteDisksMode` is restored 
created_at: 2025-05-05T18:41:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,21 +5,70 @@
 package v1alpha3
 
 import (
+	apiconversion "k8s.io/apimachinery/pkg/conversion"
 	ctrlconversion "sigs.k8s.io/controller-runtime/pkg/conversion"
 
+	"github.com/vmware-tanzu/vm-operator/api/utilconversion"
 	vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha4"
 )
 
+func Convert_v1alpha4_VirtualMachineSpec_To_v1alpha3_VirtualMachineSpec(
+	in *vmopv1.VirtualMachineSpec, out *VirtualMachineSpec, s apiconversion.Scope) error {
+
+	if err := autoConvert_v1alpha4_VirtualMachineSpec_To_v1alpha3_VirtualMachineSpec(in, out, s); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func Convert_v1alpha4_VirtualMachineStatus_To_v1alpha3_VirtualMachineStatus(
+	in *vmopv1.VirtualMachineStatus, out *VirtualMachineStatus, s apiconversion.Scope) error {
+
+	if err := autoConvert_v1alpha4_VirtualMachineStatus_To_v1alpha3_VirtualMachineStatus(in, out, s); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func restore_v1alpha4_VirtualMachinePromoteDisksMode(dst, src *vmopv1.VirtualMachine) {
+	dst.Spec.PromoteDisksMode = src.Spec.PromoteDisksMode
+}
+
 // ConvertTo converts this VirtualMachine to the Hub version.
 func (src *VirtualMachine) ConvertTo(dstRaw ctrlconversion.Hub) error {
 	dst := dstRaw.(*vmopv1.VirtualMachine)
-	return Convert_v1alpha3_VirtualMachine_To_v1alpha4_VirtualMachine(src, dst, nil)
+	if err := Convert_v1alpha3_VirtualMachine_To_v1alpha4_VirtualMachine(src, dst, nil); err != nil {
+		return err
+	}
+
+	// Manually restore data.
+	restored := &vmopv1.VirtualMachine{}
+	if ok, err := utilconversion.UnmarshalData(src, restored); err != nil || !ok {
+		return err
+	}
+
+	// BEGIN RESTORE
+
+	restore_v1alpha4_VirtualMachinePromoteDisksMode(dst, restored)
path: api/v1alpha3/virtualmachine_conversion.go
user: login: dougm
body: Ah yes, adding those. Adding v1alpha3/virtualmachine_conversion_test.go, which is copy-n-paste of   v1alpha2/virtualmachine_conversion_test.go's `VirtualMachine hub-spoke-hub` test, with `PromoteDisksMode` added to the spec. Not sure if I should prune some of the copied `hub` fields or leave as-is?
created_at: 2025-05-05T19:33:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,21 +5,70 @@
 package v1alpha3
 
 import (
+	apiconversion "k8s.io/apimachinery/pkg/conversion"
 	ctrlconversion "sigs.k8s.io/controller-runtime/pkg/conversion"
 
+	"github.com/vmware-tanzu/vm-operator/api/utilconversion"
 	vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha4"
 )
 
+func Convert_v1alpha4_VirtualMachineSpec_To_v1alpha3_VirtualMachineSpec(
+	in *vmopv1.VirtualMachineSpec, out *VirtualMachineSpec, s apiconversion.Scope) error {
+
+	if err := autoConvert_v1alpha4_VirtualMachineSpec_To_v1alpha3_VirtualMachineSpec(in, out, s); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func Convert_v1alpha4_VirtualMachineStatus_To_v1alpha3_VirtualMachineStatus(
+	in *vmopv1.VirtualMachineStatus, out *VirtualMachineStatus, s apiconversion.Scope) error {
+
+	if err := autoConvert_v1alpha4_VirtualMachineStatus_To_v1alpha3_VirtualMachineStatus(in, out, s); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func restore_v1alpha4_VirtualMachinePromoteDisksMode(dst, src *vmopv1.VirtualMachine) {
+	dst.Spec.PromoteDisksMode = src.Spec.PromoteDisksMode
+}
+
 // ConvertTo converts this VirtualMachine to the Hub version.
 func (src *VirtualMachine) ConvertTo(dstRaw ctrlconversion.Hub) error {
 	dst := dstRaw.(*vmopv1.VirtualMachine)
-	return Convert_v1alpha3_VirtualMachine_To_v1alpha4_VirtualMachine(src, dst, nil)
+	if err := Convert_v1alpha3_VirtualMachine_To_v1alpha4_VirtualMachine(src, dst, nil); err != nil {
+		return err
+	}
+
+	// Manually restore data.
+	restored := &vmopv1.VirtualMachine{}
+	if ok, err := utilconversion.UnmarshalData(src, restored); err != nil || !ok {
+		return err
+	}
+
+	// BEGIN RESTORE
+
+	restore_v1alpha4_VirtualMachinePromoteDisksMode(dst, restored)
path: api/v1alpha3/virtualmachine_conversion.go
user: login: dougm
body: Rebasing with Sai's change merged.. I will prune that to just PromoteDisksMode
created_at: 2025-05-05T20:43:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -288,8 +289,10 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Re
 	ctx = pkgcfg.JoinContext(ctx, r.Context)
 	ctx = cource.JoinContext(ctx, r.Context)
 
-	if pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey {
+	if pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey ||
+		pkgcfg.FromContext(ctx).Features.FastDeploy {
 		ctx = vmconfig.WithContext(ctx)
+		ctx = vmconfig.Register(ctx, diskpromo.New())
 		ctx = vmconfig.Register(ctx, crypto.New())
path: controllers/virtualmachine/virtualmachine/virtualmachine_controller.go
user: login: bryanv
body: Do  we want to guard the Register() calls based on their corresponding feature flag?
created_at: 2025-05-05T21:58:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -288,8 +289,10 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Re
 	ctx = pkgcfg.JoinContext(ctx, r.Context)
 	ctx = cource.JoinContext(ctx, r.Context)
 
-	if pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey {
+	if pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey ||
+		pkgcfg.FromContext(ctx).Features.FastDeploy {
 		ctx = vmconfig.WithContext(ctx)
+		ctx = vmconfig.Register(ctx, diskpromo.New())
 		ctx = vmconfig.Register(ctx, crypto.New())
path: controllers/virtualmachine/virtualmachine/virtualmachine_controller.go
user: login: dougm
body: Yes, done
created_at: 2025-05-05T22:12:57Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-05T22:51:39Z
body: ![Code Coverage](https://img.shields.io/badge/Code%20Coverage-82%25-yellow?style=flat)

Package | Line Rate | Health
-------- | --------- | ------
github.com/vmware-tanzu/vm-operator/controllers/contentlibrary/clustercontentlibraryitem | 100% | ✔
github.com/vmware-tanzu/vm-operator/controllers/contentlibrary/contentlibraryitem | 100% | ✔
github.com/vmware-tanzu/vm-operator/controllers/contentlibrary/utils | 89% | ➖
github.com/vmware-tanzu/vm-operator/controllers/infra/capability/configmap | 92% | ✔
github.com/vmware-tanzu/vm-operator/controllers/infra/capability/crd | 100% | ✔
github.com/vmware-tanzu/vm-operator/controllers/infra/configmap | 74% | ❌
github.com/vmware-tanzu/vm-operator/controllers/infra/node | 77% | ❌
github.com/vmware-tanzu/vm-operator/controllers/infra/secret | 76% | ❌
github.com/vmware-tanzu/vm-operator/controllers/infra/validatingwebhookconfiguration | 85% | ➖
github.com/vmware-tanzu/vm-operator/controllers/infra/zone | 73% | ❌
github.com/vmware-tanzu/vm-operator/controllers/storageclass | 95% | ✔
github.com/vmware-tanzu/vm-operator/controllers/storagepolicyquota | 97% | ✔
github.com/vmware-tanzu/vm-operator/controllers/util/encoding | 73% | ❌
github.com/vmware-tanzu/vm-operator/controllers/virtualmachine/storagepolicyusage | 97% | ✔
github.com/vmware-tanzu/vm-operator/controllers/virtualmachine/virtualmachine | 71% | ❌
github.com/vmware-tanzu/vm-operator/controllers/virtualmachine/volume | 86% | ➖
github.com/vmware-tanzu/vm-operator/controllers/virtualmachineclass | 75% | ❌
github.com/vmware-tanzu/vm-operator/controllers/virtualmachineimagecache | 89% | ✔
github.com/vmware-tanzu/vm-operator/controllers/virtualmachinepublishrequest | 81% | ➖
github.com/vmware-tanzu/vm-operator/controllers/virtualmachinereplicaset | 67% | ❌
github.com/vmware-tanzu/vm-operator/controllers/virtualmachineservice | 82% | ➖
github.com/vmware-tanzu/vm-operator/controllers/virtualmachineservice/providers | 92% | ✔
github.com/vmware-tanzu/vm-operator/controllers/virtualmachinesetresourcepolicy | 82% | ➖
github.com/vmware-tanzu/vm-operator/controllers/virtualmachinewebconsolerequest | 72% | ❌
github.com/vmware-tanzu/vm-operator/controllers/virtualmachinewebconsolerequest/v1alpha1 | 72% | ❌
github.com/vmware-tanzu/vm-operator/controllers/virtualmachinewebconsolerequest/v1alpha1/conditions | 88% | ➖
github.com/vmware-tanzu/vm-operator/controllers/virtualmachinewebconsolerequest/v1alpha1/patch | 78% | ❌
github.com/vmware-tanzu/vm-operator/pkg/bitmask | 100% | ✔
github.com/vmware-tanzu/vm-operator/pkg/builder | 95% | ✔
github.com/vmware-tanzu/vm-operator/pkg/conditions | 90% | ✔
github.com/vmware-tanzu/vm-operator/pkg/config | 100% | ✔
github.com/vmware-tanzu/vm-operator/pkg/config/capabilities | 100% | ✔
github.com/vmware-tanzu/vm-operator/pkg/config/env | 100% | ✔
github.com/vmware-tanzu/vm-operator/pkg/context/generic | 100% | ✔
github.com/vmware-tanzu/vm-operator/pkg/context/operation | 100% | ✔
github.com/vmware-tanzu/vm-operator/pkg/errors | 100% | ✔
github.com/vmware-tanzu/vm-operator/pkg/exit | 100% | ✔
github.com/vmware-tanzu/vm-operator/pkg/mem | 100% | ✔
github.com/vmware-tanzu/vm-operator/pkg/patch | 78% | ❌
github.com/vmware-tanzu/vm-operator/pkg/prober | 91% | ✔
github.com/vmware-tanzu/vm-operator/pkg/prober/probe | 90% | ✔
github.com/vmware-tanzu/vm-operator/pkg/prober/worker | 77% | ❌
github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere | 76% | ❌
github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/client | 80% | ➖
github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/clustermodules | 73% | ❌
github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/config | 88% | ➖
github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/contentlibrary | 73% | ❌
github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/credentials | 100% | ✔
github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/network | 80% | ➖
github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/placement | 80% | ➖
github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/session | 57% | ❌
github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/storage | 44% | ❌
github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/sysprep | 100% | ✔
github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/vcenter | 86% | ➖
github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/virtualmachine | 85% | ➖
github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/vmlifecycle | 64% | ❌
github.com/vmware-tanzu/vm-operator/pkg/record | 87% | ➖
github.com/vmware-tanzu/vm-operator/pkg/topology | 91% | ✔
github.com/vmware-tanzu/vm-operator/pkg/util | 88% | ➖
github.com/vmware-tanzu/vm-operator/pkg/util/cloudinit | 89% | ✔
github.com/vmware-tanzu/vm-operator/pkg/util/cloudinit/validate | 91% | ✔
github.com/vmware-tanzu/vm-operator/pkg/util/image | 100% | ✔
github.com/vmware-tanzu/vm-operator/pkg/util/kube | 89% | ➖
github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource | 100% | ✔
github.com/vmware-tanzu/vm-operator/pkg/util/kube/internal | 100% | ✔
github.com/vmware-tanzu/vm-operator/pkg/util/kube/proxyaddr | 73% | ❌
github.com/vmware-tanzu/vm-operator/pkg/util/kube/spq | 99% | ✔
github.com/vmware-tanzu/vm-operator/pkg/util/netplan | 100% | ✔
github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache | 75% | ❌
github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache/internal | 100% | ✔
github.com/vmware-tanzu/vm-operator/pkg/util/paused | 100% | ✔
github.com/vmware-tanzu/vm-operator/pkg/util/ptr | 100% | ✔
github.com/vmware-tanzu/vm-operator/pkg/util/resize | 99% | ✔
github.com/vmware-tanzu/vm-operator/pkg/util/vmopv1 | 81% | ➖
github.com/vmware-tanzu/vm-operator/pkg/util/vsphere/client | 64% | ❌
github.com/vmware-tanzu/vm-operator/pkg/util/vsphere/library | 100% | ✔
github.com/vmware-tanzu/vm-operator/pkg/util/vsphere/vm | 79% | ❌
github.com/vmware-tanzu/vm-operator/pkg/util/vsphere/watcher | 85% | ➖
github.com/vmware-tanzu/vm-operator/pkg/vmconfig | 95% | ✔
github.com/vmware-tanzu/vm-operator/pkg/vmconfig/crypto | 91% | ✔
github.com/vmware-tanzu/vm-operator/pkg/vmconfig/diskpromo | 91% | ✔
github.com/vmware-tanzu/vm-operator/pkg/webconsolevalidation | 100% | ✔
github.com/vmware-tanzu/vm-operator/services/vm-watcher | 94% | ✔
github.com/vmware-tanzu/vm-operator/webhooks/common | 100% | ✔
github.com/vmware-tanzu/vm-operator/webhooks/persistentvolumeclaim/validation | 95% | ✔
github.com/vmware-tanzu/vm-operator/webhooks/unifiedstoragequota/validation | 89% | ✔
github.com/vmware-tanzu/vm-operator/webhooks/virtualmachine/mutation | 87% | ➖
github.com/vmware-tanzu/vm-operator/webhooks/virtualmachine/validation | 95% | ✔
github.com/vmware-tanzu/vm-operator/webhooks/virtualmachineclass/mutation | 62% | ❌
github.com/vmware-tanzu/vm-operator/webhooks/virtualmachineclass/validation | 89% | ➖
github.com/vmware-tanzu/vm-operator/webhooks/virtualmachinepublishrequest/validation | 92% | ✔
github.com/vmware-tanzu/vm-operator/webhooks/virtualmachinereplicaset/validation | 90% | ✔
github.com/vmware-tanzu/vm-operator/webhooks/virtualmachineservice/mutation | 67% | ❌
github.com/vmware-tanzu/vm-operator/webhooks/virtualmachineservice/validation | 92% | ✔
github.com/vmware-tanzu/vm-operator/webhooks/virtualmachinesetresourcepolicy/validation | 89% | ✔
github.com/vmware-tanzu/vm-operator/webhooks/virtualmachinewebconsolerequest/v1alpha1/validation | 92% | ✔
github.com/vmware-tanzu/vm-operator/webhooks/virtualmachinewebconsolerequest/validation | 92% | ✔
**Summary** | **82%** (11534 / 14023) | ➖

_Minimum allowed line rate is `79%`_

<!-- Sticky Pull Request Comment -->
author_association: NONE