From 7c7729aa061b6ab0f5da5a7780ab80707e8106c3 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Fri, 10 Apr 2026 10:09:54 +0200 Subject: [PATCH 1/3] feat(vm): show uptime in vm brief Signed-off-by: Daniil Antoshin --- api/client/kubeclient/async.go | 12 +++-- api/client/kubeclient/streamer.go | 4 +- api/client/kubeclient/websocket.go | 4 +- api/core/v1alpha2/virtual_machine.go | 5 ++- api/core/v1alpha2/zz_generated.deepcopy.go | 4 ++ crds/doc-ru-virtualmachines.yaml | 2 + crds/virtualmachines.yaml | 11 +++-- .../pkg/controller/vm/internal/statistic.go | 10 +++++ .../controller/vm/internal/statistic_test.go | 44 +++++++++++++++++++ 9 files changed, 85 insertions(+), 11 deletions(-) diff --git a/api/client/kubeclient/async.go b/api/client/kubeclient/async.go index 61ed3f56d0..2d22345fbf 100644 --- a/api/client/kubeclient/async.go +++ b/api/client/kubeclient/async.go @@ -49,7 +49,7 @@ func (aws *asyncWSRoundTripper) WebsocketCallback(ws *websocket.Conn, resp *http if resp != nil && resp.StatusCode != http.StatusOK { return enrichError(err, resp) } - return fmt.Errorf("Can't connect to websocket: %s\n", err.Error()) + return fmt.Errorf("can't connect to websocket: %w", err) } aws.Connection <- ws @@ -105,7 +105,9 @@ func asyncSubresourceHelper( } if response != nil { - defer response.Body.Close() + defer func() { + _ = response.Body.Close() + }() switch response.StatusCode { case http.StatusOK: case http.StatusNotFound: @@ -165,7 +167,7 @@ func enrichError(httpErr error, resp *http.Response) error { if resp == nil { return httpErr } - httpErr = fmt.Errorf("Can't connect to websocket (%d): %s\n", resp.StatusCode, httpErr.Error()) + httpErr = fmt.Errorf("can't connect to websocket (%d): %w", resp.StatusCode, httpErr) status := &metav1.Status{} if resp.Header.Get("Content-Type") != "application/json" { @@ -201,7 +203,9 @@ type WebsocketRoundTripper struct { func (d *WebsocketRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { conn, resp, err := d.Dialer.Dial(r.URL.String(), r.Header) if err == nil { - defer conn.Close() + defer func() { + _ = conn.Close() + }() } return resp, d.Do(conn, resp, err) } diff --git a/api/client/kubeclient/streamer.go b/api/client/kubeclient/streamer.go index 216bfd2f0e..120929a398 100644 --- a/api/client/kubeclient/streamer.go +++ b/api/client/kubeclient/streamer.go @@ -69,10 +69,10 @@ type wsConn struct { } func (c *wsConn) SetDeadline(t time.Time) error { - if err := c.Conn.SetWriteDeadline(t); err != nil { + if err := c.SetWriteDeadline(t); err != nil { return err } - return c.Conn.SetReadDeadline(t) + return c.SetReadDeadline(t) } func NewWebsocketStreamer(conn *websocket.Conn, done chan struct{}) *wsStreamer { diff --git a/api/client/kubeclient/websocket.go b/api/client/kubeclient/websocket.go index 94c2018732..2cb4e4ccfb 100644 --- a/api/client/kubeclient/websocket.go +++ b/api/client/kubeclient/websocket.go @@ -76,7 +76,9 @@ func (s *binaryWriter) Write(p []byte) (int, error) { if err != nil { return 0, convert(err) } - defer w.Close() + defer func() { + _ = w.Close() + }() n, err := w.Write(p) return n, err } diff --git a/api/core/v1alpha2/virtual_machine.go b/api/core/v1alpha2/virtual_machine.go index 62ba4393aa..b9796b94eb 100644 --- a/api/core/v1alpha2/virtual_machine.go +++ b/api/core/v1alpha2/virtual_machine.go @@ -49,7 +49,7 @@ const ( // +kubebuilder:printcolumn:name="Migratable",priority=1,type="string",JSONPath=".status.conditions[?(@.type=='Migratable')].status",description="Is it possible to migrate a virtual machine." // +kubebuilder:printcolumn:name="Node",type="string",JSONPath=".status.nodeName",description="The node where the virtual machine is running." // +kubebuilder:printcolumn:name="IPAddress",type="string",JSONPath=".status.ipAddress",description="The IP address of the virtual machine." -// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time of creation resource." +// +kubebuilder:printcolumn:name="Uptime",type="date",JSONPath=".status.runningSince",description="Time since the virtual machine has been running." // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type VirtualMachine struct { @@ -298,6 +298,9 @@ type VirtualMachineStatus struct { Stats *VirtualMachineStats `json:"stats,omitempty"` // Migration info. MigrationState *VirtualMachineMigrationState `json:"migrationState,omitempty"` + // The timestamp when the virtual machine most recently entered a running state. + // +nullable + 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..ce1497f6eb 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." + runningSince: + description: Время, когда виртуальная машина в последний раз перешла в состояние running. observedGeneration: description: | Поколение ресурса, которое в последний раз обрабатывалось контроллером. diff --git a/crds/virtualmachines.yaml b/crds/virtualmachines.yaml index da6d232bdc..6052a91330 100644 --- a/crds/virtualmachines.yaml +++ b/crds/virtualmachines.yaml @@ -1256,6 +1256,11 @@ spec: items: type: object x-kubernetes-preserve-unknown-fields: true + runningSince: + description: The timestamp when the virtual machine most recently entered a running state. + format: date-time + nullable: true + type: string observedGeneration: type: integer description: Resource generation last processed by the controller. @@ -1460,9 +1465,9 @@ spec: jsonPath: .status.ipAddress name: IPAddress type: string - - description: Time of resource creation. - jsonPath: .metadata.creationTimestamp - name: Age + - description: Time since the virtual machine has been running. + jsonPath: .status.runningSince + name: Uptime type: date subresources: status: {} diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/statistic.go b/images/virtualization-artifact/pkg/controller/vm/internal/statistic.go index 41471b19cb..2f4e1de416 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/statistic.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/statistic.go @@ -316,6 +316,16 @@ func (h *StatisticHandler) syncStats(current, changed *v1alpha2.VirtualMachine, pts := NewPhaseTransitions(stats.PhasesTransitions, current.Status.Phase, changed.Status.Phase) stats.PhasesTransitions = pts + changed.Status.RunningSince = nil + for i := len(pts) - 1; i >= 0; i-- { + if pts[i].Phase == v1alpha2.MachineRunning { + changed.Status.RunningSince = pts[i].Timestamp.DeepCopy() + break + } + } + if changed.Status.Phase != v1alpha2.MachineRunning && changed.Status.Phase != v1alpha2.MachineMigrating && changed.Status.Phase != v1alpha2.MachinePause { + changed.Status.RunningSince = nil + } launchTimeDuration := stats.LaunchTimeDuration diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/statistic_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/statistic_test.go index 2b460b9453..e234e1c2a3 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/statistic_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/statistic_test.go @@ -18,11 +18,13 @@ package internal import ( "context" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" virtv1 "kubevirt.io/api/core/v1" @@ -308,4 +310,46 @@ var _ = Describe("TestStatisticHandler", func() { }, ), ) + + It("should expose runningSince for active virtual machine phases", func() { + runningSince := metav1.NewTime(time.Now().Add(-10 * time.Minute).Truncate(time.Second)) + current := newVM(1, ptr.To("50%"), "512Mi") + current.Status.Phase = v1alpha2.MachineRunning + current.Status.Stats = &v1alpha2.VirtualMachineStats{ + PhasesTransitions: []v1alpha2.VirtualMachinePhaseTransitionTimestamp{ + {Phase: v1alpha2.MachineStarting, Timestamp: metav1.NewTime(runningSince.Add(-15 * time.Second))}, + {Phase: v1alpha2.MachineRunning, Timestamp: runningSince}, + }, + } + changed := current.DeepCopy() + changed.Status.Phase = v1alpha2.MachineMigrating + + (&StatisticHandler{}).syncStats(current, changed, nil) + + Expect(changed.Status.RunningSince).NotTo(BeNil()) + Expect(changed.Status.RunningSince.Time).To(Equal(runningSince.Time)) + Expect(changed.Status.Stats).NotTo(BeNil()) + Expect(changed.Status.Stats.PhasesTransitions).To(HaveLen(3)) + Expect(changed.Status.Stats.PhasesTransitions[2].Phase).To(Equal(v1alpha2.MachineMigrating)) + Expect(changed.Status.Stats.PhasesTransitions[2].Timestamp.IsZero()).To(BeFalse()) + Expect(changed.Status.Stats.PhasesTransitions[2].Timestamp.Time).To(BeTemporally("~", time.Now(), 2*time.Second)) + }) + + It("should clear runningSince for inactive virtual machine phases", func() { + runningSince := metav1.NewTime(time.Now().Add(-10 * time.Minute).Truncate(time.Second)) + current := newVM(1, ptr.To("50%"), "512Mi") + current.Status.Phase = v1alpha2.MachineRunning + current.Status.RunningSince = runningSince.DeepCopy() + current.Status.Stats = &v1alpha2.VirtualMachineStats{ + PhasesTransitions: []v1alpha2.VirtualMachinePhaseTransitionTimestamp{ + {Phase: v1alpha2.MachineRunning, Timestamp: runningSince}, + }, + } + changed := current.DeepCopy() + changed.Status.Phase = v1alpha2.MachineStopped + + (&StatisticHandler{}).syncStats(current, changed, nil) + + Expect(changed.Status.RunningSince).To(BeNil()) + }) }) From 9441a5fca3f4d5e59c4dc8a84c6b872dbfd04688 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Fri, 10 Apr 2026 11:12:28 +0200 Subject: [PATCH 2/3] fix(api, vm): add phase age printer column Signed-off-by: Daniil Antoshin --- api/core/v1alpha2/virtual_machine.go | 1 + crds/virtualmachines.yaml | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/api/core/v1alpha2/virtual_machine.go b/api/core/v1alpha2/virtual_machine.go index b9796b94eb..d9c01fccfb 100644 --- a/api/core/v1alpha2/virtual_machine.go +++ b/api/core/v1alpha2/virtual_machine.go @@ -41,6 +41,7 @@ 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="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 6052a91330..5e863799da 100644 --- a/crds/virtualmachines.yaml +++ b/crds/virtualmachines.yaml @@ -1427,6 +1427,10 @@ 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: Real number of the virtual machine cores. jsonPath: .status.resources.cpu.cores name: Cores From ecad030ae65305ea5e9a98c08a750caf0f6d1541 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Fri, 10 Apr 2026 11:20:28 +0200 Subject: [PATCH 3/3] fix(api, vm): keep age and add phase age Signed-off-by: Daniil Antoshin --- api/core/v1alpha2/virtual_machine.go | 5 +-- api/core/v1alpha2/zz_generated.deepcopy.go | 4 -- crds/doc-ru-virtualmachines.yaml | 2 - crds/virtualmachines.yaml | 11 ++--- .../pkg/controller/vm/internal/statistic.go | 10 ----- .../controller/vm/internal/statistic_test.go | 44 ------------------- 6 files changed, 4 insertions(+), 72 deletions(-) diff --git a/api/core/v1alpha2/virtual_machine.go b/api/core/v1alpha2/virtual_machine.go index d9c01fccfb..5633a02610 100644 --- a/api/core/v1alpha2/virtual_machine.go +++ b/api/core/v1alpha2/virtual_machine.go @@ -50,7 +50,7 @@ const ( // +kubebuilder:printcolumn:name="Migratable",priority=1,type="string",JSONPath=".status.conditions[?(@.type=='Migratable')].status",description="Is it possible to migrate a virtual machine." // +kubebuilder:printcolumn:name="Node",type="string",JSONPath=".status.nodeName",description="The node where the virtual machine is running." // +kubebuilder:printcolumn:name="IPAddress",type="string",JSONPath=".status.ipAddress",description="The IP address of the virtual machine." -// +kubebuilder:printcolumn:name="Uptime",type="date",JSONPath=".status.runningSince",description="Time since the virtual machine has been running." +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time of creation resource." // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type VirtualMachine struct { @@ -299,9 +299,6 @@ type VirtualMachineStatus struct { Stats *VirtualMachineStats `json:"stats,omitempty"` // Migration info. MigrationState *VirtualMachineMigrationState `json:"migrationState,omitempty"` - // The timestamp when the virtual machine most recently entered a running state. - // +nullable - 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 ce1497f6eb..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." - runningSince: - description: Время, когда виртуальная машина в последний раз перешла в состояние running. observedGeneration: description: | Поколение ресурса, которое в последний раз обрабатывалось контроллером. diff --git a/crds/virtualmachines.yaml b/crds/virtualmachines.yaml index 5e863799da..88162c552b 100644 --- a/crds/virtualmachines.yaml +++ b/crds/virtualmachines.yaml @@ -1256,11 +1256,6 @@ spec: items: type: object x-kubernetes-preserve-unknown-fields: true - runningSince: - description: The timestamp when the virtual machine most recently entered a running state. - format: date-time - nullable: true - type: string observedGeneration: type: integer description: Resource generation last processed by the controller. @@ -1469,9 +1464,9 @@ spec: jsonPath: .status.ipAddress name: IPAddress type: string - - description: Time since the virtual machine has been running. - jsonPath: .status.runningSince - name: Uptime + - description: Time of resource creation. + jsonPath: .metadata.creationTimestamp + name: Age type: date subresources: status: {} diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/statistic.go b/images/virtualization-artifact/pkg/controller/vm/internal/statistic.go index 2f4e1de416..41471b19cb 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/statistic.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/statistic.go @@ -316,16 +316,6 @@ func (h *StatisticHandler) syncStats(current, changed *v1alpha2.VirtualMachine, pts := NewPhaseTransitions(stats.PhasesTransitions, current.Status.Phase, changed.Status.Phase) stats.PhasesTransitions = pts - changed.Status.RunningSince = nil - for i := len(pts) - 1; i >= 0; i-- { - if pts[i].Phase == v1alpha2.MachineRunning { - changed.Status.RunningSince = pts[i].Timestamp.DeepCopy() - break - } - } - if changed.Status.Phase != v1alpha2.MachineRunning && changed.Status.Phase != v1alpha2.MachineMigrating && changed.Status.Phase != v1alpha2.MachinePause { - changed.Status.RunningSince = nil - } launchTimeDuration := stats.LaunchTimeDuration diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/statistic_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/statistic_test.go index e234e1c2a3..2b460b9453 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/statistic_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/statistic_test.go @@ -18,13 +18,11 @@ package internal import ( "context" - "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" virtv1 "kubevirt.io/api/core/v1" @@ -310,46 +308,4 @@ var _ = Describe("TestStatisticHandler", func() { }, ), ) - - It("should expose runningSince for active virtual machine phases", func() { - runningSince := metav1.NewTime(time.Now().Add(-10 * time.Minute).Truncate(time.Second)) - current := newVM(1, ptr.To("50%"), "512Mi") - current.Status.Phase = v1alpha2.MachineRunning - current.Status.Stats = &v1alpha2.VirtualMachineStats{ - PhasesTransitions: []v1alpha2.VirtualMachinePhaseTransitionTimestamp{ - {Phase: v1alpha2.MachineStarting, Timestamp: metav1.NewTime(runningSince.Add(-15 * time.Second))}, - {Phase: v1alpha2.MachineRunning, Timestamp: runningSince}, - }, - } - changed := current.DeepCopy() - changed.Status.Phase = v1alpha2.MachineMigrating - - (&StatisticHandler{}).syncStats(current, changed, nil) - - Expect(changed.Status.RunningSince).NotTo(BeNil()) - Expect(changed.Status.RunningSince.Time).To(Equal(runningSince.Time)) - Expect(changed.Status.Stats).NotTo(BeNil()) - Expect(changed.Status.Stats.PhasesTransitions).To(HaveLen(3)) - Expect(changed.Status.Stats.PhasesTransitions[2].Phase).To(Equal(v1alpha2.MachineMigrating)) - Expect(changed.Status.Stats.PhasesTransitions[2].Timestamp.IsZero()).To(BeFalse()) - Expect(changed.Status.Stats.PhasesTransitions[2].Timestamp.Time).To(BeTemporally("~", time.Now(), 2*time.Second)) - }) - - It("should clear runningSince for inactive virtual machine phases", func() { - runningSince := metav1.NewTime(time.Now().Add(-10 * time.Minute).Truncate(time.Second)) - current := newVM(1, ptr.To("50%"), "512Mi") - current.Status.Phase = v1alpha2.MachineRunning - current.Status.RunningSince = runningSince.DeepCopy() - current.Status.Stats = &v1alpha2.VirtualMachineStats{ - PhasesTransitions: []v1alpha2.VirtualMachinePhaseTransitionTimestamp{ - {Phase: v1alpha2.MachineRunning, Timestamp: runningSince}, - }, - } - changed := current.DeepCopy() - changed.Status.Phase = v1alpha2.MachineStopped - - (&StatisticHandler{}).syncStats(current, changed, nil) - - Expect(changed.Status.RunningSince).To(BeNil()) - }) })