diff --git a/images/virtualization-artifact/cmd/virtualization-controller/main.go b/images/virtualization-artifact/cmd/virtualization-controller/main.go index f1a685a130..45bbc5efcc 100644 --- a/images/virtualization-artifact/cmd/virtualization-controller/main.go +++ b/images/virtualization-artifact/cmd/virtualization-controller/main.go @@ -355,6 +355,10 @@ func main() { log.Error(err.Error()) os.Exit(1) } + if err = vm.SetupPodGC(mgr, vmLogger, gcSettings.VMPod); err != nil { + log.Error(err.Error()) + os.Exit(1) + } vmbdaLogger := logger.NewControllerLogger(vmbda.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) if _, err = vmbda.NewController(ctx, mgr, virtClient, vmbdaLogger, controllerNamespace); err != nil { diff --git a/images/virtualization-artifact/pkg/config/load_gc_settings.go b/images/virtualization-artifact/pkg/config/load_gc_settings.go index 7cac99823f..c4eaf7b323 100644 --- a/images/virtualization-artifact/pkg/config/load_gc_settings.go +++ b/images/virtualization-artifact/pkg/config/load_gc_settings.go @@ -29,11 +29,14 @@ const ( GcVmopScheduleVar = "GC_VMOP_SCHEDULE" GcVMIMigrationTTLVar = "GC_VMI_MIGRATION_TTL" GcVMIMigrationScheduleVar = "GC_VMI_MIGRATION_SCHEDULE" + GcVMPodTTLVar = "GC_VM_POD_TTL" + GcVMPodScheduleVar = "GC_VM_POD_SCHEDULE" ) type GCSettings struct { VMOP BaseGcSettings VMIMigration BaseGcSettings + VMPod BaseGcSettings } type BaseGcSettings struct { @@ -55,6 +58,12 @@ func LoadGcSettings() (GCSettings, error) { } gcSettings.VMIMigration = base + base, err = GetBaseGCSettingsFromEnv(GcVMPodScheduleVar, GcVMPodTTLVar) + if err != nil { + return gcSettings, err + } + gcSettings.VMPod = base + return gcSettings, nil } diff --git a/images/virtualization-artifact/pkg/controller/vm/pod_gc.go b/images/virtualization-artifact/pkg/controller/vm/pod_gc.go new file mode 100644 index 0000000000..9e86e2fc05 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vm/pod_gc.go @@ -0,0 +1,108 @@ +/* +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 vm + +import ( + "context" + "time" + + corev1 "k8s.io/api/core/v1" + virtv1 "kubevirt.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/manager" + + "github.com/deckhouse/deckhouse/pkg/log" + "github.com/deckhouse/virtualization-controller/pkg/config" + "github.com/deckhouse/virtualization-controller/pkg/controller/gc" +) + +const ( + gcPodControllerName = "vm-pod-gc-controller" + defaultPodGCTTL = 24 * time.Hour + defaultPodGCMaxCount = 2 +) + +func SetupPodGC(mgr manager.Manager, log *log.Logger, gcSettings config.BaseGcSettings) error { + podGCMgr := newPodGCManager(mgr.GetClient(), gcSettings.TTL.Duration, defaultPodGCMaxCount) + + return gc.SetupGcController(gcPodControllerName, + mgr, + log.With("resource", "vm-pod"), + gcSettings.Schedule, + podGCMgr, + ) +} + +func newPodGCManager(client client.Client, ttl time.Duration, max int) *podGCManager { + if ttl == 0 { + ttl = defaultPodGCTTL + } + if max == 0 { + max = defaultPodGCMaxCount + } + return &podGCManager{ + client: client, + ttl: ttl, + max: max, + } +} + +var _ gc.ReconcileGCManager = &podGCManager{} + +type podGCManager struct { + client client.Client + ttl time.Duration + max int +} + +func (m *podGCManager) New() client.Object { + return &corev1.Pod{} +} + +func (m *podGCManager) ShouldBeDeleted(obj client.Object) bool { + pod, ok := obj.(*corev1.Pod) + if !ok { + return false + } + if _, hasLabel := pod.Labels[virtv1.VirtualMachineNameLabel]; !hasLabel { + return false + } + return pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed +} + +func (m *podGCManager) ListForDelete(ctx context.Context, now time.Time) ([]client.Object, error) { + podList := &corev1.PodList{} + err := m.client.List(ctx, podList, client.HasLabels{virtv1.VirtualMachineNameLabel}) + if err != nil { + return nil, err + } + + objs := make([]client.Object, 0, len(podList.Items)) + for _, pod := range podList.Items { + objs = append(objs, &pod) + } + + return gc.DefaultFilter(objs, m.ShouldBeDeleted, m.ttl, m.getIndex, m.max, now), nil +} + +func (m *podGCManager) getIndex(obj client.Object) string { + pod, ok := obj.(*corev1.Pod) + if !ok { + return "" + } + return pod.Namespace + "/" + pod.Labels[virtv1.VirtualMachineNameLabel] +} diff --git a/templates/virtualization-controller/_helpers.tpl b/templates/virtualization-controller/_helpers.tpl index f88f153591..0f6ac0011f 100644 --- a/templates/virtualization-controller/_helpers.tpl +++ b/templates/virtualization-controller/_helpers.tpl @@ -84,6 +84,10 @@ true value: "24h" - name: GC_VMI_MIGRATION_SCHEDULE value: "0 0 * * *" +- name: GC_VM_POD_TTL + value: "24h" +- name: GC_VM_POD_SCHEDULE + value: "0 0 * * *" {{- if (hasKey .Values.virtualization.internal.moduleConfig "liveMigration") }} - name: LIVE_MIGRATION_BANDWIDTH_PER_NODE value: {{ .Values.virtualization.internal.moduleConfig.liveMigration.bandwidthPerNode | quote }}