From 5cc0079a6ef34d51e7503ae80864de49bde38582 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Fri, 24 Apr 2026 16:55:13 +0200 Subject: [PATCH 01/11] feat(vm): add uptime printable column Signed-off-by: Daniil Antoshin --- api/core/v1alpha2/virtual_machine.go | 3 + api/core/v1alpha2/zz_generated.deepcopy.go | 4 + crds/doc-ru-virtualmachines.yaml | 3 + crds/virtualmachines.yaml | 9 +++ .../pkg/controller/vm/internal/lifecycle.go | 11 +++ .../controller/vm/internal/lifecycle_test.go | 76 +++++++++++++++++++ 6 files changed, 106 insertions(+) create mode 100644 images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go diff --git a/api/core/v1alpha2/virtual_machine.go b/api/core/v1alpha2/virtual_machine.go index 5633a02610..8ff2841662 100644 --- a/api/core/v1alpha2/virtual_machine.go +++ b/api/core/v1alpha2/virtual_machine.go @@ -42,6 +42,7 @@ const ( // +kubebuilder:resource:categories={all,virtualization},scope=Namespaced,shortName={vm},singular=virtualmachine // +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="The phase of the virtual machine." // +kubebuilder:printcolumn:name="PhaseAge",type="date",JSONPath=".status.stats.phasesTransitions[-1].timestamp",description="Time since the virtual machine entered the current phase." +// +kubebuilder:printcolumn:name="Uptime",type="date",JSONPath=".status.runningSince",description="Time since the virtual machine started running." // +kubebuilder:printcolumn:name="Cores",priority=1,type="string",JSONPath=".spec.cpu.cores",description="The number of cores of the virtual machine." // +kubebuilder:printcolumn:name="CoreFraction",priority=1,type="string",JSONPath=".spec.cpu.coreFraction",description="Virtual machine core fraction. The range of available values is set in the `sizePolicy` parameter of the VirtualMachineClass; if it is not set, use values within the 1–100% range." // +kubebuilder:printcolumn:name="Memory",priority=1,type="string",JSONPath=".spec.memory.size",description="The amount of memory of the virtual machine." @@ -299,6 +300,8 @@ type VirtualMachineStatus struct { Stats *VirtualMachineStats `json:"stats,omitempty"` // Migration info. MigrationState *VirtualMachineMigrationState `json:"migrationState,omitempty"` + // RunningSince is the timestamp when the virtual machine entered the running state. + RunningSince *metav1.Time `json:"runningSince,omitempty"` // Generating a resource that was last processed by the controller. ObservedGeneration int64 `json:"observedGeneration,omitempty"` diff --git a/api/core/v1alpha2/zz_generated.deepcopy.go b/api/core/v1alpha2/zz_generated.deepcopy.go index e1939fd64a..7f59d6789e 100644 --- a/api/core/v1alpha2/zz_generated.deepcopy.go +++ b/api/core/v1alpha2/zz_generated.deepcopy.go @@ -3510,6 +3510,10 @@ func (in *VirtualMachineStatus) DeepCopyInto(out *VirtualMachineStatus) { *out = new(VirtualMachineMigrationState) (*in).DeepCopyInto(*out) } + if in.RunningSince != nil { + in, out := &in.RunningSince, &out.RunningSince + *out = (*in).DeepCopy() + } if in.RestartAwaitingChanges != nil { in, out := &in.RestartAwaitingChanges, &out.RestartAwaitingChanges *out = make([]apiextensionsv1.JSON, len(*in)) diff --git a/crds/doc-ru-virtualmachines.yaml b/crds/doc-ru-virtualmachines.yaml index 9998fc1112..e93d74887f 100644 --- a/crds/doc-ru-virtualmachines.yaml +++ b/crds/doc-ru-virtualmachines.yaml @@ -684,6 +684,9 @@ spec: result: description: | Результат миграции: `Succeeded` или `Failed`. + runningSince: + description: | + Время перехода виртуальной машины в состояние running. stats: description: Статистика по виртуальной машине. properties: diff --git a/crds/virtualmachines.yaml b/crds/virtualmachines.yaml index 88162c552b..d8f463793e 100644 --- a/crds/virtualmachines.yaml +++ b/crds/virtualmachines.yaml @@ -1153,6 +1153,11 @@ spec: - "" - "Succeeded" - "Failed" + runningSince: + description: Time when the virtual machine entered the running state. + format: date-time + nullable: true + type: string stats: type: object description: Virtual machine statistics. @@ -1426,6 +1431,10 @@ spec: jsonPath: .status.stats.phasesTransitions[-1].timestamp name: PhaseAge type: date + - description: Time since the virtual machine started running. + jsonPath: .status.runningSince + name: Uptime + type: date - description: Real number of the virtual machine cores. jsonPath: .status.resources.cpu.cores name: Cores diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go index 4f694654c0..28da2d9aea 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go @@ -122,6 +122,7 @@ func (h *LifeCycleHandler) Name() string { func (h *LifeCycleHandler) syncRunning(ctx context.Context, vm *v1alpha2.VirtualMachine, kvvm *virtv1.VirtualMachine, kvvmi *virtv1.VirtualMachineInstance, pod *corev1.Pod, log *slog.Logger) error { cb := conditions.NewConditionBuilder(vmcondition.TypeRunning).Generation(vm.GetGeneration()) + defer syncRunningSince(vm) if pod != nil && pod.Status.Message != "" { cb.Status(metav1.ConditionFalse). @@ -229,6 +230,16 @@ func (h *LifeCycleHandler) syncRunning(ctx context.Context, vm *v1alpha2.Virtual return nil } +func syncRunningSince(vm *v1alpha2.VirtualMachine) { + running, found := conditions.GetCondition(vmcondition.TypeRunning, vm.Status.Conditions) + if !found || running.Status != metav1.ConditionTrue { + vm.Status.RunningSince = nil + return + } + + vm.Status.RunningSince = running.LastTransitionTime.DeepCopy() +} + func (h *LifeCycleHandler) checkVMPodVolumeErrors(ctx context.Context, vm *v1alpha2.VirtualMachine, log *slog.Logger) error { var podList corev1.PodList err := h.client.List(ctx, &podList, &client.ListOptions{ diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go new file mode 100644 index 0000000000..1a1d920896 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go @@ -0,0 +1,76 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package internal + +import ( + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" +) + +var _ = Describe("LifeCycleHandler", func() { + Describe("syncRunningSince", func() { + It("sets runningSince from the Running condition last transition time", func() { + transitionTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)) + vm := &v1alpha2.VirtualMachine{ + Status: v1alpha2.VirtualMachineStatus{ + Conditions: []metav1.Condition{ + { + Type: vmcondition.TypeRunning.String(), + Status: metav1.ConditionTrue, + LastTransitionTime: transitionTime, + }, + }, + }, + } + + syncRunningSince(vm) + + Expect(vm.Status.RunningSince).NotTo(BeNil()) + Expect(vm.Status.RunningSince.Time).To(Equal(transitionTime.Time)) + }) + + DescribeTable("clears runningSince when the VM is not running", + func(conditions []metav1.Condition) { + transitionTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)) + vm := &v1alpha2.VirtualMachine{ + Status: v1alpha2.VirtualMachineStatus{ + RunningSince: &transitionTime, + Conditions: conditions, + }, + } + + syncRunningSince(vm) + + Expect(vm.Status.RunningSince).To(BeNil()) + }, + Entry("without the Running condition", nil), + Entry("with the Running condition set to False", []metav1.Condition{ + { + Type: vmcondition.TypeRunning.String(), + Status: metav1.ConditionFalse, + LastTransitionTime: metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)), + }, + }), + ) + }) +}) From b5ec371b88bae8ba2f1b8826b6a2e9bb89449596 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Tue, 28 Apr 2026 12:42:13 +0200 Subject: [PATCH 02/11] fix(vm): preserve uptime while VM remains running Signed-off-by: Daniil Antoshin --- .../pkg/controller/vm/internal/lifecycle.go | 4 ++++ .../controller/vm/internal/lifecycle_test.go | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go index 28da2d9aea..21e1556276 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go @@ -237,6 +237,10 @@ func syncRunningSince(vm *v1alpha2.VirtualMachine) { return } + if vm.Status.RunningSince != nil { + return + } + vm.Status.RunningSince = running.LastTransitionTime.DeepCopy() } diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go index 1a1d920896..10888e94d3 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go @@ -49,6 +49,28 @@ var _ = Describe("LifeCycleHandler", func() { Expect(vm.Status.RunningSince.Time).To(Equal(transitionTime.Time)) }) + It("keeps existing runningSince while the VM remains running", func() { + runningSince := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)) + transitionTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 10, 0, 0, time.UTC)) + vm := &v1alpha2.VirtualMachine{ + Status: v1alpha2.VirtualMachineStatus{ + RunningSince: &runningSince, + Conditions: []metav1.Condition{ + { + Type: vmcondition.TypeRunning.String(), + Status: metav1.ConditionTrue, + LastTransitionTime: transitionTime, + }, + }, + }, + } + + syncRunningSince(vm) + + Expect(vm.Status.RunningSince).NotTo(BeNil()) + Expect(vm.Status.RunningSince.Time).To(Equal(runningSince.Time)) + }) + DescribeTable("clears runningSince when the VM is not running", func(conditions []metav1.Condition) { transitionTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)) From f5d56e4b48049c329fe1ed9db408c99405fccffe Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Tue, 28 Apr 2026 18:26:37 +0200 Subject: [PATCH 03/11] fix(vm): keep running reason during migration Signed-off-by: Daniil Antoshin --- .../pkg/controller/vm/internal/lifecycle.go | 2 +- .../pkg/controller/vm/internal/lifecycle_test.go | 13 +++++++++++++ .../pkg/controller/vm/internal/util.go | 6 +++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go index 21e1556276..0fc635c4cc 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go @@ -215,7 +215,7 @@ func (h *LifeCycleHandler) syncRunning(ctx context.Context, vm *v1alpha2.Virtual for _, c := range kvvmi.Status.Conditions { if c.Type == virtv1.VirtualMachineInstanceReady { cb.Status(conditionStatus(string(c.Status))). - Reason(getKVMIReadyReason(c.Reason)). + Reason(getKVMIReadyReason(c.Status, c.Reason)). Message(c.Message) conditions.SetCondition(cb, &vm.Status.Conditions) return nil diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go index 10888e94d3..ef7fce0ea9 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go @@ -21,13 +21,26 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" ) var _ = Describe("LifeCycleHandler", func() { + Describe("getKVMIReadyReason", func() { + DescribeTable("maps empty Ready condition reason by status", + func(status corev1.ConditionStatus, expected conditions.Stringer) { + Expect(getKVMIReadyReason(status, "").String()).To(Equal(expected.String())) + }, + Entry("true status", corev1.ConditionTrue, vmcondition.ReasonVirtualMachineRunning), + Entry("false status", corev1.ConditionFalse, conditions.ReasonUnknown), + Entry("unknown status", corev1.ConditionUnknown, conditions.ReasonUnknown), + ) + }) + Describe("syncRunningSince", func() { It("sets runningSince from the Running condition last transition time", func() { transitionTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/util.go b/images/virtualization-artifact/pkg/controller/vm/internal/util.go index 9bd9ef9639..bb0b9f6605 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/util.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/util.go @@ -201,12 +201,16 @@ const ( failedCreatePodReason string = "FailedCreate" ) -func getKVMIReadyReason(kvmiReason string) conditions.Stringer { +func getKVMIReadyReason(kvmiStatus corev1.ConditionStatus, kvmiReason string) conditions.Stringer { if r, ok := mapReasons[kvmiReason]; ok { return r } if kvmiReason == "" { + if kvmiStatus == corev1.ConditionTrue { + return vmcondition.ReasonVirtualMachineRunning + } + return conditions.ReasonUnknown } From e96059564e939d1b4b270a49b0e55e5d9bc08c95 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Tue, 28 Apr 2026 18:28:11 +0200 Subject: [PATCH 04/11] fix(vm): keep ready reason helper signature Signed-off-by: Daniil Antoshin --- .../pkg/controller/vm/internal/lifecycle.go | 2 +- .../pkg/controller/vm/internal/lifecycle_test.go | 4 ++-- .../pkg/controller/vm/internal/util.go | 14 +++++++++----- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go index 0fc635c4cc..d41fb2df5d 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go @@ -215,7 +215,7 @@ func (h *LifeCycleHandler) syncRunning(ctx context.Context, vm *v1alpha2.Virtual for _, c := range kvvmi.Status.Conditions { if c.Type == virtv1.VirtualMachineInstanceReady { cb.Status(conditionStatus(string(c.Status))). - Reason(getKVMIReadyReason(c.Status, c.Reason)). + Reason(getKVMIReadyConditionReason(c.Status, c.Reason)). Message(c.Message) conditions.SetCondition(cb, &vm.Status.Conditions) return nil diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go index ef7fce0ea9..d5ad20cc42 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go @@ -30,10 +30,10 @@ import ( ) var _ = Describe("LifeCycleHandler", func() { - Describe("getKVMIReadyReason", func() { + Describe("getKVMIReadyConditionReason", func() { DescribeTable("maps empty Ready condition reason by status", func(status corev1.ConditionStatus, expected conditions.Stringer) { - Expect(getKVMIReadyReason(status, "").String()).To(Equal(expected.String())) + Expect(getKVMIReadyConditionReason(status, "").String()).To(Equal(expected.String())) }, Entry("true status", corev1.ConditionTrue, vmcondition.ReasonVirtualMachineRunning), Entry("false status", corev1.ConditionFalse, conditions.ReasonUnknown), diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/util.go b/images/virtualization-artifact/pkg/controller/vm/internal/util.go index bb0b9f6605..8660732d05 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/util.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/util.go @@ -201,16 +201,20 @@ const ( failedCreatePodReason string = "FailedCreate" ) -func getKVMIReadyReason(kvmiStatus corev1.ConditionStatus, kvmiReason string) conditions.Stringer { +func getKVMIReadyConditionReason(kvmiStatus corev1.ConditionStatus, kvmiReason string) conditions.Stringer { + if kvmiStatus == corev1.ConditionTrue && kvmiReason == "" { + return vmcondition.ReasonVirtualMachineRunning + } + + return getKVMIReadyReason(kvmiReason) +} + +func getKVMIReadyReason(kvmiReason string) conditions.Stringer { if r, ok := mapReasons[kvmiReason]; ok { return r } if kvmiReason == "" { - if kvmiStatus == corev1.ConditionTrue { - return vmcondition.ReasonVirtualMachineRunning - } - return conditions.ReasonUnknown } From 1d9b4ae08e02b8fb5acaae795ed2e538c1844b37 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Tue, 28 Apr 2026 18:30:53 +0200 Subject: [PATCH 05/11] fix(vm): inline ready condition reason mapping Signed-off-by: Daniil Antoshin --- .../pkg/controller/vm/internal/lifecycle.go | 6 +++++- .../pkg/controller/vm/internal/lifecycle_test.go | 13 ------------- .../pkg/controller/vm/internal/util.go | 8 -------- 3 files changed, 5 insertions(+), 22 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go index d41fb2df5d..39b4286f5c 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go @@ -214,8 +214,12 @@ func (h *LifeCycleHandler) syncRunning(ctx context.Context, vm *v1alpha2.Virtual } for _, c := range kvvmi.Status.Conditions { if c.Type == virtv1.VirtualMachineInstanceReady { + reason := getKVMIReadyReason(c.Reason) + if c.Status == corev1.ConditionTrue && c.Reason == "" { + reason = vmcondition.ReasonVirtualMachineRunning + } cb.Status(conditionStatus(string(c.Status))). - Reason(getKVMIReadyConditionReason(c.Status, c.Reason)). + Reason(reason). Message(c.Message) conditions.SetCondition(cb, &vm.Status.Conditions) return nil diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go index d5ad20cc42..10888e94d3 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go @@ -21,26 +21,13 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" ) var _ = Describe("LifeCycleHandler", func() { - Describe("getKVMIReadyConditionReason", func() { - DescribeTable("maps empty Ready condition reason by status", - func(status corev1.ConditionStatus, expected conditions.Stringer) { - Expect(getKVMIReadyConditionReason(status, "").String()).To(Equal(expected.String())) - }, - Entry("true status", corev1.ConditionTrue, vmcondition.ReasonVirtualMachineRunning), - Entry("false status", corev1.ConditionFalse, conditions.ReasonUnknown), - Entry("unknown status", corev1.ConditionUnknown, conditions.ReasonUnknown), - ) - }) - Describe("syncRunningSince", func() { It("sets runningSince from the Running condition last transition time", func() { transitionTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/util.go b/images/virtualization-artifact/pkg/controller/vm/internal/util.go index 8660732d05..9bd9ef9639 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/util.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/util.go @@ -201,14 +201,6 @@ const ( failedCreatePodReason string = "FailedCreate" ) -func getKVMIReadyConditionReason(kvmiStatus corev1.ConditionStatus, kvmiReason string) conditions.Stringer { - if kvmiStatus == corev1.ConditionTrue && kvmiReason == "" { - return vmcondition.ReasonVirtualMachineRunning - } - - return getKVMIReadyReason(kvmiReason) -} - func getKVMIReadyReason(kvmiReason string) conditions.Stringer { if r, ok := mapReasons[kvmiReason]; ok { return r From d26b41aa36abe2437caea0782f5a058f4e696796 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Tue, 28 Apr 2026 18:33:45 +0200 Subject: [PATCH 06/11] fix(vm): use running condition for uptime column Signed-off-by: Daniil Antoshin --- api/core/v1alpha2/virtual_machine.go | 2 +- crds/virtualmachines.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/core/v1alpha2/virtual_machine.go b/api/core/v1alpha2/virtual_machine.go index 8ff2841662..004b240bb4 100644 --- a/api/core/v1alpha2/virtual_machine.go +++ b/api/core/v1alpha2/virtual_machine.go @@ -42,7 +42,7 @@ const ( // +kubebuilder:resource:categories={all,virtualization},scope=Namespaced,shortName={vm},singular=virtualmachine // +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="The phase of the virtual machine." // +kubebuilder:printcolumn:name="PhaseAge",type="date",JSONPath=".status.stats.phasesTransitions[-1].timestamp",description="Time since the virtual machine entered the current phase." -// +kubebuilder:printcolumn:name="Uptime",type="date",JSONPath=".status.runningSince",description="Time since the virtual machine started running." +// +kubebuilder:printcolumn:name="Uptime",type="date",JSONPath=".status.conditions[?(@.type==\"Running\" && @.status==\"True\")].lastTransitionTime",description="Time since the virtual machine started running." // +kubebuilder:printcolumn:name="Cores",priority=1,type="string",JSONPath=".spec.cpu.cores",description="The number of cores of the virtual machine." // +kubebuilder:printcolumn:name="CoreFraction",priority=1,type="string",JSONPath=".spec.cpu.coreFraction",description="Virtual machine core fraction. The range of available values is set in the `sizePolicy` parameter of the VirtualMachineClass; if it is not set, use values within the 1–100% range." // +kubebuilder:printcolumn:name="Memory",priority=1,type="string",JSONPath=".spec.memory.size",description="The amount of memory of the virtual machine." diff --git a/crds/virtualmachines.yaml b/crds/virtualmachines.yaml index d8f463793e..f0b81d0daa 100644 --- a/crds/virtualmachines.yaml +++ b/crds/virtualmachines.yaml @@ -1432,7 +1432,7 @@ spec: name: PhaseAge type: date - description: Time since the virtual machine started running. - jsonPath: .status.runningSince + jsonPath: '.status.conditions[?(@.type=="Running" && @.status=="True")].lastTransitionTime' name: Uptime type: date - description: Real number of the virtual machine cores. From 3786a2cbb7f982d2298c7a1559c338dac6d3b5e3 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Tue, 28 Apr 2026 18:36:10 +0200 Subject: [PATCH 07/11] refactor(vm): remove runningSince status field Signed-off-by: Daniil Antoshin --- api/core/v1alpha2/virtual_machine.go | 2 - api/core/v1alpha2/zz_generated.deepcopy.go | 4 - crds/doc-ru-virtualmachines.yaml | 3 - crds/virtualmachines.yaml | 5 - .../pkg/controller/vm/internal/lifecycle.go | 15 --- .../controller/vm/internal/lifecycle_test.go | 98 ------------------- 6 files changed, 127 deletions(-) delete mode 100644 images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go diff --git a/api/core/v1alpha2/virtual_machine.go b/api/core/v1alpha2/virtual_machine.go index 004b240bb4..2e7cb36791 100644 --- a/api/core/v1alpha2/virtual_machine.go +++ b/api/core/v1alpha2/virtual_machine.go @@ -300,8 +300,6 @@ type VirtualMachineStatus struct { Stats *VirtualMachineStats `json:"stats,omitempty"` // Migration info. MigrationState *VirtualMachineMigrationState `json:"migrationState,omitempty"` - // RunningSince is the timestamp when the virtual machine entered the running state. - RunningSince *metav1.Time `json:"runningSince,omitempty"` // Generating a resource that was last processed by the controller. ObservedGeneration int64 `json:"observedGeneration,omitempty"` diff --git a/api/core/v1alpha2/zz_generated.deepcopy.go b/api/core/v1alpha2/zz_generated.deepcopy.go index 7f59d6789e..e1939fd64a 100644 --- a/api/core/v1alpha2/zz_generated.deepcopy.go +++ b/api/core/v1alpha2/zz_generated.deepcopy.go @@ -3510,10 +3510,6 @@ func (in *VirtualMachineStatus) DeepCopyInto(out *VirtualMachineStatus) { *out = new(VirtualMachineMigrationState) (*in).DeepCopyInto(*out) } - if in.RunningSince != nil { - in, out := &in.RunningSince, &out.RunningSince - *out = (*in).DeepCopy() - } if in.RestartAwaitingChanges != nil { in, out := &in.RestartAwaitingChanges, &out.RestartAwaitingChanges *out = make([]apiextensionsv1.JSON, len(*in)) diff --git a/crds/doc-ru-virtualmachines.yaml b/crds/doc-ru-virtualmachines.yaml index e93d74887f..9998fc1112 100644 --- a/crds/doc-ru-virtualmachines.yaml +++ b/crds/doc-ru-virtualmachines.yaml @@ -684,9 +684,6 @@ spec: result: description: | Результат миграции: `Succeeded` или `Failed`. - runningSince: - description: | - Время перехода виртуальной машины в состояние running. stats: description: Статистика по виртуальной машине. properties: diff --git a/crds/virtualmachines.yaml b/crds/virtualmachines.yaml index f0b81d0daa..90fcc69641 100644 --- a/crds/virtualmachines.yaml +++ b/crds/virtualmachines.yaml @@ -1153,11 +1153,6 @@ spec: - "" - "Succeeded" - "Failed" - runningSince: - description: Time when the virtual machine entered the running state. - format: date-time - nullable: true - type: string stats: type: object description: Virtual machine statistics. diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go index 39b4286f5c..370a26d909 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go @@ -122,7 +122,6 @@ func (h *LifeCycleHandler) Name() string { func (h *LifeCycleHandler) syncRunning(ctx context.Context, vm *v1alpha2.VirtualMachine, kvvm *virtv1.VirtualMachine, kvvmi *virtv1.VirtualMachineInstance, pod *corev1.Pod, log *slog.Logger) error { cb := conditions.NewConditionBuilder(vmcondition.TypeRunning).Generation(vm.GetGeneration()) - defer syncRunningSince(vm) if pod != nil && pod.Status.Message != "" { cb.Status(metav1.ConditionFalse). @@ -234,20 +233,6 @@ func (h *LifeCycleHandler) syncRunning(ctx context.Context, vm *v1alpha2.Virtual return nil } -func syncRunningSince(vm *v1alpha2.VirtualMachine) { - running, found := conditions.GetCondition(vmcondition.TypeRunning, vm.Status.Conditions) - if !found || running.Status != metav1.ConditionTrue { - vm.Status.RunningSince = nil - return - } - - if vm.Status.RunningSince != nil { - return - } - - vm.Status.RunningSince = running.LastTransitionTime.DeepCopy() -} - func (h *LifeCycleHandler) checkVMPodVolumeErrors(ctx context.Context, vm *v1alpha2.VirtualMachine, log *slog.Logger) error { var podList corev1.PodList err := h.client.List(ctx, &podList, &client.ListOptions{ diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go deleted file mode 100644 index 10888e94d3..0000000000 --- a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go +++ /dev/null @@ -1,98 +0,0 @@ -/* -Copyright 2026 Flant JSC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package internal - -import ( - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/deckhouse/virtualization/api/core/v1alpha2" - "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" -) - -var _ = Describe("LifeCycleHandler", func() { - Describe("syncRunningSince", func() { - It("sets runningSince from the Running condition last transition time", func() { - transitionTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)) - vm := &v1alpha2.VirtualMachine{ - Status: v1alpha2.VirtualMachineStatus{ - Conditions: []metav1.Condition{ - { - Type: vmcondition.TypeRunning.String(), - Status: metav1.ConditionTrue, - LastTransitionTime: transitionTime, - }, - }, - }, - } - - syncRunningSince(vm) - - Expect(vm.Status.RunningSince).NotTo(BeNil()) - Expect(vm.Status.RunningSince.Time).To(Equal(transitionTime.Time)) - }) - - It("keeps existing runningSince while the VM remains running", func() { - runningSince := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)) - transitionTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 10, 0, 0, time.UTC)) - vm := &v1alpha2.VirtualMachine{ - Status: v1alpha2.VirtualMachineStatus{ - RunningSince: &runningSince, - Conditions: []metav1.Condition{ - { - Type: vmcondition.TypeRunning.String(), - Status: metav1.ConditionTrue, - LastTransitionTime: transitionTime, - }, - }, - }, - } - - syncRunningSince(vm) - - Expect(vm.Status.RunningSince).NotTo(BeNil()) - Expect(vm.Status.RunningSince.Time).To(Equal(runningSince.Time)) - }) - - DescribeTable("clears runningSince when the VM is not running", - func(conditions []metav1.Condition) { - transitionTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)) - vm := &v1alpha2.VirtualMachine{ - Status: v1alpha2.VirtualMachineStatus{ - RunningSince: &transitionTime, - Conditions: conditions, - }, - } - - syncRunningSince(vm) - - Expect(vm.Status.RunningSince).To(BeNil()) - }, - Entry("without the Running condition", nil), - Entry("with the Running condition set to False", []metav1.Condition{ - { - Type: vmcondition.TypeRunning.String(), - Status: metav1.ConditionFalse, - LastTransitionTime: metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)), - }, - }), - ) - }) -}) From c5268d2edc567cae17e16e8f643ec7be2af2bcbb Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Tue, 28 Apr 2026 18:50:06 +0200 Subject: [PATCH 08/11] feat(vm): track uptime start in stats Signed-off-by: Daniil Antoshin --- api/core/v1alpha2/virtual_machine.go | 4 +- api/core/v1alpha2/zz_generated.deepcopy.go | 4 + crds/doc-ru-virtualmachines.yaml | 2 + crds/virtualmachines.yaml | 7 +- .../pkg/controller/vm/internal/lifecycle.go | 21 ++++ .../controller/vm/internal/lifecycle_test.go | 100 ++++++++++++++++++ 6 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go diff --git a/api/core/v1alpha2/virtual_machine.go b/api/core/v1alpha2/virtual_machine.go index 2e7cb36791..0e26bf862b 100644 --- a/api/core/v1alpha2/virtual_machine.go +++ b/api/core/v1alpha2/virtual_machine.go @@ -42,7 +42,7 @@ const ( // +kubebuilder:resource:categories={all,virtualization},scope=Namespaced,shortName={vm},singular=virtualmachine // +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="The phase of the virtual machine." // +kubebuilder:printcolumn:name="PhaseAge",type="date",JSONPath=".status.stats.phasesTransitions[-1].timestamp",description="Time since the virtual machine entered the current phase." -// +kubebuilder:printcolumn:name="Uptime",type="date",JSONPath=".status.conditions[?(@.type==\"Running\" && @.status==\"True\")].lastTransitionTime",description="Time since the virtual machine started running." +// +kubebuilder:printcolumn:name="Uptime",type="date",JSONPath=".status.stats.startedAt",description="Time since the virtual machine started running." // +kubebuilder:printcolumn:name="Cores",priority=1,type="string",JSONPath=".spec.cpu.cores",description="The number of cores of the virtual machine." // +kubebuilder:printcolumn:name="CoreFraction",priority=1,type="string",JSONPath=".spec.cpu.coreFraction",description="Virtual machine core fraction. The range of available values is set in the `sizePolicy` parameter of the VirtualMachineClass; if it is not set, use values within the 1–100% range." // +kubebuilder:printcolumn:name="Memory",priority=1,type="string",JSONPath=".spec.memory.size",description="The amount of memory of the virtual machine." @@ -331,6 +331,8 @@ type VirtualMachineStats struct { PhasesTransitions []VirtualMachinePhaseTransitionTimestamp `json:"phasesTransitions,omitempty"` // Launch information. LaunchTimeDuration VirtualMachineLaunchTimeDuration `json:"launchTimeDuration,omitempty"` + // StartedAt is the timestamp when the virtual machine started running. + StartedAt *metav1.Time `json:"startedAt,omitempty"` } // VirtualMachinePhaseTransitionTimestamp gives a timestamp in relation to when a phase is set on a vm. diff --git a/api/core/v1alpha2/zz_generated.deepcopy.go b/api/core/v1alpha2/zz_generated.deepcopy.go index e1939fd64a..a6f0c4fad1 100644 --- a/api/core/v1alpha2/zz_generated.deepcopy.go +++ b/api/core/v1alpha2/zz_generated.deepcopy.go @@ -3471,6 +3471,10 @@ func (in *VirtualMachineStats) DeepCopyInto(out *VirtualMachineStats) { } } in.LaunchTimeDuration.DeepCopyInto(&out.LaunchTimeDuration) + if in.StartedAt != nil { + in, out := &in.StartedAt, &out.StartedAt + *out = (*in).DeepCopy() + } return } diff --git a/crds/doc-ru-virtualmachines.yaml b/crds/doc-ru-virtualmachines.yaml index 9998fc1112..4c2f484eeb 100644 --- a/crds/doc-ru-virtualmachines.yaml +++ b/crds/doc-ru-virtualmachines.yaml @@ -704,6 +704,8 @@ spec: description: Время ожидания запуска виртуальной машины. `starting` -> `running`. guestOSAgentStarting: description: Время ожидания запуска guestOsAgent. `running` -> `running` с guestOSAgent." + startedAt: + description: Время запуска виртуальной машины. observedGeneration: description: | Поколение ресурса, которое в последний раз обрабатывалось контроллером. diff --git a/crds/virtualmachines.yaml b/crds/virtualmachines.yaml index 90fcc69641..51ede93cb0 100644 --- a/crds/virtualmachines.yaml +++ b/crds/virtualmachines.yaml @@ -1197,6 +1197,11 @@ spec: description: Waiting time for the guestOsAgent to start. `running` -> `running` with guestOSAgent. nullable: true type: string + startedAt: + description: Time when the virtual machine started running. + format: date-time + nullable: true + type: string conditions: description: State of the running virtual machine. items: @@ -1427,7 +1432,7 @@ spec: name: PhaseAge type: date - description: Time since the virtual machine started running. - jsonPath: '.status.conditions[?(@.type=="Running" && @.status=="True")].lastTransitionTime' + jsonPath: .status.stats.startedAt name: Uptime type: date - description: Real number of the virtual machine cores. diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go index 370a26d909..ea8e68b043 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go @@ -122,6 +122,7 @@ func (h *LifeCycleHandler) Name() string { func (h *LifeCycleHandler) syncRunning(ctx context.Context, vm *v1alpha2.VirtualMachine, kvvm *virtv1.VirtualMachine, kvvmi *virtv1.VirtualMachineInstance, pod *corev1.Pod, log *slog.Logger) error { cb := conditions.NewConditionBuilder(vmcondition.TypeRunning).Generation(vm.GetGeneration()) + defer syncStartedAt(vm) if pod != nil && pod.Status.Message != "" { cb.Status(metav1.ConditionFalse). @@ -233,6 +234,26 @@ func (h *LifeCycleHandler) syncRunning(ctx context.Context, vm *v1alpha2.Virtual return nil } +func syncStartedAt(vm *v1alpha2.VirtualMachine) { + running, found := conditions.GetCondition(vmcondition.TypeRunning, vm.Status.Conditions) + if !found || running.Status != metav1.ConditionTrue { + if vm.Status.Stats != nil { + vm.Status.Stats.StartedAt = nil + } + return + } + + if vm.Status.Stats == nil { + vm.Status.Stats = &v1alpha2.VirtualMachineStats{} + } + + if vm.Status.Stats.StartedAt != nil { + return + } + + vm.Status.Stats.StartedAt = running.LastTransitionTime.DeepCopy() +} + func (h *LifeCycleHandler) checkVMPodVolumeErrors(ctx context.Context, vm *v1alpha2.VirtualMachine, log *slog.Logger) error { var podList corev1.PodList err := h.client.List(ctx, &podList, &client.ListOptions{ diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go new file mode 100644 index 0000000000..86546e66d4 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go @@ -0,0 +1,100 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package internal + +import ( + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" +) + +var _ = Describe("LifeCycleHandler", func() { + Describe("syncStartedAt", func() { + It("sets startedAt from the Running condition last transition time", func() { + transitionTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)) + vm := &v1alpha2.VirtualMachine{ + Status: v1alpha2.VirtualMachineStatus{ + Conditions: []metav1.Condition{ + { + Type: vmcondition.TypeRunning.String(), + Status: metav1.ConditionTrue, + LastTransitionTime: transitionTime, + }, + }, + }, + } + + syncStartedAt(vm) + + Expect(vm.Status.Stats).NotTo(BeNil()) + Expect(vm.Status.Stats.StartedAt).NotTo(BeNil()) + Expect(vm.Status.Stats.StartedAt.Time).To(Equal(transitionTime.Time)) + }) + + It("keeps existing startedAt while the VM remains running", func() { + startedAt := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)) + transitionTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 10, 0, 0, time.UTC)) + vm := &v1alpha2.VirtualMachine{ + Status: v1alpha2.VirtualMachineStatus{ + Stats: &v1alpha2.VirtualMachineStats{StartedAt: &startedAt}, + Conditions: []metav1.Condition{ + { + Type: vmcondition.TypeRunning.String(), + Status: metav1.ConditionTrue, + LastTransitionTime: transitionTime, + }, + }, + }, + } + + syncStartedAt(vm) + + Expect(vm.Status.Stats.StartedAt).NotTo(BeNil()) + Expect(vm.Status.Stats.StartedAt.Time).To(Equal(startedAt.Time)) + }) + + DescribeTable("clears startedAt when the VM is not running", + func(conditions []metav1.Condition) { + startedAt := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)) + vm := &v1alpha2.VirtualMachine{ + Status: v1alpha2.VirtualMachineStatus{ + Stats: &v1alpha2.VirtualMachineStats{StartedAt: &startedAt}, + Conditions: conditions, + }, + } + + syncStartedAt(vm) + + Expect(vm.Status.Stats).NotTo(BeNil()) + Expect(vm.Status.Stats.StartedAt).To(BeNil()) + }, + Entry("without the Running condition", nil), + Entry("with the Running condition set to False", []metav1.Condition{ + { + Type: vmcondition.TypeRunning.String(), + Status: metav1.ConditionFalse, + LastTransitionTime: metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)), + }, + }), + ) + }) +}) From b8d6a9a8fca2bc7c84cb255d68664716fe9ee577 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Tue, 28 Apr 2026 18:55:38 +0200 Subject: [PATCH 09/11] refactor(vm): derive uptime from running reason Signed-off-by: Daniil Antoshin --- api/core/v1alpha2/virtual_machine.go | 4 +- api/core/v1alpha2/zz_generated.deepcopy.go | 4 - crds/doc-ru-virtualmachines.yaml | 2 - crds/virtualmachines.yaml | 7 +- .../pkg/controller/vm/internal/lifecycle.go | 21 ---- .../controller/vm/internal/lifecycle_test.go | 100 ------------------ 6 files changed, 2 insertions(+), 136 deletions(-) delete mode 100644 images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go diff --git a/api/core/v1alpha2/virtual_machine.go b/api/core/v1alpha2/virtual_machine.go index 0e26bf862b..77c3fd244d 100644 --- a/api/core/v1alpha2/virtual_machine.go +++ b/api/core/v1alpha2/virtual_machine.go @@ -42,7 +42,7 @@ const ( // +kubebuilder:resource:categories={all,virtualization},scope=Namespaced,shortName={vm},singular=virtualmachine // +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="The phase of the virtual machine." // +kubebuilder:printcolumn:name="PhaseAge",type="date",JSONPath=".status.stats.phasesTransitions[-1].timestamp",description="Time since the virtual machine entered the current phase." -// +kubebuilder:printcolumn:name="Uptime",type="date",JSONPath=".status.stats.startedAt",description="Time since the virtual machine started running." +// +kubebuilder:printcolumn:name="Uptime",type="date",JSONPath=".status.conditions[?(@.reason==\"Running\")].lastTransitionTime",description="Time since the virtual machine started running." // +kubebuilder:printcolumn:name="Cores",priority=1,type="string",JSONPath=".spec.cpu.cores",description="The number of cores of the virtual machine." // +kubebuilder:printcolumn:name="CoreFraction",priority=1,type="string",JSONPath=".spec.cpu.coreFraction",description="Virtual machine core fraction. The range of available values is set in the `sizePolicy` parameter of the VirtualMachineClass; if it is not set, use values within the 1–100% range." // +kubebuilder:printcolumn:name="Memory",priority=1,type="string",JSONPath=".spec.memory.size",description="The amount of memory of the virtual machine." @@ -331,8 +331,6 @@ type VirtualMachineStats struct { PhasesTransitions []VirtualMachinePhaseTransitionTimestamp `json:"phasesTransitions,omitempty"` // Launch information. LaunchTimeDuration VirtualMachineLaunchTimeDuration `json:"launchTimeDuration,omitempty"` - // StartedAt is the timestamp when the virtual machine started running. - StartedAt *metav1.Time `json:"startedAt,omitempty"` } // VirtualMachinePhaseTransitionTimestamp gives a timestamp in relation to when a phase is set on a vm. diff --git a/api/core/v1alpha2/zz_generated.deepcopy.go b/api/core/v1alpha2/zz_generated.deepcopy.go index a6f0c4fad1..e1939fd64a 100644 --- a/api/core/v1alpha2/zz_generated.deepcopy.go +++ b/api/core/v1alpha2/zz_generated.deepcopy.go @@ -3471,10 +3471,6 @@ func (in *VirtualMachineStats) DeepCopyInto(out *VirtualMachineStats) { } } in.LaunchTimeDuration.DeepCopyInto(&out.LaunchTimeDuration) - if in.StartedAt != nil { - in, out := &in.StartedAt, &out.StartedAt - *out = (*in).DeepCopy() - } return } diff --git a/crds/doc-ru-virtualmachines.yaml b/crds/doc-ru-virtualmachines.yaml index 4c2f484eeb..9998fc1112 100644 --- a/crds/doc-ru-virtualmachines.yaml +++ b/crds/doc-ru-virtualmachines.yaml @@ -704,8 +704,6 @@ spec: description: Время ожидания запуска виртуальной машины. `starting` -> `running`. guestOSAgentStarting: description: Время ожидания запуска guestOsAgent. `running` -> `running` с guestOSAgent." - startedAt: - description: Время запуска виртуальной машины. observedGeneration: description: | Поколение ресурса, которое в последний раз обрабатывалось контроллером. diff --git a/crds/virtualmachines.yaml b/crds/virtualmachines.yaml index 51ede93cb0..95dc239c5d 100644 --- a/crds/virtualmachines.yaml +++ b/crds/virtualmachines.yaml @@ -1197,11 +1197,6 @@ spec: description: Waiting time for the guestOsAgent to start. `running` -> `running` with guestOSAgent. nullable: true type: string - startedAt: - description: Time when the virtual machine started running. - format: date-time - nullable: true - type: string conditions: description: State of the running virtual machine. items: @@ -1432,7 +1427,7 @@ spec: name: PhaseAge type: date - description: Time since the virtual machine started running. - jsonPath: .status.stats.startedAt + jsonPath: '.status.conditions[?(@.reason=="Running")].lastTransitionTime' name: Uptime type: date - description: Real number of the virtual machine cores. diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go index ea8e68b043..370a26d909 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go @@ -122,7 +122,6 @@ func (h *LifeCycleHandler) Name() string { func (h *LifeCycleHandler) syncRunning(ctx context.Context, vm *v1alpha2.VirtualMachine, kvvm *virtv1.VirtualMachine, kvvmi *virtv1.VirtualMachineInstance, pod *corev1.Pod, log *slog.Logger) error { cb := conditions.NewConditionBuilder(vmcondition.TypeRunning).Generation(vm.GetGeneration()) - defer syncStartedAt(vm) if pod != nil && pod.Status.Message != "" { cb.Status(metav1.ConditionFalse). @@ -234,26 +233,6 @@ func (h *LifeCycleHandler) syncRunning(ctx context.Context, vm *v1alpha2.Virtual return nil } -func syncStartedAt(vm *v1alpha2.VirtualMachine) { - running, found := conditions.GetCondition(vmcondition.TypeRunning, vm.Status.Conditions) - if !found || running.Status != metav1.ConditionTrue { - if vm.Status.Stats != nil { - vm.Status.Stats.StartedAt = nil - } - return - } - - if vm.Status.Stats == nil { - vm.Status.Stats = &v1alpha2.VirtualMachineStats{} - } - - if vm.Status.Stats.StartedAt != nil { - return - } - - vm.Status.Stats.StartedAt = running.LastTransitionTime.DeepCopy() -} - func (h *LifeCycleHandler) checkVMPodVolumeErrors(ctx context.Context, vm *v1alpha2.VirtualMachine, log *slog.Logger) error { var podList corev1.PodList err := h.client.List(ctx, &podList, &client.ListOptions{ diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go deleted file mode 100644 index 86546e66d4..0000000000 --- a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle_test.go +++ /dev/null @@ -1,100 +0,0 @@ -/* -Copyright 2026 Flant JSC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package internal - -import ( - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/deckhouse/virtualization/api/core/v1alpha2" - "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" -) - -var _ = Describe("LifeCycleHandler", func() { - Describe("syncStartedAt", func() { - It("sets startedAt from the Running condition last transition time", func() { - transitionTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)) - vm := &v1alpha2.VirtualMachine{ - Status: v1alpha2.VirtualMachineStatus{ - Conditions: []metav1.Condition{ - { - Type: vmcondition.TypeRunning.String(), - Status: metav1.ConditionTrue, - LastTransitionTime: transitionTime, - }, - }, - }, - } - - syncStartedAt(vm) - - Expect(vm.Status.Stats).NotTo(BeNil()) - Expect(vm.Status.Stats.StartedAt).NotTo(BeNil()) - Expect(vm.Status.Stats.StartedAt.Time).To(Equal(transitionTime.Time)) - }) - - It("keeps existing startedAt while the VM remains running", func() { - startedAt := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)) - transitionTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 10, 0, 0, time.UTC)) - vm := &v1alpha2.VirtualMachine{ - Status: v1alpha2.VirtualMachineStatus{ - Stats: &v1alpha2.VirtualMachineStats{StartedAt: &startedAt}, - Conditions: []metav1.Condition{ - { - Type: vmcondition.TypeRunning.String(), - Status: metav1.ConditionTrue, - LastTransitionTime: transitionTime, - }, - }, - }, - } - - syncStartedAt(vm) - - Expect(vm.Status.Stats.StartedAt).NotTo(BeNil()) - Expect(vm.Status.Stats.StartedAt.Time).To(Equal(startedAt.Time)) - }) - - DescribeTable("clears startedAt when the VM is not running", - func(conditions []metav1.Condition) { - startedAt := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)) - vm := &v1alpha2.VirtualMachine{ - Status: v1alpha2.VirtualMachineStatus{ - Stats: &v1alpha2.VirtualMachineStats{StartedAt: &startedAt}, - Conditions: conditions, - }, - } - - syncStartedAt(vm) - - Expect(vm.Status.Stats).NotTo(BeNil()) - Expect(vm.Status.Stats.StartedAt).To(BeNil()) - }, - Entry("without the Running condition", nil), - Entry("with the Running condition set to False", []metav1.Condition{ - { - Type: vmcondition.TypeRunning.String(), - Status: metav1.ConditionFalse, - LastTransitionTime: metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)), - }, - }), - ) - }) -}) From 8eeb119d48e27e1643646dd7992a915b862590c7 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Tue, 28 Apr 2026 19:10:47 +0200 Subject: [PATCH 10/11] docs(vm): clarify uptime condition reason Signed-off-by: Daniil Antoshin --- crds/doc-ru-virtualmachines.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crds/doc-ru-virtualmachines.yaml b/crds/doc-ru-virtualmachines.yaml index 9998fc1112..d825e07ef2 100644 --- a/crds/doc-ru-virtualmachines.yaml +++ b/crds/doc-ru-virtualmachines.yaml @@ -626,7 +626,9 @@ spec: `.metadata.generation`, на основе которого было установлено условие. Например, если `.metadata.generation` в настоящее время имеет значение `12`, а `.status.conditions[x].observedgeneration` имеет значение `9`, то условие устарело. reason: - description: Краткая причина последнего перехода состояния. + description: | + Краткая причина последнего перехода состояния. + Значение `Running` используется для отображения времени работы ВМ в колонке `Uptime`. status: description: | Статус условия. Возможные значения: `True`, `False`, `Unknown`. From 38322d658488d52aeaa7bb89171a714fcc234606 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Tue, 28 Apr 2026 19:12:28 +0200 Subject: [PATCH 11/11] refactor(vm): remove phase age column Signed-off-by: Daniil Antoshin --- api/core/v1alpha2/virtual_machine.go | 1 - crds/virtualmachines.yaml | 4 ---- 2 files changed, 5 deletions(-) diff --git a/api/core/v1alpha2/virtual_machine.go b/api/core/v1alpha2/virtual_machine.go index 77c3fd244d..51f848406c 100644 --- a/api/core/v1alpha2/virtual_machine.go +++ b/api/core/v1alpha2/virtual_machine.go @@ -41,7 +41,6 @@ const ( // +kubebuilder:subresource:status // +kubebuilder:resource:categories={all,virtualization},scope=Namespaced,shortName={vm},singular=virtualmachine // +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="The phase of the virtual machine." -// +kubebuilder:printcolumn:name="PhaseAge",type="date",JSONPath=".status.stats.phasesTransitions[-1].timestamp",description="Time since the virtual machine entered the current phase." // +kubebuilder:printcolumn:name="Uptime",type="date",JSONPath=".status.conditions[?(@.reason==\"Running\")].lastTransitionTime",description="Time since the virtual machine started running." // +kubebuilder:printcolumn:name="Cores",priority=1,type="string",JSONPath=".spec.cpu.cores",description="The number of cores of the virtual machine." // +kubebuilder:printcolumn:name="CoreFraction",priority=1,type="string",JSONPath=".spec.cpu.coreFraction",description="Virtual machine core fraction. The range of available values is set in the `sizePolicy` parameter of the VirtualMachineClass; if it is not set, use values within the 1–100% range." diff --git a/crds/virtualmachines.yaml b/crds/virtualmachines.yaml index 95dc239c5d..9243b4fb32 100644 --- a/crds/virtualmachines.yaml +++ b/crds/virtualmachines.yaml @@ -1422,10 +1422,6 @@ spec: jsonPath: .status.phase name: Phase type: string - - description: Time since the virtual machine entered the current phase. - jsonPath: .status.stats.phasesTransitions[-1].timestamp - name: PhaseAge - type: date - description: Time since the virtual machine started running. jsonPath: '.status.conditions[?(@.reason=="Running")].lastTransitionTime' name: Uptime