diff --git a/images/virtualization-artifact/pkg/controller/indexer/event_indexer.go b/images/virtualization-artifact/pkg/controller/indexer/event_indexer.go new file mode 100644 index 0000000000..26a4e7a574 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/indexer/event_indexer.go @@ -0,0 +1,42 @@ +/* +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 indexer + +import ( + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func IndexEventByInvolvedObjectName() (obj client.Object, field string, extractValue client.IndexerFunc) { + return &corev1.Event{}, IndexFieldEventByInvolvedObjectName, func(object client.Object) []string { + event, ok := object.(*corev1.Event) + if !ok || event == nil { + return nil + } + return []string{event.InvolvedObject.Name} + } +} + +func IndexEventByInvolvedObjectKind() (obj client.Object, field string, extractValue client.IndexerFunc) { + return &corev1.Event{}, IndexFieldEventByInvolvedObjectKind, func(object client.Object) []string { + event, ok := object.(*corev1.Event) + if !ok || event == nil { + return nil + } + return []string{event.InvolvedObject.Kind} + } +} diff --git a/images/virtualization-artifact/pkg/controller/indexer/indexer.go b/images/virtualization-artifact/pkg/controller/indexer/indexer.go index 5f01c64b7e..beaac374df 100644 --- a/images/virtualization-artifact/pkg/controller/indexer/indexer.go +++ b/images/virtualization-artifact/pkg/controller/indexer/indexer.go @@ -65,6 +65,9 @@ const ( IndexFieldVMIPLeaseByVMIP = "spec.virtualMachineIPAddressRef" IndexFieldVMByProvisioningSecret = "spec.provisioning.secretRef" + + IndexFieldEventByInvolvedObjectName = "involvedObject.name" + IndexFieldEventByInvolvedObjectKind = "involvedObject.kind" ) var IndexGetters = []IndexGetter{ @@ -91,6 +94,8 @@ var IndexGetters = []IndexGetter{ IndexVMMACByAddress, IndexVMMACLeaseByVMMAC, IndexVMIPLeaseByVMIP, + IndexEventByInvolvedObjectName, + IndexEventByInvolvedObjectKind, } type IndexGetter func() (obj client.Object, field string, extractValue client.IndexerFunc) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go index a10ea2aff9..347068a476 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go @@ -23,6 +23,8 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" virtv1 "kubevirt.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -31,6 +33,7 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" "github.com/deckhouse/virtualization-controller/pkg/controller/service" "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/state" + "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/watcher" "github.com/deckhouse/virtualization-controller/pkg/eventrecord" "github.com/deckhouse/virtualization-controller/pkg/logger" "github.com/deckhouse/virtualization/api/core/v1alpha2" @@ -101,7 +104,7 @@ func (h *LifeCycleHandler) Handle(ctx context.Context, s state.VirtualMachineSta log := logger.FromContext(ctx).With(logger.SlogHandler(nameLifeCycleHandler)) - h.syncRunning(changed, kvvm, kvvmi, pod, log) + h.syncRunning(ctx, changed, kvvm, kvvmi, pod, log) return reconcile.Result{}, nil } @@ -109,7 +112,7 @@ func (h *LifeCycleHandler) Name() string { return nameLifeCycleHandler } -func (h *LifeCycleHandler) syncRunning(vm *v1alpha2.VirtualMachine, kvvm *virtv1.VirtualMachine, kvvmi *virtv1.VirtualMachineInstance, pod *corev1.Pod, log *slog.Logger) { +func (h *LifeCycleHandler) syncRunning(ctx context.Context, vm *v1alpha2.VirtualMachine, kvvm *virtv1.VirtualMachine, kvvmi *virtv1.VirtualMachineInstance, pod *corev1.Pod, log *slog.Logger) { cb := conditions.NewConditionBuilder(vmcondition.TypeRunning).Generation(vm.GetGeneration()) if pod != nil && pod.Status.Message != "" { @@ -120,6 +123,14 @@ func (h *LifeCycleHandler) syncRunning(vm *v1alpha2.VirtualMachine, kvvm *virtv1 return } + if volumeError := h.checkPodVolumeErrors(ctx, vm, log); volumeError != nil { + cb.Status(metav1.ConditionFalse). + Reason(vmcondition.ReasonPodNotStarted). + Message(volumeError.Error()) + conditions.SetCondition(cb, &vm.Status.Conditions) + return + } + if kvvm != nil { podScheduled := service.GetKVVMCondition(string(corev1.PodScheduled), kvvm.Status.Conditions) if podScheduled != nil && podScheduled.Status == corev1.ConditionFalse { @@ -202,3 +213,64 @@ func (h *LifeCycleHandler) syncRunning(vm *v1alpha2.VirtualMachine, kvvm *virtv1 cb.Reason(vmcondition.ReasonVirtualMachineNotRunning).Status(metav1.ConditionFalse) conditions.SetCondition(cb, &vm.Status.Conditions) } + +func (h *LifeCycleHandler) checkPodVolumeErrors(ctx context.Context, vm *v1alpha2.VirtualMachine, log *slog.Logger) error { + var podList corev1.PodList + err := h.client.List(ctx, &podList, &client.ListOptions{ + Namespace: vm.Namespace, + LabelSelector: labels.SelectorFromSet(map[string]string{ + virtv1.VirtualMachineNameLabel: vm.Name, + }), + }) + if err != nil { + log.Error("Failed to list pods", "error", err) + return nil + } + + for i := range podList.Items { + if volumeErr := h.getPodVolumeError(ctx, &podList.Items[i], log); volumeErr != nil { + return volumeErr + } + } + + return nil +} + +func isContainerCreating(pod *corev1.Pod) bool { + if pod.Status.Phase != corev1.PodPending { + return false + } + for _, cs := range pod.Status.ContainerStatuses { + if cs.State.Waiting != nil && cs.State.Waiting.Reason == "ContainerCreating" { + return true + } + } + return false +} + +func (h *LifeCycleHandler) getPodVolumeError(ctx context.Context, pod *corev1.Pod, log *slog.Logger) error { + if !isContainerCreating(pod) { + return nil + } + + eventList := &corev1.EventList{} + err := h.client.List(ctx, eventList, &client.ListOptions{ + Namespace: pod.Namespace, + FieldSelector: fields.SelectorFromSet(fields.Set{ + "involvedObject.name": pod.Name, + "involvedObject.kind": "Pod", + }), + }) + if err != nil { + log.Error("Failed to list pod events", "error", err) + return nil + } + + for _, e := range eventList.Items { + if e.Type == corev1.EventTypeWarning && (e.Reason == watcher.ReasonFailedAttachVolume || e.Reason == watcher.ReasonFailedMount) { + return fmt.Errorf("%s: %s", e.Reason, e.Message) + } + } + + return nil +} diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/network.go b/images/virtualization-artifact/pkg/controller/vm/internal/network.go index 51da70ea0c..b61f990c99 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/network.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/network.go @@ -185,7 +185,11 @@ func extractNetworkStatusFromPods(pods *corev1.PodList) (string, error) { networkStatusAnnotation, found := pod.Annotations[annotations.AnnNetworksStatus] if !found { - errorMessages = append(errorMessages, "Cannot determine the status of additional interfaces, waiting for a response from the SDN module") + if pod.Status.Phase == corev1.PodRunning { + errorMessages = append(errorMessages, "Cannot determine the status of additional interfaces, waiting for a response from the SDN module") + } else { + errorMessages = append(errorMessages, "Waiting for virt-launcher pod to start") + } continue } diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/watcher/volumeevent_watcher.go b/images/virtualization-artifact/pkg/controller/vm/internal/watcher/volumeevent_watcher.go new file mode 100644 index 0000000000..de57362f09 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vm/internal/watcher/volumeevent_watcher.go @@ -0,0 +1,104 @@ +/* +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 watcher + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + virtv1 "kubevirt.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +const ( + ReasonFailedAttachVolume = "FailedAttachVolume" + ReasonFailedMount = "FailedMount" +) + +func NewVolumeEventWatcher(client client.Client) *VolumeEventWatcher { + return &VolumeEventWatcher{ + client: client, + } +} + +type VolumeEventWatcher struct { + client client.Client +} + +func (w *VolumeEventWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + if err := ctr.Watch( + source.Kind( + mgr.GetCache(), + &corev1.Event{}, + handler.TypedEnqueueRequestsFromMapFunc(func(ctx context.Context, e *corev1.Event) []reconcile.Request { + if e.InvolvedObject.Kind != "Pod" { + return nil + } + + if e.Reason != ReasonFailedAttachVolume && e.Reason != ReasonFailedMount { + return nil + } + + pod := &corev1.Pod{} + if err := w.client.Get(ctx, types.NamespacedName{ + Name: e.InvolvedObject.Name, + Namespace: e.InvolvedObject.Namespace, + }, pod); err != nil { + return nil + } + + vmName, hasLabel := pod.GetLabels()[virtv1.VirtualMachineNameLabel] + if !hasLabel { + return nil + } + + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: vmName, + Namespace: pod.GetNamespace(), + }, + }, + } + }), + predicate.TypedFuncs[*corev1.Event]{ + CreateFunc: func(e event.TypedCreateEvent[*corev1.Event]) bool { + return e.Object.Type == corev1.EventTypeWarning && + (e.Object.Reason == ReasonFailedAttachVolume || e.Object.Reason == ReasonFailedMount) + }, + UpdateFunc: func(e event.TypedUpdateEvent[*corev1.Event]) bool { + return false + }, + DeleteFunc: func(e event.TypedDeleteEvent[*corev1.Event]) bool { + return false + }, + }, + ), + ); err != nil { + return fmt.Errorf("error setting watch on Event: %w", err) + } + return nil +} diff --git a/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go b/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go index ba45da80bd..7f10b3116f 100644 --- a/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go @@ -65,6 +65,7 @@ func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr watcher.NewKVVMWatcher(), watcher.NewKVVMIWatcher(), watcher.NewPodWatcher(), + watcher.NewVolumeEventWatcher(mgr.GetClient()), watcher.NewVirtualImageWatcher(mgr.GetClient()), watcher.NewClusterVirtualImageWatcher(mgr.GetClient()), watcher.NewVirtualDiskWatcher(mgr.GetClient()), diff --git a/templates/virtualization-controller/rbac-for-us.yaml b/templates/virtualization-controller/rbac-for-us.yaml index 73acbabd5a..cb742ce14b 100644 --- a/templates/virtualization-controller/rbac-for-us.yaml +++ b/templates/virtualization-controller/rbac-for-us.yaml @@ -101,6 +101,8 @@ rules: verbs: - create - patch + - list + - watch - apiGroups: - cdi.internal.virtualization.deckhouse.io resources: