From 0612e83e80ce58820f5c83b68c1543ac5674ed6f Mon Sep 17 00:00:00 2001 From: Faisal Abujabal Date: Tue, 30 Dec 2025 13:04:53 -0600 Subject: [PATCH] Set the VC created at timestamp in VM CR status Introduing a new Provider and Provider.CreationTimestamp field in the VM's status custom resource which sets the creation timestamp from VC/VPXD on VM's creation --- api/v1alpha1/zz_generated.conversion.go | 1 + api/v1alpha2/zz_generated.conversion.go | 1 + api/v1alpha3/zz_generated.conversion.go | 1 + api/v1alpha4/zz_generated.conversion.go | 1 + api/v1alpha5/virtualmachine_types.go | 20 ++++++ api/v1alpha5/zz_generated.deepcopy.go | 24 +++++++ ...vmoperator.vmware.com_virtualmachines.yaml | 16 +++++ .../vsphere/vmlifecycle/update_status.go | 18 ++++- .../vsphere/vmlifecycle/update_status_test.go | 69 +++++++++++++++++++ 9 files changed, 149 insertions(+), 2 deletions(-) diff --git a/api/v1alpha1/zz_generated.conversion.go b/api/v1alpha1/zz_generated.conversion.go index 14f67888e..2421a08a7 100644 --- a/api/v1alpha1/zz_generated.conversion.go +++ b/api/v1alpha1/zz_generated.conversion.go @@ -2207,6 +2207,7 @@ func autoConvert_v1alpha5_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.Provider requires manual conversion: does not exist in peer-type // WARNING: in.CurrentSnapshot requires manual conversion: does not exist in peer-type // WARNING: in.RootSnapshots requires manual conversion: does not exist in peer-type // WARNING: in.Guest requires manual conversion: does not exist in peer-type diff --git a/api/v1alpha2/zz_generated.conversion.go b/api/v1alpha2/zz_generated.conversion.go index 95c8bfaf4..19e28322e 100644 --- a/api/v1alpha2/zz_generated.conversion.go +++ b/api/v1alpha2/zz_generated.conversion.go @@ -3883,6 +3883,7 @@ func autoConvert_v1alpha5_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.Provider requires manual conversion: does not exist in peer-type // WARNING: in.CurrentSnapshot requires manual conversion: does not exist in peer-type // WARNING: in.RootSnapshots requires manual conversion: does not exist in peer-type // WARNING: in.Guest requires manual conversion: does not exist in peer-type diff --git a/api/v1alpha3/zz_generated.conversion.go b/api/v1alpha3/zz_generated.conversion.go index 531564e78..2b80ae7a9 100644 --- a/api/v1alpha3/zz_generated.conversion.go +++ b/api/v1alpha3/zz_generated.conversion.go @@ -4471,6 +4471,7 @@ func autoConvert_v1alpha5_VirtualMachineStatus_To_v1alpha3_VirtualMachineStatus( } else { out.Storage = nil } + // WARNING: in.Provider requires manual conversion: does not exist in peer-type // WARNING: in.CurrentSnapshot requires manual conversion: does not exist in peer-type // WARNING: in.RootSnapshots requires manual conversion: does not exist in peer-type // WARNING: in.Guest requires manual conversion: does not exist in peer-type diff --git a/api/v1alpha4/zz_generated.conversion.go b/api/v1alpha4/zz_generated.conversion.go index 8c69df43f..61fdb8df6 100644 --- a/api/v1alpha4/zz_generated.conversion.go +++ b/api/v1alpha4/zz_generated.conversion.go @@ -4642,6 +4642,7 @@ func autoConvert_v1alpha5_VirtualMachineStatus_To_v1alpha4_VirtualMachineStatus( } else { out.Storage = nil } + // WARNING: in.Provider requires manual conversion: does not exist in peer-type // WARNING: in.CurrentSnapshot requires manual conversion: does not exist in peer-type // WARNING: in.RootSnapshots requires manual conversion: does not exist in peer-type // WARNING: in.Guest requires manual conversion: does not exist in peer-type diff --git a/api/v1alpha5/virtualmachine_types.go b/api/v1alpha5/virtualmachine_types.go index 09813a572..f43fcb8e7 100644 --- a/api/v1alpha5/virtualmachine_types.go +++ b/api/v1alpha5/virtualmachine_types.go @@ -1220,6 +1220,20 @@ type VirtualMachineGuestStatus struct { GuestFullName string `json:"guestFullName,omitempty"` } +// VirtualMachineProviderStatus describes the observed state of the +// VirtualMachine from the underlying infrastructure provider (vSphere/vCenter). +type VirtualMachineProviderStatus struct { + // +optional + + // CreationTimestamp describes the timestamp when the VirtualMachine was + // created in the infrastructure provider. This value is retrieved + // from the VM's config.createDate field and is immutable once set. + // + // This field is only populated after the VM has been successfully created + // in vCenter and the creation timestamp is available. + CreationTimestamp *metav1.Time `json:"creationTimestamp,omitempty"` +} + // VirtualMachineStatus defines the observed state of a VirtualMachine instance. type VirtualMachineStatus struct { // +optional @@ -1320,6 +1334,12 @@ type VirtualMachineStatus struct { // +optional + // Provider describes the observed state of the VirtualMachine from + // the underlying infrastructure provider (vSphere/vCenter). + Provider *VirtualMachineProviderStatus `json:"provider,omitempty"` + + // +optional + // CurrentSnapshot describes the observed working snapshot of the VirtualMachine. // This field contains the name of the current snapshot. CurrentSnapshot *VirtualMachineSnapshotReference `json:"currentSnapshot,omitempty"` diff --git a/api/v1alpha5/zz_generated.deepcopy.go b/api/v1alpha5/zz_generated.deepcopy.go index 95eab11fe..61b396ed6 100644 --- a/api/v1alpha5/zz_generated.deepcopy.go +++ b/api/v1alpha5/zz_generated.deepcopy.go @@ -2669,6 +2669,25 @@ func (in *VirtualMachinePlacementStatus) DeepCopy() *VirtualMachinePlacementStat return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineProviderStatus) DeepCopyInto(out *VirtualMachineProviderStatus) { + *out = *in + if in.CreationTimestamp != nil { + in, out := &in.CreationTimestamp, &out.CreationTimestamp + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineProviderStatus. +func (in *VirtualMachineProviderStatus) DeepCopy() *VirtualMachineProviderStatus { + if in == nil { + return nil + } + out := new(VirtualMachineProviderStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VirtualMachinePublishRequest) DeepCopyInto(out *VirtualMachinePublishRequest) { *out = *in @@ -3544,6 +3563,11 @@ func (in *VirtualMachineStatus) DeepCopyInto(out *VirtualMachineStatus) { *out = new(VirtualMachineStorageStatus) (*in).DeepCopyInto(*out) } + if in.Provider != nil { + in, out := &in.Provider, &out.Provider + *out = new(VirtualMachineProviderStatus) + (*in).DeepCopyInto(*out) + } if in.CurrentSnapshot != nil { in, out := &in.CurrentSnapshot, &out.CurrentSnapshot *out = new(VirtualMachineSnapshotReference) diff --git a/config/crd/bases/vmoperator.vmware.com_virtualmachines.yaml b/config/crd/bases/vmoperator.vmware.com_virtualmachines.yaml index cc1ae8b17..2115694e3 100644 --- a/config/crd/bases/vmoperator.vmware.com_virtualmachines.yaml +++ b/config/crd/bases/vmoperator.vmware.com_virtualmachines.yaml @@ -13215,6 +13215,22 @@ spec: - PoweredOn - Suspended type: string + provider: + description: |- + Provider describes the observed state of the VirtualMachine from + the underlying infrastructure provider (vSphere/vCenter). + properties: + creationTimestamp: + description: |- + CreationTimestamp describes the timestamp when the VirtualMachine was + created in the infrastructure provider. This value is retrieved + from the VM's config.createDate field and is immutable once set. + + This field is only populated after the VM has been successfully created + in vCenter and the creation timestamp is available. + format: date-time + type: string + type: object rootSnapshots: description: |- RootSnapshots represents the observed list of root snapshots of diff --git a/pkg/providers/vsphere/vmlifecycle/update_status.go b/pkg/providers/vsphere/vmlifecycle/update_status.go index 3dce7b4a5..65b3b14fd 100644 --- a/pkg/providers/vsphere/vmlifecycle/update_status.go +++ b/pkg/providers/vsphere/vmlifecycle/update_status.go @@ -66,7 +66,7 @@ func ReconcileStatus( errs = append(errs, reconcileStatusAnno2Conditions(vmCtx, k8sClient, vcVM, data)...) errs = append(errs, reconcileStatusClass(vmCtx, k8sClient, vcVM, data)...) errs = append(errs, reconcileStatusPowerState(vmCtx, k8sClient, vcVM, data)...) - errs = append(errs, reconcileStatusIdentifiers(vmCtx, k8sClient, vcVM, data)...) + errs = append(errs, reconcileStatusPlatform(vmCtx, k8sClient, vcVM, data)...) errs = append(errs, reconcileStatusHardware(vmCtx, k8sClient, vcVM, data)...) errs = append(errs, reconcileStatusHardwareVersion(vmCtx, k8sClient, vcVM, data)...) errs = append(errs, reconcileStatusGuest(vmCtx, k8sClient, vcVM, data)...) @@ -327,16 +327,30 @@ func reconcileStatusPowerState( return nil } -func reconcileStatusIdentifiers( +// reconcileStatusPlatform updates the VM's status with immutable metadata +// from vCenter, including identifiers (UniqueID, BiosUUID, InstanceUUID) +// and provider-specific information like the creation timestamp. +func reconcileStatusPlatform( vmCtx pkgctx.VirtualMachineContext, _ ctrlclient.Client, _ *object.VirtualMachine, _ ReconcileStatusData) []error { //nolint:unparam + // TODO(v1alpha6): Deprecate and migrate these fields to the Provider status. vmCtx.VM.Status.UniqueID = vmCtx.MoVM.Self.Value vmCtx.VM.Status.BiosUUID = vmCtx.MoVM.Summary.Config.Uuid vmCtx.VM.Status.InstanceUUID = vmCtx.MoVM.Summary.Config.InstanceUuid + if vmCtx.VM.Status.Provider == nil { + vmCtx.VM.Status.Provider = &vmopv1.VirtualMachineProviderStatus{} + } + // The creation timestamp is immutable once set. + if vmCtx.VM.Status.Provider.CreationTimestamp == nil { + if config := vmCtx.MoVM.Config; config != nil && config.CreateDate != nil { + vmCtx.VM.Status.Provider.CreationTimestamp = &metav1.Time{Time: *config.CreateDate} + } + } + return nil } diff --git a/pkg/providers/vsphere/vmlifecycle/update_status_test.go b/pkg/providers/vsphere/vmlifecycle/update_status_test.go index 7cd4f89a9..88a80068f 100644 --- a/pkg/providers/vsphere/vmlifecycle/update_status_test.go +++ b/pkg/providers/vsphere/vmlifecycle/update_status_test.go @@ -3607,6 +3607,75 @@ var _ = Describe("UpdateStatus", func() { Expect(status.InstanceUUID).To(Equal(instanceUUID)) Expect(status.HardwareVersion).To(Equal(int32(19))) }) + + Context("Provider.CreationTimestamp", func() { + var createDate *metav1.Time + + BeforeEach(func() { + t := metav1.Now() + createDate = &t + if vmCtx.MoVM.Config == nil { + vmCtx.MoVM.Config = &vimtypes.VirtualMachineConfigInfo{} + } + vmCtx.MoVM.Config.CreateDate = &createDate.Time + }) + + When("Provider.CreationTimestamp is not set in status", func() { + BeforeEach(func() { + vmCtx.VM.Status.Provider = nil + }) + + It("should set Provider.CreationTimestamp from config.createDate", func() { + Expect(vmCtx.VM.Status.Provider).ToNot(BeNil()) + Expect(vmCtx.VM.Status.Provider.CreationTimestamp).ToNot(BeNil()) + Expect(vmCtx.VM.Status.Provider.CreationTimestamp.Time).To(Equal(createDate.Time)) + }) + }) + + When("Provider.CreationTimestamp is already set in status", func() { + var existingDate *metav1.Time + + BeforeEach(func() { + // set created at to 1 day ago. + t := metav1.NewTime(metav1.Now().Add(-24 * 3600 * 1000000000)) + existingDate = &t + vmCtx.VM.Status.Provider = &vmopv1.VirtualMachineProviderStatus{ + CreationTimestamp: existingDate, + } + }) + + It("should not change Provider.CreationTimestamp because it is immutable", func() { + Expect(vmCtx.VM.Status.Provider).ToNot(BeNil()) + Expect(vmCtx.VM.Status.Provider.CreationTimestamp).ToNot(BeNil()) + Expect(vmCtx.VM.Status.Provider.CreationTimestamp.Time).To(Equal(existingDate.Time)) + Expect(vmCtx.VM.Status.Provider.CreationTimestamp.Time).ToNot(Equal(createDate.Time)) + }) + }) + + When("config.createDate is nil", func() { + BeforeEach(func() { + vmCtx.MoVM.Config.CreateDate = nil + vmCtx.VM.Status.Provider = nil + }) + + It("should initialize Provider but not set CreationTimestamp", func() { + Expect(vmCtx.VM.Status.Provider).ToNot(BeNil()) + Expect(vmCtx.VM.Status.Provider.CreationTimestamp).To(BeNil()) + }) + }) + + When("config is nil", func() { + BeforeEach(func() { + vmCtx.MoVM.Config = nil + vmCtx.VM.Status.Provider = nil + }) + + It("should initialize Provider but not set CreationTimestamp", func() { + Expect(vmCtx.VM.Status.Provider).ToNot(BeNil()) + Expect(vmCtx.VM.Status.Provider.CreationTimestamp).To(BeNil()) + }) + }) + }) }) Context("Misc Status fields", func() {