Skip to content

Commit e1db9d2

Browse files
authored
fix(vm): uptime update status field (#2304)
Add .status.stats.lastStartTime to VirtualMachine and use it for the Uptime printable column. lastStartTime is calculated from the VM Running condition. As a temporary compatibility fallback, if kvvmi.status.phaseTransitionTimestamps contains the Running phase timestamp and it differs from the VM condition timestamp by more than 10 minutes, the controller uses the VMI timestamp instead. The fallback is marked with TODO and should be removed after 2026-10-29. --------- Signed-off-by: Daniil Antoshin <daniil.antoshin@flant.com>
1 parent 3181e16 commit e1db9d2

6 files changed

Lines changed: 179 additions & 3 deletions

File tree

api/core/v1alpha2/virtual_machine.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const (
4141
// +kubebuilder:subresource:status
4242
// +kubebuilder:resource:categories={all,virtualization},scope=Namespaced,shortName={vm},singular=virtualmachine
4343
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="The phase of the virtual machine."
44-
// +kubebuilder:printcolumn:name="Uptime",type="date",JSONPath=".status.conditions[?(@.reason==\"Running\")].lastTransitionTime",description="Time since the virtual machine started running."
44+
// +kubebuilder:printcolumn:name="Uptime",type="date",JSONPath=".status.stats.lastStartTime",description="Time since the virtual machine started running."
4545
// +kubebuilder:printcolumn:name="Cores",priority=1,type="string",JSONPath=".spec.cpu.cores",description="The number of cores of the virtual machine."
4646
// +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."
4747
// +kubebuilder:printcolumn:name="Memory",priority=1,type="string",JSONPath=".spec.memory.size",description="The amount of memory of the virtual machine."
@@ -330,6 +330,8 @@ type VirtualMachineStats struct {
330330
PhasesTransitions []VirtualMachinePhaseTransitionTimestamp `json:"phasesTransitions,omitempty"`
331331
// Launch information.
332332
LaunchTimeDuration VirtualMachineLaunchTimeDuration `json:"launchTimeDuration,omitempty"`
333+
// LastStartTime is the timestamp when the virtual machine last started running.
334+
LastStartTime *metav1.Time `json:"lastStartTime,omitempty"`
333335
}
334336

335337
// VirtualMachinePhaseTransitionTimestamp gives a timestamp in relation to when a phase is set on a vm.

api/core/v1alpha2/zz_generated.deepcopy.go

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crds/doc-ru-virtualmachines.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,8 @@ spec:
706706
description: Время ожидания запуска виртуальной машины. `starting` -> `running`.
707707
guestOSAgentStarting:
708708
description: Время ожидания запуска guestOsAgent. `running` -> `running` с guestOSAgent."
709+
lastStartTime:
710+
description: Время последнего запуска виртуальной машины.
709711
observedGeneration:
710712
description: |
711713
Поколение ресурса, которое в последний раз обрабатывалось контроллером.

crds/virtualmachines.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1197,6 +1197,11 @@ spec:
11971197
description: Waiting time for the guestOsAgent to start. `running` -> `running` with guestOSAgent.
11981198
nullable: true
11991199
type: string
1200+
lastStartTime:
1201+
description: Time when the virtual machine last started running.
1202+
format: date-time
1203+
nullable: true
1204+
type: string
12001205
conditions:
12011206
description: State of the running virtual machine.
12021207
items:
@@ -1423,7 +1428,7 @@ spec:
14231428
name: Phase
14241429
type: string
14251430
- description: Time since the virtual machine started running.
1426-
jsonPath: '.status.conditions[?(@.reason=="Running")].lastTransitionTime'
1431+
jsonPath: .status.stats.lastStartTime
14271432
name: Uptime
14281433
type: date
14291434
- description: Real number of the virtual machine cores.

images/virtualization-artifact/pkg/controller/vm/internal/statistic.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,14 @@ import (
3434
"github.com/deckhouse/virtualization-controller/pkg/controller/kvbuilder"
3535
"github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/state"
3636
"github.com/deckhouse/virtualization/api/core/v1alpha2"
37+
"github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition"
3738
)
3839

39-
const nameStatisticHandler = "StatisticHandler"
40+
const (
41+
nameStatisticHandler = "StatisticHandler"
42+
// TODO: Remove this fallback after 2026-10-29.
43+
lastStartTimePhaseTransitionMaxDiff = 10 * time.Minute
44+
)
4045

4146
func NewStatisticHandler(client client.Client) *StatisticHandler {
4247
return &StatisticHandler{client: client}
@@ -367,6 +372,52 @@ func (h *StatisticHandler) syncStats(current, changed *v1alpha2.VirtualMachine,
367372

368373
stats.LaunchTimeDuration = launchTimeDuration
369374
changed.Status.Stats = &stats
375+
syncLastStartTime(changed, kvvmi)
376+
}
377+
378+
func syncLastStartTime(vm *v1alpha2.VirtualMachine, kvvmi *virtv1.VirtualMachineInstance) {
379+
running := getRunningCondition(vm)
380+
if running == nil || running.Status != metav1.ConditionTrue {
381+
if vm.Status.Stats != nil {
382+
vm.Status.Stats.LastStartTime = nil
383+
}
384+
return
385+
}
386+
387+
kvvmiRunningAt := getKVVMIRunningPhaseTransitionTimestamp(kvvmi)
388+
if kvvmiRunningAt != nil && running.LastTransitionTime.Sub(kvvmiRunningAt.Time).Abs() > lastStartTimePhaseTransitionMaxDiff {
389+
running.LastTransitionTime = *kvvmiRunningAt.DeepCopy()
390+
}
391+
392+
if vm.Status.Stats == nil {
393+
vm.Status.Stats = &v1alpha2.VirtualMachineStats{}
394+
}
395+
vm.Status.Stats.LastStartTime = running.LastTransitionTime.DeepCopy()
396+
}
397+
398+
func getRunningCondition(vm *v1alpha2.VirtualMachine) *metav1.Condition {
399+
for i := range vm.Status.Conditions {
400+
if vm.Status.Conditions[i].Type == vmcondition.TypeRunning.String() {
401+
return &vm.Status.Conditions[i]
402+
}
403+
}
404+
405+
return nil
406+
}
407+
408+
func getKVVMIRunningPhaseTransitionTimestamp(kvvmi *virtv1.VirtualMachineInstance) *metav1.Time {
409+
if kvvmi == nil {
410+
return nil
411+
}
412+
413+
for i := len(kvvmi.Status.PhaseTransitionTimestamps) - 1; i >= 0; i-- {
414+
transition := kvvmi.Status.PhaseTransitionTimestamps[i]
415+
if transition.Phase == virtv1.Running {
416+
return &transition.PhaseTransitionTimestamp
417+
}
418+
}
419+
420+
return nil
370421
}
371422

372423
func osInfoIsEmpty(info virtv1.VirtualMachineInstanceGuestOSInfo) bool {

images/virtualization-artifact/pkg/controller/vm/internal/statistic_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ package internal
1818

1919
import (
2020
"context"
21+
"time"
2122

2223
. "github.com/onsi/ginkgo/v2"
2324
. "github.com/onsi/gomega"
2425
corev1 "k8s.io/api/core/v1"
2526
"k8s.io/apimachinery/pkg/api/resource"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2628
"k8s.io/apimachinery/pkg/types"
2729
"k8s.io/utils/ptr"
2830
virtv1 "kubevirt.io/api/core/v1"
@@ -34,6 +36,7 @@ import (
3436
"github.com/deckhouse/virtualization-controller/pkg/controller/reconciler"
3537
"github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/state"
3638
"github.com/deckhouse/virtualization/api/core/v1alpha2"
39+
"github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition"
3740
)
3841

3942
var _ = Describe("TestStatisticHandler", func() {
@@ -309,3 +312,112 @@ var _ = Describe("TestStatisticHandler", func() {
309312
),
310313
)
311314
})
315+
316+
var _ = Describe("StatisticHandler", func() {
317+
Describe("syncLastStartTime", func() {
318+
It("sets lastStartTime from the Running condition last transition time", func() {
319+
transitionTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC))
320+
vm := newVMWithRunningCondition(transitionTime)
321+
322+
syncLastStartTime(vm, nil)
323+
324+
Expect(vm.Status.Stats).NotTo(BeNil())
325+
Expect(vm.Status.Stats.LastStartTime).NotTo(BeNil())
326+
Expect(vm.Status.Stats.LastStartTime.Time).To(Equal(transitionTime.Time))
327+
})
328+
329+
It("sets lastStartTime from the VMI Running phase transition if it differs from the Running condition transition by more than ten minutes", func() {
330+
conditionTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 20, 0, 0, time.UTC))
331+
vmiRunningTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC))
332+
vm := newVMWithRunningCondition(conditionTime)
333+
kvvmi := newKVVMIWithRunningPhaseTransition(vmiRunningTime)
334+
335+
syncLastStartTime(vm, kvvmi)
336+
337+
Expect(vm.Status.Stats).NotTo(BeNil())
338+
Expect(vm.Status.Stats.LastStartTime).NotTo(BeNil())
339+
Expect(vm.Status.Stats.LastStartTime.Time).To(Equal(vmiRunningTime.Time))
340+
Expect(vm.Status.Conditions[0].LastTransitionTime.Time).To(Equal(vmiRunningTime.Time))
341+
})
342+
343+
It("sets lastStartTime from the VMI Running phase transition if it is newer than the Running condition transition by more than ten minutes", func() {
344+
conditionTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC))
345+
vmiRunningTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 20, 0, 0, time.UTC))
346+
vm := newVMWithRunningCondition(conditionTime)
347+
kvvmi := newKVVMIWithRunningPhaseTransition(vmiRunningTime)
348+
349+
syncLastStartTime(vm, kvvmi)
350+
351+
Expect(vm.Status.Stats).NotTo(BeNil())
352+
Expect(vm.Status.Stats.LastStartTime).NotTo(BeNil())
353+
Expect(vm.Status.Stats.LastStartTime.Time).To(Equal(vmiRunningTime.Time))
354+
Expect(vm.Status.Conditions[0].LastTransitionTime.Time).To(Equal(vmiRunningTime.Time))
355+
})
356+
357+
It("sets lastStartTime from the Running condition when the VMI Running phase transition does not differ by more than ten minutes", func() {
358+
conditionTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 9, 0, 0, time.UTC))
359+
vmiRunningTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC))
360+
vm := newVMWithRunningCondition(conditionTime)
361+
kvvmi := newKVVMIWithRunningPhaseTransition(vmiRunningTime)
362+
363+
syncLastStartTime(vm, kvvmi)
364+
365+
Expect(vm.Status.Stats).NotTo(BeNil())
366+
Expect(vm.Status.Stats.LastStartTime).NotTo(BeNil())
367+
Expect(vm.Status.Stats.LastStartTime.Time).To(Equal(conditionTime.Time))
368+
Expect(vm.Status.Conditions[0].LastTransitionTime.Time).To(Equal(conditionTime.Time))
369+
})
370+
371+
DescribeTable("clears lastStartTime when the VM is not running",
372+
func(conditions []metav1.Condition) {
373+
lastStartTime := metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC))
374+
vm := &v1alpha2.VirtualMachine{
375+
Status: v1alpha2.VirtualMachineStatus{
376+
Stats: &v1alpha2.VirtualMachineStats{LastStartTime: &lastStartTime},
377+
Conditions: conditions,
378+
},
379+
}
380+
381+
syncLastStartTime(vm, nil)
382+
383+
Expect(vm.Status.Stats).NotTo(BeNil())
384+
Expect(vm.Status.Stats.LastStartTime).To(BeNil())
385+
},
386+
Entry("without the Running condition", nil),
387+
Entry("with the Running condition set to False", []metav1.Condition{
388+
{
389+
Type: vmcondition.TypeRunning.String(),
390+
Status: metav1.ConditionFalse,
391+
LastTransitionTime: metav1.NewTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)),
392+
},
393+
}),
394+
)
395+
})
396+
})
397+
398+
func newVMWithRunningCondition(transitionTime metav1.Time) *v1alpha2.VirtualMachine {
399+
return &v1alpha2.VirtualMachine{
400+
Status: v1alpha2.VirtualMachineStatus{
401+
Conditions: []metav1.Condition{
402+
{
403+
Type: vmcondition.TypeRunning.String(),
404+
Status: metav1.ConditionTrue,
405+
LastTransitionTime: transitionTime,
406+
},
407+
},
408+
},
409+
}
410+
}
411+
412+
func newKVVMIWithRunningPhaseTransition(transitionTime metav1.Time) *virtv1.VirtualMachineInstance {
413+
return &virtv1.VirtualMachineInstance{
414+
Status: virtv1.VirtualMachineInstanceStatus{
415+
PhaseTransitionTimestamps: []virtv1.VirtualMachineInstancePhaseTransitionTimestamp{
416+
{
417+
Phase: virtv1.Running,
418+
PhaseTransitionTimestamp: transitionTime,
419+
},
420+
},
421+
},
422+
}
423+
}

0 commit comments

Comments
 (0)