diff --git a/images/virtualization-artifact/cmd/virtualization-controller/main.go b/images/virtualization-artifact/cmd/virtualization-controller/main.go index f4b0f9d26a..d993399ab4 100644 --- a/images/virtualization-artifact/cmd/virtualization-controller/main.go +++ b/images/virtualization-artifact/cmd/virtualization-controller/main.go @@ -46,10 +46,10 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/controller/livemigration" mc "github.com/deckhouse/virtualization-controller/pkg/controller/moduleconfig" mcapi "github.com/deckhouse/virtualization-controller/pkg/controller/moduleconfig/api" - "github.com/deckhouse/virtualization-controller/pkg/controller/vd" + "github.com/deckhouse/virtualization-controller/pkg/controller/supervd" + "github.com/deckhouse/virtualization-controller/pkg/controller/supervm" "github.com/deckhouse/virtualization-controller/pkg/controller/vdsnapshot" "github.com/deckhouse/virtualization-controller/pkg/controller/vi" - "github.com/deckhouse/virtualization-controller/pkg/controller/vm" "github.com/deckhouse/virtualization-controller/pkg/controller/vmbda" "github.com/deckhouse/virtualization-controller/pkg/controller/vmclass" "github.com/deckhouse/virtualization-controller/pkg/controller/vmip" @@ -313,77 +313,79 @@ func main() { os.Exit(1) } - cviLogger := logger.NewControllerLogger(cvi.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) + logFactory := logger.NewFactory(logLevel, logOutput, logDebugVerbosity, logDebugControllerList) + + cviLogger := logFactory(cvi.ControllerName) if _, err = cvi.NewController(ctx, mgr, cviLogger, importSettings.ImporterImage, importSettings.UploaderImage, importSettings.Requirements, dvcrSettings, controllerNamespace); err != nil { log.Error(err.Error()) os.Exit(1) } - vdLogger := logger.NewControllerLogger(vd.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) - if _, err = vd.NewController(ctx, mgr, vdLogger, importSettings.ImporterImage, importSettings.UploaderImage, importSettings.Requirements, dvcrSettings, vdStorageClassSettings); err != nil { + viLogger := logFactory(vi.ControllerName) + if _, err = vi.NewController(ctx, mgr, viLogger, importSettings.ImporterImage, importSettings.UploaderImage, importSettings.BounderImage, importSettings.Requirements, dvcrSettings, viStorageClassSettings); err != nil { log.Error(err.Error()) os.Exit(1) } - viLogger := logger.NewControllerLogger(vi.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) - if _, err = vi.NewController(ctx, mgr, viLogger, importSettings.ImporterImage, importSettings.UploaderImage, importSettings.BounderImage, importSettings.Requirements, dvcrSettings, viStorageClassSettings); err != nil { + supervdLogger := logFactory(supervd.ControllerName) + if _, err = supervd.NewController(ctx, mgr, importSettings.ImporterImage, importSettings.UploaderImage, importSettings.Requirements, dvcrSettings, vdStorageClassSettings, logFactory, supervdLogger); err != nil { log.Error(err.Error()) os.Exit(1) } - vmLogger := logger.NewControllerLogger(vm.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) - if err = vm.SetupController(ctx, mgr, vmLogger, dvcrSettings, firmwareImage); err != nil { + supervmLogger := logFactory(supervm.ControllerName) + if err = supervm.SetupController(ctx, mgr, dvcrSettings, firmwareImage, logFactory, supervmLogger); err != nil { log.Error(err.Error()) os.Exit(1) } - if err = vm.SetupGC(mgr, vmLogger, gcSettings.VMIMigration); err != nil { + if err = supervm.SetupGC(mgr, supervmLogger, gcSettings.VMIMigration); err != nil { log.Error(err.Error()) os.Exit(1) } - vmbdaLogger := logger.NewControllerLogger(vmbda.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) + vmbdaLogger := logFactory(vmbda.ControllerName) if _, err = vmbda.NewController(ctx, mgr, virtClient, vmbdaLogger, controllerNamespace); err != nil { log.Error(err.Error()) os.Exit(1) } - vmipLogger := logger.NewControllerLogger(vmip.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) + vmipLogger := logFactory(vmip.ControllerName) if _, err = vmip.NewController(ctx, mgr, virtClient, vmipLogger, virtualMachineCIDRs); err != nil { log.Error(err.Error()) os.Exit(1) } - vmipleaseLogger := logger.NewControllerLogger(vmiplease.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) + vmipleaseLogger := logFactory(vmiplease.ControllerName) if _, err = vmiplease.NewController(ctx, mgr, vmipleaseLogger, virtualMachineIPLeasesRetentionDuration); err != nil { log.Error(err.Error()) os.Exit(1) } - vmclassLogger := logger.NewControllerLogger(vmclass.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) + vmclassLogger := logFactory(vmclass.ControllerName) if _, err = vmclass.NewController(ctx, mgr, controllerNamespace, vmclassLogger); err != nil { log.Error(err.Error()) os.Exit(1) } - vdsnapshotLogger := logger.NewControllerLogger(vdsnapshot.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) + vdsnapshotLogger := logFactory(vdsnapshot.ControllerName) if _, err = vdsnapshot.NewController(ctx, mgr, vdsnapshotLogger, virtClient); err != nil { log.Error(err.Error()) os.Exit(1) } - vmsnapshotLogger := logger.NewControllerLogger(vmsnapshot.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) + vmsnapshotLogger := logFactory(vmsnapshot.ControllerName) if err = vmsnapshot.NewController(ctx, mgr, vmsnapshotLogger, virtClient); err != nil { log.Error(err.Error()) os.Exit(1) } - vmrestoreLogger := logger.NewControllerLogger(vmrestore.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) + vmrestoreLogger := logFactory(vmrestore.ControllerName) if err = vmrestore.NewController(ctx, mgr, vmrestoreLogger); err != nil { log.Error(err.Error()) os.Exit(1) } - vmopLogger := logger.NewControllerLogger(vmop.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) + vmopLogger := logFactory(vmop.ControllerName) if err = vmop.SetupController(ctx, mgr, vmopLogger); err != nil { log.Error(err.Error()) os.Exit(1) @@ -393,7 +395,7 @@ func main() { os.Exit(1) } - liveMigrationLogger := logger.NewControllerLogger(livemigration.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) + liveMigrationLogger := logFactory(livemigration.ControllerName) if err = livemigration.SetupController(ctx, mgr, liveMigrationLogger); err != nil { log.Error(err.Error()) os.Exit(1) @@ -404,31 +406,31 @@ func main() { os.Exit(1) } - workloadUpdaterLogger := logger.NewControllerLogger(workloadupdater.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) + workloadUpdaterLogger := logFactory(workloadupdater.ControllerName) if err = workloadupdater.SetupController(ctx, mgr, workloadUpdaterLogger, firmwareImage, controllerNamespace, virtControllerName); err != nil { log.Error(err.Error()) os.Exit(1) } - evacuationLogger := logger.NewControllerLogger(evacuation.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) + evacuationLogger := logFactory(evacuation.ControllerName) if err = evacuation.SetupController(ctx, mgr, virtClient, evacuationLogger); err != nil { log.Error(err.Error()) os.Exit(1) } - volumeMigrationLogger := logger.NewControllerLogger(volumemigration.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) + volumeMigrationLogger := logFactory(volumemigration.ControllerName) if err = volumemigration.SetupController(ctx, mgr, volumeMigrationLogger); err != nil { log.Error(err.Error()) os.Exit(1) } - vmmacLogger := logger.NewControllerLogger(vmmac.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) + vmmacLogger := logFactory(vmmac.ControllerName) if _, err = vmmac.NewController(ctx, mgr, vmmacLogger, clusterUUID, virtClient); err != nil { log.Error(err.Error()) os.Exit(1) } - vmmacleaseLogger := logger.NewControllerLogger(vmmaclease.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) + vmmacleaseLogger := logFactory(vmmaclease.ControllerName) if _, err = vmmaclease.NewController(ctx, mgr, vmmacleaseLogger); err != nil { log.Error(err.Error()) os.Exit(1) diff --git a/images/virtualization-artifact/pkg/controller/netmanager/ipam.go b/images/virtualization-artifact/pkg/controller/netmanager/ipam.go index 5f2e4a5cc9..04ed55ed61 100644 --- a/images/virtualization-artifact/pkg/controller/netmanager/ipam.go +++ b/images/virtualization-artifact/pkg/controller/netmanager/ipam.go @@ -48,12 +48,14 @@ func (m IPAM) IsBound(vmName string, vmip *v1alpha2.VirtualMachineIPAddress) boo return false } - attachedCondition, _ := conditions.GetCondition(vmipcondition.AttachedType, vmip.Status.Conditions) - if attachedCondition.Status != metav1.ConditionTrue || !conditions.IsLastUpdated(attachedCondition, vmip) { - return false - } - - return vmip.Status.VirtualMachine == vmName + // TODO: temporary solution to test performance. + // attachedCondition, _ := conditions.GetCondition(vmipcondition.AttachedType, vmip.Status.Conditions) + // if attachedCondition.Status != metav1.ConditionTrue || !conditions.IsLastUpdated(attachedCondition, vmip) { + // return false + // } + + // return vmip.Status.VirtualMachine == vmName + return true } func (m IPAM) CheckIPAddressAvailableForBinding(vmName string, vmip *v1alpha2.VirtualMachineIPAddress) error { diff --git a/images/virtualization-artifact/pkg/controller/supervd/internal/semaphore.go b/images/virtualization-artifact/pkg/controller/supervd/internal/semaphore.go new file mode 100644 index 0000000000..16cdf3a93d --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/supervd/internal/semaphore.go @@ -0,0 +1,106 @@ +/* +Copyright 2024 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 ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/config" + vdcontroller "github.com/deckhouse/virtualization-controller/pkg/controller/vd" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + "github.com/deckhouse/virtualization-controller/pkg/logger" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type SemaphoreHandler struct { + mgr manager.Manager + logFactory logger.Factory + importerImage string + uploaderImage string + requirements corev1.ResourceRequirements + dvcr *dvcr.Settings + storageClassSettings config.VirtualDiskStorageClassSettings + + controllers map[string]chan reconcile.Request +} + +func NewSemaphoreHandler( + mgr manager.Manager, + logFactory logger.Factory, + importerImage string, + uploaderImage string, + requirements corev1.ResourceRequirements, + dvcr *dvcr.Settings, + storageClassSettings config.VirtualDiskStorageClassSettings, +) *SemaphoreHandler { + return &SemaphoreHandler{ + mgr: mgr, + logFactory: logFactory, + importerImage: importerImage, + uploaderImage: uploaderImage, + requirements: requirements, + dvcr: dvcr, + storageClassSettings: storageClassSettings, + controllers: make(map[string]chan reconcile.Request), + } +} + +func (h *SemaphoreHandler) Handle(ctx context.Context, vd *v1alpha2.VirtualDisk) (reconcile.Result, error) { + logger.FromContext(ctx).Warn("[test][SUPER] STARTED") + + queue, ok := h.controllers[vd.Namespace] + if !ok { + logger.FromContext(ctx).Warn("[test][SUPER] START NEW NAMESPACED CONTROLLER") + + queue = make(chan reconcile.Request) + + _, err := vdcontroller.NewController( + ctx, + h.mgr, + h.logFactory(vdcontroller.ControllerName+"-"+vd.Namespace), + h.importerImage, + h.uploaderImage, + h.requirements, + h.dvcr, + h.storageClassSettings, + vd.Namespace, + queue, + ) + if err != nil { + return reconcile.Result{}, fmt.Errorf("setup vm controller for %q: %w", vd.Namespace, err) + } + + h.controllers[vd.Namespace] = queue + } + + logger.FromContext(ctx).Warn("[test][SUPER] PUSH REQUEST TO QUEUE") + + queue <- reconcile.Request{NamespacedName: types.NamespacedName{ + Name: vd.Name, + Namespace: vd.Namespace, + }} + + logger.FromContext(ctx).Warn("[test][SUPER] FINISHED") + + return reconcile.Result{}, nil +} diff --git a/images/virtualization-artifact/pkg/controller/supervd/internal/service/vd_storage_class_service.go b/images/virtualization-artifact/pkg/controller/supervd/internal/service/vd_storage_class_service.go new file mode 100644 index 0000000000..7a5aaf2603 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/supervd/internal/service/vd_storage_class_service.go @@ -0,0 +1,117 @@ +/* +Copyright 2024 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 service + +import ( + "context" + "errors" + "slices" + + corev1 "k8s.io/api/core/v1" + storagev1 "k8s.io/api/storage/v1" + + "github.com/deckhouse/virtualization-controller/pkg/config" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" +) + +var ( + ErrStorageClassNotFound = errors.New("storage class not found") + ErrStorageClassNotAllowed = errors.New("storage class not allowed") +) + +type VirtualDiskStorageClassService struct { + *service.BaseStorageClassService + + storageClassSettings config.VirtualDiskStorageClassSettings +} + +func NewVirtualDiskStorageClassService(svc *service.BaseStorageClassService, settings config.VirtualDiskStorageClassSettings) *VirtualDiskStorageClassService { + return &VirtualDiskStorageClassService{ + BaseStorageClassService: svc, + storageClassSettings: settings, + } +} + +// GetValidatedStorageClass determines the storage class for VD from global settings and resource spec. +// +// Global settings contain a default storage class and an array of allowed storageClasses from the ModuleConfig. +// Storage class is allowed if contained in the "allowed" array. +// +// Storage class from the spec has the most priority with fallback to global settings: +// 1. Return `storageClassName` if specified in the resource spec and allowed by global settings. +// 2. Return global `defaultStorageClass` if specified. +// 3. Return cluster-wide default storage class if specified and allowed. +// +// Errors: +// 1. Return error if no storage class is specified. +// 2. Return error if specified non-empty class is not allowed. +func (svc *VirtualDiskStorageClassService) GetValidatedStorageClass(storageClassFromSpec *string, clusterDefaultStorageClass *storagev1.StorageClass) (*string, error) { + if svc.storageClassSettings.DefaultStorageClassName == "" && len(svc.storageClassSettings.AllowedStorageClassNames) == 0 { + return storageClassFromSpec, nil + } + + if storageClassFromSpec != nil && *storageClassFromSpec != "" { + if slices.Contains(svc.storageClassSettings.AllowedStorageClassNames, *storageClassFromSpec) { + return storageClassFromSpec, nil + } + + if svc.storageClassSettings.DefaultStorageClassName != "" && svc.storageClassSettings.DefaultStorageClassName == *storageClassFromSpec { + return storageClassFromSpec, nil + } + + return nil, ErrStorageClassNotAllowed + } + + if svc.storageClassSettings.DefaultStorageClassName != "" { + return &svc.storageClassSettings.DefaultStorageClassName, nil + } + + if clusterDefaultStorageClass != nil && clusterDefaultStorageClass.Name != "" { + if slices.Contains(svc.storageClassSettings.AllowedStorageClassNames, clusterDefaultStorageClass.Name) { + return &clusterDefaultStorageClass.Name, nil + } + + return nil, ErrStorageClassNotAllowed + } + + return nil, ErrStorageClassNotFound +} + +func (svc *VirtualDiskStorageClassService) IsStorageClassAllowed(scName string) bool { + if svc.storageClassSettings.DefaultStorageClassName == "" && len(svc.storageClassSettings.AllowedStorageClassNames) == 0 { + return true + } + + if slices.Contains(svc.storageClassSettings.AllowedStorageClassNames, scName) { + return true + } + + if svc.storageClassSettings.DefaultStorageClassName != "" && svc.storageClassSettings.DefaultStorageClassName == scName { + return true + } + + return false +} + +func (svc *VirtualDiskStorageClassService) GetModuleStorageClass(ctx context.Context) (*storagev1.StorageClass, error) { + return svc.GetStorageClass(ctx, svc.storageClassSettings.DefaultStorageClassName) +} + +func (svc *VirtualDiskStorageClassService) GetPersistentVolumeClaim(ctx context.Context, sup supplements.Generator) (*corev1.PersistentVolumeClaim, error) { + return svc.BaseStorageClassService.GetPersistentVolumeClaim(ctx, sup) +} diff --git a/images/virtualization-artifact/pkg/controller/supervd/internal/service/vd_storage_class_service_test.go b/images/virtualization-artifact/pkg/controller/supervd/internal/service/vd_storage_class_service_test.go new file mode 100644 index 0000000000..2686c62c1c --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/supervd/internal/service/vd_storage_class_service_test.go @@ -0,0 +1,167 @@ +/* +Copyright 2024 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 service + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + storagev1 "k8s.io/api/storage/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + "github.com/deckhouse/virtualization-controller/pkg/config" +) + +var _ = Describe("VirtualDiskStorageClassService", func() { + var ( + service *VirtualDiskStorageClassService + storageClassSettings config.VirtualDiskStorageClassSettings + clusterDefaultStorageClass *storagev1.StorageClass + ) + + BeforeEach(func() { + clusterDefaultStorageClass = &storagev1.StorageClass{ObjectMeta: metav1.ObjectMeta{Name: "default-cluster-storage"}} + }) + + Context("when settings are empty", func() { + It("returns the storageClassFromSpec", func() { + storageClassSettings = config.VirtualDiskStorageClassSettings{} + service = NewVirtualDiskStorageClassService(nil, storageClassSettings) + sc := ptr.To("requested-storage-class") + storageClass, err := service.GetValidatedStorageClass(sc, clusterDefaultStorageClass) + + Expect(err).To(BeNil()) + Expect(storageClass).To(Equal(sc)) + }) + }) + + Context("when settings are empty and storageClassFromSpec is empty", func() { + It("returns the storageClassFromSpec", func() { + storageClassSettings = config.VirtualDiskStorageClassSettings{} + service = NewVirtualDiskStorageClassService(nil, storageClassSettings) + + storageClass, err := service.GetValidatedStorageClass(nil, clusterDefaultStorageClass) + + Expect(err).To(BeNil()) + Expect(storageClass).To(BeNil()) + }) + }) + + Context("when settings and clusterDefaultStorageClass are empty", func() { + It("returns the storageClassFromSpec", func() { + storageClassSettings = config.VirtualDiskStorageClassSettings{} + service = NewVirtualDiskStorageClassService(nil, storageClassSettings) + sc := ptr.To("requested-storage-class") + storageClass, err := service.GetValidatedStorageClass(sc, nil) + + Expect(err).To(BeNil()) + Expect(storageClass).To(Equal(sc)) + }) + }) + + Context("when AllowedStorageClassNames exist, but DefaultStorageClassName is empty", func() { + BeforeEach(func() { + storageClassSettings = config.VirtualDiskStorageClassSettings{ + AllowedStorageClassNames: []string{"allowed-storage-class"}, + DefaultStorageClassName: "", + } + service = NewVirtualDiskStorageClassService(nil, storageClassSettings) + }) + + It("returns the requested storage class if it's in the allowed list", func() { + sc := ptr.To("allowed-storage-class") + storageClass, err := service.GetValidatedStorageClass(sc, clusterDefaultStorageClass) + + Expect(err).To(BeNil()) + Expect(storageClass).To(Equal(sc)) + }) + + It("returns an error if the requested storage class is not in the allowed list", func() { + _, err := service.GetValidatedStorageClass(ptr.To("not-allowed-storage-class"), clusterDefaultStorageClass) + + Expect(err).To(Equal(ErrStorageClassNotAllowed)) + }) + + It("returns an error if storageClassFromSpec is empty", func() { + _, err := service.GetValidatedStorageClass(nil, clusterDefaultStorageClass) + + Expect(err).To(Equal(ErrStorageClassNotAllowed)) + }) + }) + + Context("when AllowedStorageClassNames is empty, but DefaultStorageClassName exist", func() { + BeforeEach(func() { + storageClassSettings = config.VirtualDiskStorageClassSettings{ + AllowedStorageClassNames: []string{}, + DefaultStorageClassName: "default-storage-class", + } + service = NewVirtualDiskStorageClassService(nil, storageClassSettings) + }) + + It("returns the default storage class if storageClassFromSpec is empty", func() { + storageClass, err := service.GetValidatedStorageClass(nil, clusterDefaultStorageClass) + + Expect(err).To(BeNil()) + Expect(storageClass).To(Equal(ptr.To("default-storage-class"))) + }) + + It("returns the requested storage class if it matches the default storage class", func() { + sc := ptr.To("default-storage-class") + storageClass, err := service.GetValidatedStorageClass(sc, clusterDefaultStorageClass) + + Expect(err).To(BeNil()) + Expect(storageClass).To(Equal(sc)) + }) + + It("returns an error if the requested storage class does not match the default", func() { + _, err := service.GetValidatedStorageClass(ptr.To("different-storage-class"), clusterDefaultStorageClass) + + Expect(err).To(Equal(ErrStorageClassNotAllowed)) + }) + }) + + Context("when both AllowedStorageClassNames and DefaultStorageClassName exist", func() { + BeforeEach(func() { + storageClassSettings = config.VirtualDiskStorageClassSettings{ + AllowedStorageClassNames: []string{"allowed-storage-class"}, + DefaultStorageClassName: "default-storage-class", + } + service = NewVirtualDiskStorageClassService(nil, storageClassSettings) + }) + + It("returns the default storage class if storageClassFromSpec is empty", func() { + storageClass, err := service.GetValidatedStorageClass(nil, clusterDefaultStorageClass) + + Expect(err).To(BeNil()) + Expect(storageClass).To(Equal(ptr.To("default-storage-class"))) + }) + + It("returns the requested storage class if it's in the allowed list", func() { + sc := ptr.To("allowed-storage-class") + storageClass, err := service.GetValidatedStorageClass(sc, clusterDefaultStorageClass) + + Expect(err).To(BeNil()) + Expect(storageClass).To(Equal(sc)) + }) + + It("returns an error if the requested storage class is not in the allowed list", func() { + _, err := service.GetValidatedStorageClass(ptr.To("not-allowed-storage-class"), clusterDefaultStorageClass) + + Expect(err).To(Equal(ErrStorageClassNotAllowed)) + }) + }) +}) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/validator/name_validator.go b/images/virtualization-artifact/pkg/controller/supervd/internal/validator/name_validator.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vd/internal/validator/name_validator.go rename to images/virtualization-artifact/pkg/controller/supervd/internal/validator/name_validator.go diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/validator/pvc_size_validator.go b/images/virtualization-artifact/pkg/controller/supervd/internal/validator/pvc_size_validator.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vd/internal/validator/pvc_size_validator.go rename to images/virtualization-artifact/pkg/controller/supervd/internal/validator/pvc_size_validator.go diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/validator/spec_changes_validator.go b/images/virtualization-artifact/pkg/controller/supervd/internal/validator/spec_changes_validator.go similarity index 98% rename from images/virtualization-artifact/pkg/controller/vd/internal/validator/spec_changes_validator.go rename to images/virtualization-artifact/pkg/controller/supervd/internal/validator/spec_changes_validator.go index d500237c3e..fbe07dd3ad 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/validator/spec_changes_validator.go +++ b/images/virtualization-artifact/pkg/controller/supervd/internal/validator/spec_changes_validator.go @@ -25,7 +25,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" - intsvc "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/service" + intsvc "github.com/deckhouse/virtualization-controller/pkg/controller/supervd/internal/service" "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" ) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/validator/storage_class_validator.go b/images/virtualization-artifact/pkg/controller/supervd/internal/validator/storage_class_validator.go similarity index 99% rename from images/virtualization-artifact/pkg/controller/vd/internal/validator/storage_class_validator.go rename to images/virtualization-artifact/pkg/controller/supervd/internal/validator/storage_class_validator.go index b67c75b9ee..ecbdef07cc 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/validator/storage_class_validator.go +++ b/images/virtualization-artifact/pkg/controller/supervd/internal/validator/storage_class_validator.go @@ -27,7 +27,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" commonvd "github.com/deckhouse/virtualization-controller/pkg/common/vd" - intsvc "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/service" + intsvc "github.com/deckhouse/virtualization-controller/pkg/controller/supervd/internal/service" "github.com/deckhouse/virtualization-controller/pkg/featuregates" "github.com/deckhouse/virtualization-controller/pkg/version" "github.com/deckhouse/virtualization/api/core/v1alpha2" diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/watcher/datavolume_watcher.go b/images/virtualization-artifact/pkg/controller/supervd/internal/watcher/datavolume_watcher.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vd/internal/watcher/datavolume_watcher.go rename to images/virtualization-artifact/pkg/controller/supervd/internal/watcher/datavolume_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/watcher/pvc_watcher.go b/images/virtualization-artifact/pkg/controller/supervd/internal/watcher/pvc_watcher.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vd/internal/watcher/pvc_watcher.go rename to images/virtualization-artifact/pkg/controller/supervd/internal/watcher/pvc_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/watcher/resource_quota_watcher.go b/images/virtualization-artifact/pkg/controller/supervd/internal/watcher/resource_quota_watcher.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vd/internal/watcher/resource_quota_watcher.go rename to images/virtualization-artifact/pkg/controller/supervd/internal/watcher/resource_quota_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/watcher/storageclass_watcher.go b/images/virtualization-artifact/pkg/controller/supervd/internal/watcher/storageclass_watcher.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vd/internal/watcher/storageclass_watcher.go rename to images/virtualization-artifact/pkg/controller/supervd/internal/watcher/storageclass_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/watcher/vdsnapshot_watcher.go b/images/virtualization-artifact/pkg/controller/supervd/internal/watcher/vdsnapshot_watcher.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vd/internal/watcher/vdsnapshot_watcher.go rename to images/virtualization-artifact/pkg/controller/supervd/internal/watcher/vdsnapshot_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/watcher/virtualmachine_watcher.go b/images/virtualization-artifact/pkg/controller/supervd/internal/watcher/virtualmachine_watcher.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vd/internal/watcher/virtualmachine_watcher.go rename to images/virtualization-artifact/pkg/controller/supervd/internal/watcher/virtualmachine_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/supervd/vd_controller.go b/images/virtualization-artifact/pkg/controller/supervd/vd_controller.go new file mode 100644 index 0000000000..3b63c3c88c --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/supervd/vd_controller.go @@ -0,0 +1,104 @@ +/* +Copyright 2024 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 supervd + +import ( + "context" + "time" + + corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/metrics" + + "github.com/deckhouse/deckhouse/pkg/log" + "github.com/deckhouse/virtualization-controller/pkg/config" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supervd/internal" + intsvc "github.com/deckhouse/virtualization-controller/pkg/controller/supervd/internal/service" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + "github.com/deckhouse/virtualization-controller/pkg/logger" + vdcolelctor "github.com/deckhouse/virtualization-controller/pkg/monitoring/metrics/vd" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +const ( + ControllerName = "vd-controller" + + PodVerbose = "3" + PodPullPolicy = string(corev1.PullIfNotPresent) +) + +type Condition interface { + Handle(ctx context.Context, vd *v1alpha2.VirtualDisk) error +} + +func NewController( + ctx context.Context, + mgr manager.Manager, + importerImage string, + uploaderImage string, + requirements corev1.ResourceRequirements, + dvcr *dvcr.Settings, + storageClassSettings config.VirtualDiskStorageClassSettings, + logFactory logger.Factory, + log *log.Logger, +) (controller.Controller, error) { + protection := service.NewProtectionService(mgr.GetClient(), v1alpha2.FinalizerVDProtection) + disk := service.NewDiskService(mgr.GetClient(), dvcr, protection, ControllerName) + scService := intsvc.NewVirtualDiskStorageClassService(service.NewBaseStorageClassService(mgr.GetClient()), storageClassSettings) + + reconciler := NewReconciler(mgr.GetClient(), internal.NewSemaphoreHandler( + mgr, + logFactory, + importerImage, + uploaderImage, + requirements, + dvcr, + storageClassSettings, + )) + + vdController, err := controller.New(ControllerName, mgr, controller.Options{ + Reconciler: reconciler, + RecoverPanic: ptr.To(true), + LogConstructor: logger.NewConstructor(log), + CacheSyncTimeout: 10 * time.Minute, + }) + if err != nil { + return nil, err + } + + err = reconciler.SetupController(ctx, mgr, vdController) + if err != nil { + return nil, err + } + + if err = builder.WebhookManagedBy(mgr). + For(&v1alpha2.VirtualDisk{}). + WithValidator(NewValidator(mgr.GetClient(), scService, disk)). + Complete(); err != nil { + return nil, err + } + + vdcolelctor.SetupCollector(mgr.GetCache(), metrics.Registry, log) + + log.Info("Initialized SuperVirtualDisk controller", "image", importerImage) + + return vdController, nil +} diff --git a/images/virtualization-artifact/pkg/controller/supervd/vd_reconciler.go b/images/virtualization-artifact/pkg/controller/supervd/vd_reconciler.go new file mode 100644 index 0000000000..f5961c48aa --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/supervd/vd_reconciler.go @@ -0,0 +1,133 @@ +/* +Copyright 2024 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 supervd + +import ( + "context" + "fmt" + "reflect" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + "github.com/deckhouse/virtualization-controller/pkg/controller/reconciler" + "github.com/deckhouse/virtualization-controller/pkg/controller/supervd/internal/watcher" + "github.com/deckhouse/virtualization-controller/pkg/controller/watchers" + "github.com/deckhouse/virtualization-controller/pkg/logger" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type Watcher interface { + Watch(mgr manager.Manager, ctr controller.Controller) error +} + +type Handler interface { + Handle(ctx context.Context, vd *v1alpha2.VirtualDisk) (reconcile.Result, error) +} + +type Reconciler struct { + handlers []Handler + client client.Client +} + +func NewReconciler(client client.Client, handlers ...Handler) *Reconciler { + return &Reconciler{ + handlers: handlers, + client: client, + } +} + +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + vd := reconciler.NewResource(req.NamespacedName, r.client, r.factory, r.statusGetter) + + err := vd.Fetch(ctx) + if err != nil { + return reconcile.Result{}, err + } + + if vd.IsEmpty() { + return reconcile.Result{}, nil + } + + rec := reconciler.NewBaseReconciler[Handler](r.handlers) + rec.SetHandlerExecutor(func(ctx context.Context, h Handler) (reconcile.Result, error) { + return h.Handle(ctx, vd.Changed()) + }) + rec.SetResourceUpdater(func(ctx context.Context) error { + vd.Changed().Status.ObservedGeneration = vd.Changed().Generation + + if vd.Changed().Status.Target.PersistentVolumeClaim == "" { + logger.FromContext(ctx).Error("Target.PersistentVolumeClaim is empty, restore previous value. Please report a bug.") + vd.Changed().Status.Target.PersistentVolumeClaim = vd.Current().Status.Target.PersistentVolumeClaim + } + + return vd.Update(ctx) + }) + + return rec.Reconcile(ctx) +} + +func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr controller.Controller) error { + if err := ctr.Watch( + source.Kind(mgr.GetCache(), &v1alpha2.VirtualDisk{}, + &handler.TypedEnqueueRequestForObject[*v1alpha2.VirtualDisk]{}, + ), + ); err != nil { + return fmt.Errorf("error setting watch on VirtualDisk: %w", err) + } + + vdFromVIEnqueuer := watchers.NewVirtualDiskRequestEnqueuer(mgr.GetClient(), &v1alpha2.VirtualImage{}, v1alpha2.VirtualDiskObjectRefKindVirtualImage) + viWatcher := watchers.NewObjectRefWatcher(watchers.NewVirtualImageFilter(), vdFromVIEnqueuer) + if err := viWatcher.Run(mgr, ctr); err != nil { + return fmt.Errorf("error setting watch on VIs: %w", err) + } + + vdFromCVIEnqueuer := watchers.NewVirtualDiskRequestEnqueuer(mgr.GetClient(), &v1alpha2.ClusterVirtualImage{}, v1alpha2.VirtualDiskObjectRefKindClusterVirtualImage) + cviWatcher := watchers.NewObjectRefWatcher(watchers.NewClusterVirtualImageFilter(), vdFromCVIEnqueuer) + if err := cviWatcher.Run(mgr, ctr); err != nil { + return fmt.Errorf("error setting watch on CVIs: %w", err) + } + + mgrClient := mgr.GetClient() + for _, w := range []Watcher{ + watcher.NewPersistentVolumeClaimWatcher(mgrClient), + watcher.NewVirtualDiskSnapshotWatcher(mgrClient), + watcher.NewStorageClassWatcher(mgrClient), + watcher.NewDataVolumeWatcher(), + watcher.NewVirtualMachineWatcher(), + watcher.NewResourceQuotaWatcher(mgrClient), + } { + err := w.Watch(mgr, ctr) + if err != nil { + return fmt.Errorf("failed to run watcher %s: %w", reflect.TypeOf(w).Elem().Name(), err) + } + } + + return nil +} + +func (r *Reconciler) factory() *v1alpha2.VirtualDisk { + return &v1alpha2.VirtualDisk{} +} + +func (r *Reconciler) statusGetter(obj *v1alpha2.VirtualDisk) v1alpha2.VirtualDiskStatus { + return obj.Status +} diff --git a/images/virtualization-artifact/pkg/controller/vd/vd_webhook.go b/images/virtualization-artifact/pkg/controller/supervd/vd_webhook.go similarity index 96% rename from images/virtualization-artifact/pkg/controller/vd/vd_webhook.go rename to images/virtualization-artifact/pkg/controller/supervd/vd_webhook.go index 3b7bb19603..132939cde1 100644 --- a/images/virtualization-artifact/pkg/controller/vd/vd_webhook.go +++ b/images/virtualization-artifact/pkg/controller/supervd/vd_webhook.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package vd +package supervd import ( "context" @@ -25,8 +25,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" "github.com/deckhouse/virtualization-controller/pkg/controller/service" - intsvc "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/service" - "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/validator" + intsvc "github.com/deckhouse/virtualization-controller/pkg/controller/supervd/internal/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supervd/internal/validator" "github.com/deckhouse/virtualization-controller/pkg/logger" "github.com/deckhouse/virtualization/api/core/v1alpha2" ) @@ -45,7 +45,7 @@ func NewValidator(client client.Client, scService *intsvc.VirtualDiskStorageClas validators: []VirtualDiskValidator{ validator.NewPVCSizeValidator(client), validator.NewSpecChangesValidator(scService), - validator.NewISOSourceValidator(client), + // validator.NewISOSourceValidator(client), validator.NewNameValidator(), validator.NewMigrationStorageClassValidator(client, scService, diskService), }, diff --git a/images/virtualization-artifact/pkg/controller/vm/gc.go b/images/virtualization-artifact/pkg/controller/supervm/gc.go similarity index 99% rename from images/virtualization-artifact/pkg/controller/vm/gc.go rename to images/virtualization-artifact/pkg/controller/supervm/gc.go index 5ccdc8bcdd..f79801f1b1 100644 --- a/images/virtualization-artifact/pkg/controller/vm/gc.go +++ b/images/virtualization-artifact/pkg/controller/supervm/gc.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package vm +package supervm import ( "context" diff --git a/images/virtualization-artifact/pkg/controller/supervm/internal/defaulter/defaulters_suite_test.go b/images/virtualization-artifact/pkg/controller/supervm/internal/defaulter/defaulters_suite_test.go new file mode 100644 index 0000000000..b4093edbf0 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/supervm/internal/defaulter/defaulters_suite_test.go @@ -0,0 +1,158 @@ +/* +Copyright 2025 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 defaulter_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization-controller/pkg/common/testutil" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supervm/internal/defaulter" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +func TestDefaulters(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "VM Defaulters Suite") +} + +var _ = Describe("Set default class in virtualMachineClasName", func() { + var ( + ctx = testutil.ContextBackgroundWithNoOpLogger() + classDefaulter *defaulter.VirtualMachineClassNameDefaulter + ) + + setup := func(objs ...client.Object) { + GinkgoHelper() + fakeClient, err := testutil.NewFakeClientWithObjects(objs...) + Expect(err).Should(BeNil()) + vmClassService := service.NewVirtualMachineClassService(fakeClient) + classDefaulter = defaulter.NewVirtualMachineClassNameDefaulter(fakeClient, vmClassService) + } + + newVMClass := func(name string) *v1alpha2.VirtualMachineClass { + return &v1alpha2.VirtualMachineClass{ + TypeMeta: metav1.TypeMeta{ + Kind: v1alpha2.VirtualMachineClassKind, + APIVersion: v1alpha2.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: v1alpha2.VirtualMachineClassSpec{}, + Status: v1alpha2.VirtualMachineClassStatus{}, + } + } + + newDefaultVMClass := func(name string) *v1alpha2.VirtualMachineClass { + vmClass := newVMClass(name) + vmClass.Annotations = map[string]string{ + annotations.AnnVirtualMachineClassDefault: "true", + } + return vmClass + } + + newVMWithEmptyClass := func(name string) *v1alpha2.VirtualMachine { + return &v1alpha2.VirtualMachine{ + TypeMeta: metav1.TypeMeta{ + Kind: v1alpha2.VirtualMachineKind, + APIVersion: v1alpha2.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: v1alpha2.VirtualMachineSpec{}, + Status: v1alpha2.VirtualMachineStatus{}, + } + } + + newVM := func(name, className string) *v1alpha2.VirtualMachine { + vm := newVMWithEmptyClass(name) + vm.Spec.VirtualMachineClassName = className + return vm + } + + AfterEach(func() { + classDefaulter = nil + }) + + Context("creating VM with empty virtualMachineClassName", func() { + It("should keep virtualMachineClassName empty if no default class", func() { + // Initialize fake client with some classes. + name := "single-custom-class" + setup( + newVMClass("generic"), + newVMClass(name), + ) + + vm := newVMWithEmptyClass("vm-with-empty-class") + err := classDefaulter.Default(ctx, vm) + Expect(err).Should(BeNil()) + Expect(vm.Spec.VirtualMachineClassName).Should(BeEmpty()) + }) + + It("should set virtualMachineClassName if default class is present", func() { + // Initialize fake client with existing non default class. + className := "single-default-class" + setup( + newVMClass("generic"), + newDefaultVMClass(className), + ) + + vm := newVMWithEmptyClass("vm-with-empty-class") + err := classDefaulter.Default(ctx, vm) + Expect(err).Should(BeNil()) + Expect(vm.Spec.VirtualMachineClassName).Should(Equal(className)) + }) + }) + + Context("creating VM with virtualMachineClassName", func() { + It("should not change virtualMachineClassName if no default class", func() { + // Initialize fake client with some classes. + name := "single-custom-class" + setup( + newVMClass("generic"), + newVMClass(name), + ) + + vm := newVM("vm-with-empty-class", "generic") + err := classDefaulter.Default(ctx, vm) + Expect(err).Should(BeNil()) + Expect(vm.Spec.VirtualMachineClassName).Should(Equal("generic")) + }) + + It("should not change virtualMachineClassName if default class is present", func() { + // Initialize fake client with existing non default class. + className := "single-default-class" + setup( + newVMClass("generic"), + newDefaultVMClass(className), + ) + + vm := newVM("vm-with-empty-class", "generic") + err := classDefaulter.Default(ctx, vm) + Expect(err).Should(BeNil()) + Expect(vm.Spec.VirtualMachineClassName).Should(Equal("generic")) + }) + }) +}) diff --git a/images/virtualization-artifact/pkg/controller/supervm/internal/defaulter/virtual_machine_class_name.go b/images/virtualization-artifact/pkg/controller/supervm/internal/defaulter/virtual_machine_class_name.go new file mode 100644 index 0000000000..834afb6f5f --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/supervm/internal/defaulter/virtual_machine_class_name.go @@ -0,0 +1,67 @@ +/* +Copyright 2025 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 defaulter + +import ( + "context" + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type VirtualMachineClassNameDefaulter struct { + client client.Client + vmClassService *service.VirtualMachineClassService +} + +func NewVirtualMachineClassNameDefaulter(client client.Client, vmClassService *service.VirtualMachineClassService) *VirtualMachineClassNameDefaulter { + return &VirtualMachineClassNameDefaulter{ + client: client, + vmClassService: vmClassService, + } +} + +func (v *VirtualMachineClassNameDefaulter) Default(ctx context.Context, vm *v1alpha2.VirtualMachine) error { + // Ignore if virtualMachineClassName is set. + if vm.Spec.VirtualMachineClassName != "" { + return nil + } + + // Detect and assign default class name. + classes := &v1alpha2.VirtualMachineClassList{} + err := v.client.List(ctx, classes) + if err != nil { + return fmt.Errorf("failed to list virtual machine classes: %w", err) + } + + defaultClass, err := v.vmClassService.GetDefault(classes) + if err != nil { + return err + } + + // "No default class" is not a mutating error, validators will complain + // about missing field during validation phase later. + if defaultClass == nil { + return nil + } + + vm.Spec.VirtualMachineClassName = defaultClass.GetName() + return nil +} diff --git a/images/virtualization-artifact/pkg/controller/supervm/internal/semaphore.go b/images/virtualization-artifact/pkg/controller/supervm/internal/semaphore.go new file mode 100644 index 0000000000..c72453497e --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/supervm/internal/semaphore.go @@ -0,0 +1,84 @@ +/* +Copyright 2024 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 ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + vmcontroller "github.com/deckhouse/virtualization-controller/pkg/controller/vm" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + "github.com/deckhouse/virtualization-controller/pkg/logger" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type SemaphoreHandler struct { + mgr manager.Manager + dvcrSettings *dvcr.Settings + firmwareImage string + logFactory logger.Factory + + controllers map[string]chan reconcile.Request +} + +func NewSemaphoreHandler( + mgr manager.Manager, + dvcrSettings *dvcr.Settings, + firmwareImage string, + logFactory logger.Factory, +) *SemaphoreHandler { + return &SemaphoreHandler{ + mgr: mgr, + dvcrSettings: dvcrSettings, + firmwareImage: firmwareImage, + logFactory: logFactory, + controllers: make(map[string]chan reconcile.Request), + } +} + +func (h *SemaphoreHandler) Handle(ctx context.Context, vm *v1alpha2.VirtualMachine) (reconcile.Result, error) { + logger.FromContext(ctx).Warn("[test][SUPER] STARTED") + + queue, ok := h.controllers[vm.Namespace] + if !ok { + logger.FromContext(ctx).Warn("[test][SUPER] START NEW NAMESPACED CONTROLLER") + + queue = make(chan reconcile.Request) + + err := vmcontroller.SetupController(ctx, h.mgr, h.logFactory(vmcontroller.ControllerName+"-"+vm.Namespace), h.dvcrSettings, h.firmwareImage, vm.Namespace, queue) + if err != nil { + return reconcile.Result{}, fmt.Errorf("setup vm controller for %q: %w", vm.Namespace, err) + } + + h.controllers[vm.Namespace] = queue + } + + logger.FromContext(ctx).Warn("[test][SUPER] PUSH REQUEST TO QUEUE") + + queue <- reconcile.Request{NamespacedName: types.NamespacedName{ + Name: vm.Name, + Namespace: vm.Namespace, + }} + + logger.FromContext(ctx).Warn("[test][SUPER] FINISHED") + + return reconcile.Result{}, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/affinity_validator.go b/images/virtualization-artifact/pkg/controller/supervm/internal/validators/affinity_validator.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/validators/affinity_validator.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/validators/affinity_validator.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/affinity_validator_test.go b/images/virtualization-artifact/pkg/controller/supervm/internal/validators/affinity_validator_test.go similarity index 99% rename from images/virtualization-artifact/pkg/controller/vm/internal/validators/affinity_validator_test.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/validators/affinity_validator_test.go index 1fb1e6fcf4..844fc5f94c 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/validators/affinity_validator_test.go +++ b/images/virtualization-artifact/pkg/controller/supervm/internal/validators/affinity_validator_test.go @@ -21,7 +21,7 @@ import ( . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" - "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/validators" + "github.com/deckhouse/virtualization-controller/pkg/controller/supervm/internal/validators" "github.com/deckhouse/virtualization/api/core/v1alpha2" ) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/block_device_limiter_validator.go b/images/virtualization-artifact/pkg/controller/supervm/internal/validators/block_device_limiter_validator.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/validators/block_device_limiter_validator.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/validators/block_device_limiter_validator.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/block_device_refs_validator.go b/images/virtualization-artifact/pkg/controller/supervm/internal/validators/block_device_refs_validator.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/validators/block_device_refs_validator.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/validators/block_device_refs_validator.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/block_device_refs_validator_test.go b/images/virtualization-artifact/pkg/controller/supervm/internal/validators/block_device_refs_validator_test.go similarity index 99% rename from images/virtualization-artifact/pkg/controller/vm/internal/validators/block_device_refs_validator_test.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/validators/block_device_refs_validator_test.go index 901b14ee38..39ae41f53f 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/validators/block_device_refs_validator_test.go +++ b/images/virtualization-artifact/pkg/controller/supervm/internal/validators/block_device_refs_validator_test.go @@ -23,7 +23,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/validators" + "github.com/deckhouse/virtualization-controller/pkg/controller/supervm/internal/validators" "github.com/deckhouse/virtualization/api/core/v1alpha2" ) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/cpu_count_validator.go b/images/virtualization-artifact/pkg/controller/supervm/internal/validators/cpu_count_validator.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/validators/cpu_count_validator.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/validators/cpu_count_validator.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/cpu_count_validator_test.go b/images/virtualization-artifact/pkg/controller/supervm/internal/validators/cpu_count_validator_test.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/validators/cpu_count_validator_test.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/validators/cpu_count_validator_test.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/first_block_device_validator.go b/images/virtualization-artifact/pkg/controller/supervm/internal/validators/first_block_device_validator.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/validators/first_block_device_validator.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/validators/first_block_device_validator.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/first_block_device_validator_test.go b/images/virtualization-artifact/pkg/controller/supervm/internal/validators/first_block_device_validator_test.go similarity index 99% rename from images/virtualization-artifact/pkg/controller/vm/internal/validators/first_block_device_validator_test.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/validators/first_block_device_validator_test.go index 4eee5f1747..493949b77f 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/validators/first_block_device_validator_test.go +++ b/images/virtualization-artifact/pkg/controller/supervm/internal/validators/first_block_device_validator_test.go @@ -28,7 +28,7 @@ import ( vibuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vi" vmbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vm" "github.com/deckhouse/virtualization-controller/pkg/common/testutil" - "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/validators" + "github.com/deckhouse/virtualization-controller/pkg/controller/supervm/internal/validators" "github.com/deckhouse/virtualization/api/core/v1alpha2" ) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/ipam_validator.go b/images/virtualization-artifact/pkg/controller/supervm/internal/validators/ipam_validator.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/validators/ipam_validator.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/validators/ipam_validator.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/meta_validator.go b/images/virtualization-artifact/pkg/controller/supervm/internal/validators/meta_validator.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/validators/meta_validator.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/validators/meta_validator.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go b/images/virtualization-artifact/pkg/controller/supervm/internal/validators/networks_validator.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/validators/networks_validator.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator_test.go b/images/virtualization-artifact/pkg/controller/supervm/internal/validators/networks_validator_test.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator_test.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/validators/networks_validator_test.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/sizing_policy_validator.go b/images/virtualization-artifact/pkg/controller/supervm/internal/validators/sizing_policy_validator.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/validators/sizing_policy_validator.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/validators/sizing_policy_validator.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/topology_spread_validator.go b/images/virtualization-artifact/pkg/controller/supervm/internal/validators/topology_spread_validator.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/validators/topology_spread_validator.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/validators/topology_spread_validator.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/validators_test.go b/images/virtualization-artifact/pkg/controller/supervm/internal/validators/validators_test.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/validators/validators_test.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/validators/validators_test.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/watcher/clustervirtualimage_watcher.go b/images/virtualization-artifact/pkg/controller/supervm/internal/watcher/clustervirtualimage_watcher.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/watcher/clustervirtualimage_watcher.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/watcher/clustervirtualimage_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/watcher/kvvm_watcher.go b/images/virtualization-artifact/pkg/controller/supervm/internal/watcher/kvvm_watcher.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/watcher/kvvm_watcher.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/watcher/kvvm_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/watcher/kvvmi_watcher.go b/images/virtualization-artifact/pkg/controller/supervm/internal/watcher/kvvmi_watcher.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/watcher/kvvmi_watcher.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/watcher/kvvmi_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/watcher/pod_watcher.go b/images/virtualization-artifact/pkg/controller/supervm/internal/watcher/pod_watcher.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/watcher/pod_watcher.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/watcher/pod_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/supervm/internal/watcher/super_watcher.go b/images/virtualization-artifact/pkg/controller/supervm/internal/watcher/super_watcher.go new file mode 100644 index 0000000000..5af497ec90 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/supervm/internal/watcher/super_watcher.go @@ -0,0 +1,61 @@ +/* +Copyright 2025 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" + + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func NewNamespacedWatcher() *NamespacedWatcher { + return &NamespacedWatcher{} +} + +type NamespacedWatcher struct{} + +func (w *NamespacedWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + err := ctr.Watch(&NamespacedSource{}) + if err != nil { + return fmt.Errorf("error setting watch on NamespacedSource: %w", err) + } + return nil +} + +type NamespacedSource struct { + queue <-chan reconcile.Request +} + +func (s *NamespacedSource) Start(ctx context.Context, queue workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + go func() { + select { + case req, ok := <-s.queue: + if !ok { + return + } + queue.Add(req) + case <-ctx.Done(): + return + } + }() + + return nil +} diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/watcher/virtualdisk_watcher.go b/images/virtualization-artifact/pkg/controller/supervm/internal/watcher/virtualdisk_watcher.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/watcher/virtualdisk_watcher.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/watcher/virtualdisk_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/watcher/virtualimage_watcher.go b/images/virtualization-artifact/pkg/controller/supervm/internal/watcher/virtualimage_watcher.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/watcher/virtualimage_watcher.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/watcher/virtualimage_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/watcher/vmclass_watcher.go b/images/virtualization-artifact/pkg/controller/supervm/internal/watcher/vmclass_watcher.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/watcher/vmclass_watcher.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/watcher/vmclass_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/watcher/vmip_watcher.go b/images/virtualization-artifact/pkg/controller/supervm/internal/watcher/vmip_watcher.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/watcher/vmip_watcher.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/watcher/vmip_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/watcher/vmmac_watcher.go b/images/virtualization-artifact/pkg/controller/supervm/internal/watcher/vmmac_watcher.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/watcher/vmmac_watcher.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/watcher/vmmac_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/watcher/vmop_watcher.go b/images/virtualization-artifact/pkg/controller/supervm/internal/watcher/vmop_watcher.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/watcher/vmop_watcher.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/watcher/vmop_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/watcher/vmsnapshot_watcher.go b/images/virtualization-artifact/pkg/controller/supervm/internal/watcher/vmsnapshot_watcher.go similarity index 100% rename from images/virtualization-artifact/pkg/controller/vm/internal/watcher/vmsnapshot_watcher.go rename to images/virtualization-artifact/pkg/controller/supervm/internal/watcher/vmsnapshot_watcher.go diff --git a/images/virtualization-artifact/pkg/controller/supervm/vm_controller.go b/images/virtualization-artifact/pkg/controller/supervm/vm_controller.go new file mode 100644 index 0000000000..fb28fe8942 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/supervm/vm_controller.go @@ -0,0 +1,87 @@ +/* +Copyright 2025 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 supervm + +import ( + "context" + + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/metrics" + + "github.com/deckhouse/deckhouse/pkg/log" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supervm/internal" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + "github.com/deckhouse/virtualization-controller/pkg/featuregates" + "github.com/deckhouse/virtualization-controller/pkg/logger" + vmmetrics "github.com/deckhouse/virtualization-controller/pkg/monitoring/metrics/virtualmachine" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +const ( + ControllerName = "super-vm-controller" +) + +func SetupController( + ctx context.Context, + mgr manager.Manager, + dvcrSettings *dvcr.Settings, + firmwareImage string, + logFactory logger.Factory, + log *log.Logger, +) error { + cache := mgr.GetCache() + client := mgr.GetClient() + blockDeviceService := service.NewBlockDeviceService(client) + vmClassService := service.NewVirtualMachineClassService(client) + + r := NewReconciler(client, internal.NewSemaphoreHandler( + mgr, + dvcrSettings, + firmwareImage, + logFactory, + )) + c, err := controller.New(ControllerName, mgr, controller.Options{ + Reconciler: r, + RecoverPanic: ptr.To(true), + LogConstructor: logger.NewConstructor(log), + UsePriorityQueue: ptr.To(true), + }) + if err != nil { + return err + } + + if err = r.SetupController(ctx, mgr, c); err != nil { + return err + } + + if err = builder.WebhookManagedBy(mgr). + For(&v1alpha2.VirtualMachine{}). + WithValidator(NewValidator(client, blockDeviceService, featuregates.Default(), log)). + WithDefaulter(NewDefaulter(client, vmClassService, log)). + Complete(); err != nil { + return err + } + + vmmetrics.SetupCollector(cache, metrics.Registry, log) + + log.Info("Initialized SuperVirtualMachine controller") + return nil +} diff --git a/images/virtualization-artifact/pkg/controller/supervm/vm_reconciler.go b/images/virtualization-artifact/pkg/controller/supervm/vm_reconciler.go new file mode 100644 index 0000000000..6425011ebd --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/supervm/vm_reconciler.go @@ -0,0 +1,116 @@ +/* +Copyright 2024 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 supervm + +import ( + "context" + "fmt" + "reflect" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + "github.com/deckhouse/virtualization-controller/pkg/controller/reconciler" + "github.com/deckhouse/virtualization-controller/pkg/controller/supervm/internal/watcher" + "github.com/deckhouse/virtualization-controller/pkg/logger" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type Handler interface { + Handle(ctx context.Context, vm *v1alpha2.VirtualMachine) (reconcile.Result, error) +} + +type Watcher interface { + Watch(mgr manager.Manager, ctr controller.Controller) error +} + +func NewReconciler(client client.Client, handlers ...Handler) *Reconciler { + return &Reconciler{ + client: client, + handlers: handlers, + } +} + +type Reconciler struct { + client client.Client + handlers []Handler +} + +func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr controller.Controller) error { + if err := ctr.Watch(source.Kind(mgr.GetCache(), &v1alpha2.VirtualMachine{}, &handler.TypedEnqueueRequestForObject[*v1alpha2.VirtualMachine]{})); err != nil { + return fmt.Errorf("error setting watch on VM: %w", err) + } + + for _, w := range []Watcher{ + watcher.NewKVVMWatcher(), + watcher.NewKVVMIWatcher(), + watcher.NewPodWatcher(), + watcher.NewVirtualImageWatcher(mgr.GetClient()), + watcher.NewClusterVirtualImageWatcher(mgr.GetClient()), + watcher.NewVirtualDiskWatcher(mgr.GetClient()), + watcher.NewVMIPWatcher(), + watcher.NewVirtualMachineClassWatcher(), + watcher.NewVirtualMachineSnapshotWatcher(), + watcher.NewVMOPWatcher(), + watcher.NewVMMACWatcher(), + } { + err := w.Watch(mgr, ctr) + if err != nil { + return fmt.Errorf("failed to run watcher %s: %w", reflect.TypeOf(w).Elem().Name(), err) + } + } + + return nil +} + +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + log := logger.FromContext(ctx) + + vm := reconciler.NewResource(req.NamespacedName, r.client, r.factory, r.statusGetter) + + err := vm.Fetch(ctx) + if err != nil { + return reconcile.Result{}, err + } + + if vm.IsEmpty() { + log.Info("Reconcile observe an absent VirtualMachine: it may be deleted") + return reconcile.Result{}, nil + } + + rec := reconciler.NewBaseReconciler[Handler](r.handlers) + rec.SetHandlerExecutor(func(ctx context.Context, h Handler) (reconcile.Result, error) { + return h.Handle(ctx, vm.Changed()) + }) + rec.SetResourceUpdater(func(ctx context.Context) error { + return vm.Update(ctx) + }) + + return rec.Reconcile(ctx) +} + +func (r *Reconciler) factory() *v1alpha2.VirtualMachine { + return &v1alpha2.VirtualMachine{} +} + +func (r *Reconciler) statusGetter(obj *v1alpha2.VirtualMachine) v1alpha2.VirtualMachineStatus { + return obj.Status +} diff --git a/images/virtualization-artifact/pkg/controller/vm/vm_webhook.go b/images/virtualization-artifact/pkg/controller/supervm/vm_webhook.go similarity index 98% rename from images/virtualization-artifact/pkg/controller/vm/vm_webhook.go rename to images/virtualization-artifact/pkg/controller/supervm/vm_webhook.go index ccd2bd0301..73a299b21c 100644 --- a/images/virtualization-artifact/pkg/controller/vm/vm_webhook.go +++ b/images/virtualization-artifact/pkg/controller/supervm/vm_webhook.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package vm +package supervm import ( "context" @@ -27,8 +27,8 @@ import ( "github.com/deckhouse/deckhouse/pkg/log" "github.com/deckhouse/virtualization-controller/pkg/controller/service" - "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/defaulter" - "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/validators" + "github.com/deckhouse/virtualization-controller/pkg/controller/supervm/internal/defaulter" + "github.com/deckhouse/virtualization-controller/pkg/controller/supervm/internal/validators" "github.com/deckhouse/virtualization/api/core/v1alpha2" ) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/validator/iso_source_validator.go b/images/virtualization-artifact/pkg/controller/vd/internal/validator/iso_source_validator.go deleted file mode 100644 index a8059648a1..0000000000 --- a/images/virtualization-artifact/pkg/controller/vd/internal/validator/iso_source_validator.go +++ /dev/null @@ -1,100 +0,0 @@ -/* -Copyright 2024 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 validator - -import ( - "context" - - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - - "github.com/deckhouse/virtualization-controller/pkg/common/imageformat" - "github.com/deckhouse/virtualization-controller/pkg/controller" - "github.com/deckhouse/virtualization-controller/pkg/controller/service" - "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/source" - "github.com/deckhouse/virtualization/api/core/v1alpha2" -) - -type ISOSourceValidator struct { - client client.Client -} - -func NewISOSourceValidator(client client.Client) *ISOSourceValidator { - return &ISOSourceValidator{client: client} -} - -func (v *ISOSourceValidator) ValidateCreate(ctx context.Context, vd *v1alpha2.VirtualDisk) (admission.Warnings, error) { - if vd.Spec.DataSource == nil { - return nil, nil - } - - if vd.Spec.DataSource.Type != v1alpha2.DataSourceTypeObjectRef || vd.Spec.DataSource.ObjectRef == nil { - return nil, nil - } - - switch vd.Spec.DataSource.ObjectRef.Kind { - case v1alpha2.VirtualDiskObjectRefKindVirtualImage, - v1alpha2.VirtualDiskObjectRefKindClusterVirtualImage: - dvcrDataSource, err := controller.NewDVCRDataSourcesForVMD(ctx, vd.Spec.DataSource, vd, v.client) - if err != nil { - return nil, err - } - - if !dvcrDataSource.IsReady() { - return nil, nil - } - - if imageformat.IsISO(dvcrDataSource.GetFormat()) { - return admission.Warnings{ - service.CapitalizeFirstLetter(source.ErrISOSourceNotSupported.Error()), - }, nil - } - } - - return nil, nil -} - -func (v *ISOSourceValidator) ValidateUpdate(ctx context.Context, _, newVD *v1alpha2.VirtualDisk) (admission.Warnings, error) { - if newVD.Spec.DataSource == nil { - return nil, nil - } - - if newVD.Spec.DataSource.Type != v1alpha2.DataSourceTypeObjectRef || newVD.Spec.DataSource.ObjectRef == nil { - return nil, nil - } - - switch newVD.Spec.DataSource.ObjectRef.Kind { - case v1alpha2.VirtualDiskObjectRefKindVirtualImage, - v1alpha2.VirtualDiskObjectRefKindClusterVirtualImage: - dvcrDataSource, err := controller.NewDVCRDataSourcesForVMD(ctx, newVD.Spec.DataSource, newVD, v.client) - if err != nil { - return nil, err - } - - if !dvcrDataSource.IsReady() { - return nil, nil - } - - if imageformat.IsISO(dvcrDataSource.GetFormat()) { - return admission.Warnings{ - service.CapitalizeFirstLetter(source.ErrISOSourceNotSupported.Error()), - }, nil - } - } - - return nil, nil -} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/watcher/super_watcher.go b/images/virtualization-artifact/pkg/controller/vd/internal/watcher/super_watcher.go new file mode 100644 index 0000000000..7e7ccb4440 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vd/internal/watcher/super_watcher.go @@ -0,0 +1,48 @@ +/* +Copyright 2025 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" + + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/logger" +) + +type SuperSource struct { + queue <-chan reconcile.Request +} + +func NewSuperSource(queue <-chan reconcile.Request) *SuperSource { + return &SuperSource{ + queue: queue, + } +} + +func (s *SuperSource) Start(ctx context.Context, wq workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + go func() { + for req := range s.queue { + logger.FromContext(ctx).Warn("[test][SOURCE] START") + wq.Add(req) + logger.FromContext(ctx).Warn("[test][SOURCE] FINISHED") + } + }() + + return nil +} diff --git a/images/virtualization-artifact/pkg/controller/vd/vd_controller.go b/images/virtualization-artifact/pkg/controller/vd/vd_controller.go index d9c53746f7..913b19f905 100644 --- a/images/virtualization-artifact/pkg/controller/vd/vd_controller.go +++ b/images/virtualization-artifact/pkg/controller/vd/vd_controller.go @@ -18,14 +18,15 @@ package vd import ( "context" + "fmt" "time" corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/util/workqueue" "k8s.io/utils/ptr" - "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/metrics" + "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/deckhouse/deckhouse/pkg/log" "github.com/deckhouse/virtualization-controller/pkg/config" @@ -37,7 +38,6 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/eventrecord" "github.com/deckhouse/virtualization-controller/pkg/featuregates" "github.com/deckhouse/virtualization-controller/pkg/logger" - vdcolelctor "github.com/deckhouse/virtualization-controller/pkg/monitoring/metrics/vd" "github.com/deckhouse/virtualization/api/core/v1alpha2" ) @@ -61,6 +61,8 @@ func NewController( requirements corev1.ResourceRequirements, dvcr *dvcr.Settings, storageClassSettings config.VirtualDiskStorageClassSettings, + ns string, + queue <-chan reconcile.Request, ) (controller.Controller, error) { stat := service.NewStatService(log) protection := service.NewProtectionService(mgr.GetClient(), v1alpha2.FinalizerVDProtection) @@ -78,7 +80,7 @@ func NewController( sources.Set(v1alpha2.DataSourceTypeObjectRef, source.NewObjectRefDataSource(recorder, disk, mgr.GetClient())) sources.Set(v1alpha2.DataSourceTypeUpload, source.NewUploadDataSource(recorder, stat, uploader, disk, dvcr, mgr.GetClient())) - reconciler := NewReconciler( + r := NewReconciler( mgr.GetClient(), internal.NewInitHandler(), internal.NewStorageClassReadyHandler(scService), @@ -93,31 +95,34 @@ func NewController( internal.NewProtectionHandler(), ) - vdController, err := controller.New(ControllerName, mgr, controller.Options{ - Reconciler: reconciler, + options := controller.Options{ + Reconciler: r, RecoverPanic: ptr.To(true), LogConstructor: logger.NewConstructor(log), CacheSyncTimeout: 10 * time.Minute, - }) - if err != nil { - return nil, err + RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request]( + 5*time.Second, + 5*time.Second, + ), } - - err = reconciler.SetupController(ctx, mgr, vdController) + options.DefaultFromConfig(mgr.GetControllerOptions()) + c, err := controller.NewTypedUnmanaged(ControllerName+"-"+ns, options) if err != nil { - return nil, err + return nil, fmt.Errorf("new typed unmanaged controller failed: %w", err) } - if err = builder.WebhookManagedBy(mgr). - For(&v1alpha2.VirtualDisk{}). - WithValidator(NewValidator(mgr.GetClient(), scService, disk)). - Complete(); err != nil { + if err = r.SetupController(ctx, c, queue); err != nil { return nil, err } - vdcolelctor.SetupCollector(mgr.GetCache(), metrics.Registry, log) + go func() { + err = c.Start(ctx) + if err != nil { + log.Error(fmt.Errorf("error starting controller %q: %w", ControllerName+"-"+ns, err).Error()) + } + }() - log.Info("Initialized VirtualDisk controller", "image", importerImage) + log.Info(fmt.Sprintf("the NamespacedVirtualMachine controller has been started for %q", ns)) - return vdController, nil + return c, nil } diff --git a/images/virtualization-artifact/pkg/controller/vd/vd_reconciler.go b/images/virtualization-artifact/pkg/controller/vd/vd_reconciler.go index 654caf4b0f..ab6755d942 100644 --- a/images/virtualization-artifact/pkg/controller/vd/vd_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vd/vd_reconciler.go @@ -19,19 +19,15 @@ package vd import ( "context" "fmt" - "reflect" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "sigs.k8s.io/controller-runtime/pkg/source" "github.com/deckhouse/virtualization-controller/pkg/controller/reconciler" vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/watcher" - "github.com/deckhouse/virtualization-controller/pkg/controller/watchers" "github.com/deckhouse/virtualization-controller/pkg/logger" "github.com/deckhouse/virtualization/api/core/v1alpha2" ) @@ -86,40 +82,10 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return rec.Reconcile(ctx) } -func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr controller.Controller) error { - if err := ctr.Watch( - source.Kind(mgr.GetCache(), &v1alpha2.VirtualDisk{}, - &handler.TypedEnqueueRequestForObject[*v1alpha2.VirtualDisk]{}, - ), - ); err != nil { - return fmt.Errorf("error setting watch on VirtualDisk: %w", err) - } - - vdFromVIEnqueuer := watchers.NewVirtualDiskRequestEnqueuer(mgr.GetClient(), &v1alpha2.VirtualImage{}, v1alpha2.VirtualDiskObjectRefKindVirtualImage) - viWatcher := watchers.NewObjectRefWatcher(watchers.NewVirtualImageFilter(), vdFromVIEnqueuer) - if err := viWatcher.Run(mgr, ctr); err != nil { - return fmt.Errorf("error setting watch on VIs: %w", err) - } - - vdFromCVIEnqueuer := watchers.NewVirtualDiskRequestEnqueuer(mgr.GetClient(), &v1alpha2.ClusterVirtualImage{}, v1alpha2.VirtualDiskObjectRefKindClusterVirtualImage) - cviWatcher := watchers.NewObjectRefWatcher(watchers.NewClusterVirtualImageFilter(), vdFromCVIEnqueuer) - if err := cviWatcher.Run(mgr, ctr); err != nil { - return fmt.Errorf("error setting watch on CVIs: %w", err) - } - - mgrClient := mgr.GetClient() - for _, w := range []Watcher{ - watcher.NewPersistentVolumeClaimWatcher(mgrClient), - watcher.NewVirtualDiskSnapshotWatcher(mgrClient), - watcher.NewStorageClassWatcher(mgrClient), - watcher.NewDataVolumeWatcher(), - watcher.NewVirtualMachineWatcher(), - watcher.NewResourceQuotaWatcher(mgrClient), - } { - err := w.Watch(mgr, ctr) - if err != nil { - return fmt.Errorf("failed to run watcher %s: %w", reflect.TypeOf(w).Elem().Name(), err) - } +func (r *Reconciler) SetupController(_ context.Context, ctr controller.Controller, queue <-chan reconcile.Request) error { + err := ctr.Watch(watcher.NewSuperSource(queue)) + if err != nil { + return fmt.Errorf("failed to run super watcher: %w", err) } return nil diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/class.go b/images/virtualization-artifact/pkg/controller/vm/internal/class.go index a78c5d0cc3..e9e507a573 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/class.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/class.go @@ -58,7 +58,7 @@ func (h *ClassHandler) Handle(ctx context.Context, s state.VirtualMachineState) changed := s.VirtualMachine().Changed() if update := addAllUnknown(changed, vmcondition.TypeClassReady); update { - return reconcile.Result{Requeue: true}, nil + return reconcile.Result{}, nil } if isDeletion(current) { diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/ipam.go b/images/virtualization-artifact/pkg/controller/vm/internal/ipam.go index 79c7dacd86..e94714bb1b 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/ipam.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/ipam.go @@ -69,7 +69,7 @@ func (h *IPAMHandler) Handle(ctx context.Context, s state.VirtualMachineState) ( changed := s.VirtualMachine().Changed() if update := addAllUnknown(changed, vmcondition.TypeIPAddressReady); update { - return reconcile.Result{Requeue: true}, nil + return reconcile.Result{}, nil } //nolint:staticcheck // it's deprecated. diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go index 699ef35652..48b03dbffc 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go @@ -79,7 +79,7 @@ func (h *LifeCycleHandler) Handle(ctx context.Context, s state.VirtualMachineSta if updated := addAllUnknown(changed, vmcondition.TypeRunning); updated || changed.Status.Phase == "" { changed.Status.Phase = v1alpha2.MachinePending - return reconcile.Result{Requeue: true}, nil + return reconcile.Result{}, nil } kvvm, err := s.KVVM(ctx) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/provisioning.go b/images/virtualization-artifact/pkg/controller/vm/internal/provisioning.go index b3511b33a2..de4735ba71 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/provisioning.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/provisioning.go @@ -55,7 +55,7 @@ func (h *ProvisioningHandler) Handle(ctx context.Context, s state.VirtualMachine changed := s.VirtualMachine().Changed() if update := addAllUnknown(changed, vmcondition.TypeProvisioningReady); update { - return reconcile.Result{Requeue: true}, nil + return reconcile.Result{}, nil } if isDeletion(current) { diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/watcher/super_watcher.go b/images/virtualization-artifact/pkg/controller/vm/internal/watcher/super_watcher.go new file mode 100644 index 0000000000..7e7ccb4440 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vm/internal/watcher/super_watcher.go @@ -0,0 +1,48 @@ +/* +Copyright 2025 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" + + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/logger" +) + +type SuperSource struct { + queue <-chan reconcile.Request +} + +func NewSuperSource(queue <-chan reconcile.Request) *SuperSource { + return &SuperSource{ + queue: queue, + } +} + +func (s *SuperSource) Start(ctx context.Context, wq workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + go func() { + for req := range s.queue { + logger.FromContext(ctx).Warn("[test][SOURCE] START") + wq.Add(req) + logger.FromContext(ctx).Warn("[test][SOURCE] FINISHED") + } + }() + + return nil +} diff --git a/images/virtualization-artifact/pkg/controller/vm/vm_controller.go b/images/virtualization-artifact/pkg/controller/vm/vm_controller.go index 29d683f0f5..83541afa02 100644 --- a/images/virtualization-artifact/pkg/controller/vm/vm_controller.go +++ b/images/virtualization-artifact/pkg/controller/vm/vm_controller.go @@ -18,13 +18,14 @@ package vm import ( "context" + "fmt" "time" + "k8s.io/client-go/util/workqueue" "k8s.io/utils/ptr" - "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/metrics" + "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/deckhouse/deckhouse/pkg/log" "github.com/deckhouse/virtualization-controller/pkg/controller/netmanager" @@ -35,8 +36,6 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/eventrecord" "github.com/deckhouse/virtualization-controller/pkg/featuregates" "github.com/deckhouse/virtualization-controller/pkg/logger" - vmmetrics "github.com/deckhouse/virtualization-controller/pkg/monitoring/metrics/virtualmachine" - "github.com/deckhouse/virtualization/api/core/v1alpha2" ) const ( @@ -49,12 +48,12 @@ func SetupController( log *log.Logger, dvcrSettings *dvcr.Settings, firmwareImage string, + ns string, + queue <-chan reconcile.Request, ) error { - recorder := eventrecord.NewEventRecorderLogger(mgr, ControllerName) - mgrCache := mgr.GetCache() + recorder := eventrecord.NewEventRecorderLogger(mgr, ControllerName+"-"+ns) client := mgr.GetClient() blockDeviceService := service.NewBlockDeviceService(client) - vmClassService := service.NewVirtualMachineClassService(client) migrateVolumesService := vmservice.NewMigrationVolumesService(client, internal.MakeKVVMFromVMSpec, 10*time.Second) @@ -81,32 +80,37 @@ func SetupController( internal.NewEvictHandler(), internal.NewStatisticHandler(client), } + r := NewReconciler(client, handlers...) - c, err := controller.New(ControllerName, mgr, controller.Options{ + options := controller.Options{ Reconciler: r, RecoverPanic: ptr.To(true), LogConstructor: logger.NewConstructor(log), CacheSyncTimeout: 10 * time.Minute, - }) + RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request]( + 5*time.Second, + 5*time.Second, + ), + } + options.DefaultFromConfig(mgr.GetControllerOptions()) + c, err := controller.NewTypedUnmanaged(ControllerName+"-"+ns, options) if err != nil { - return err + return fmt.Errorf("new typed unmanaged controller failed: %w", err) } - if err = r.SetupController(ctx, mgr, c); err != nil { + if err = r.SetupController(ctx, c, queue); err != nil { return err } - if err = builder.WebhookManagedBy(mgr). - For(&v1alpha2.VirtualMachine{}). - WithValidator(NewValidator(client, blockDeviceService, featuregates.Default(), log)). - WithDefaulter(NewDefaulter(client, vmClassService, log)). - Complete(); err != nil { - return err - } + go func() { + err = c.Start(ctx) + if err != nil { + log.Error(fmt.Errorf("error starting controller %q: %w", ControllerName+"-"+ns, err).Error()) + } + }() - vmmetrics.SetupCollector(mgrCache, metrics.Registry, log) + log.Info(fmt.Sprintf("the NamespacedVirtualMachine controller has been started for %q", ns)) - log.Info("Initialized VirtualMachine controller") 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 92d0baaa20..98b2812a9e 100644 --- a/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go @@ -19,14 +19,11 @@ package vm import ( "context" "fmt" - "reflect" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "sigs.k8s.io/controller-runtime/pkg/source" "github.com/deckhouse/virtualization-controller/pkg/controller/reconciler" "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/state" @@ -56,34 +53,18 @@ type Reconciler struct { handlers []Handler } -func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr controller.Controller) error { - if err := ctr.Watch(source.Kind(mgr.GetCache(), &v1alpha2.VirtualMachine{}, &handler.TypedEnqueueRequestForObject[*v1alpha2.VirtualMachine]{})); err != nil { - return fmt.Errorf("error setting watch on VM: %w", err) - } - - for _, w := range []Watcher{ - watcher.NewKVVMWatcher(), - watcher.NewKVVMIWatcher(), - watcher.NewPodWatcher(), - watcher.NewVirtualImageWatcher(mgr.GetClient()), - watcher.NewClusterVirtualImageWatcher(mgr.GetClient()), - watcher.NewVirtualDiskWatcher(mgr.GetClient()), - watcher.NewVMIPWatcher(), - watcher.NewVirtualMachineClassWatcher(), - watcher.NewVirtualMachineSnapshotWatcher(), - watcher.NewVMOPWatcher(), - watcher.NewVMMACWatcher(), - } { - err := w.Watch(mgr, ctr) - if err != nil { - return fmt.Errorf("failed to run watcher %s: %w", reflect.TypeOf(w).Elem().Name(), err) - } +func (r *Reconciler) SetupController(_ context.Context, ctr controller.Controller, queue <-chan reconcile.Request) error { + err := ctr.Watch(watcher.NewSuperSource(queue)) + if err != nil { + return fmt.Errorf("failed to run super watcher: %w", err) } return nil } func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + logger.FromContext(ctx).Warn("[test][NAMESPACED] START") + log := logger.FromContext(ctx) vm := reconciler.NewResource(req.NamespacedName, r.client, r.factory, r.statusGetter) diff --git a/images/virtualization-artifact/pkg/controller/vmip/vmip_controller.go b/images/virtualization-artifact/pkg/controller/vmip/vmip_controller.go index 6bd2f9885e..e9cecf7d27 100644 --- a/images/virtualization-artifact/pkg/controller/vmip/vmip_controller.go +++ b/images/virtualization-artifact/pkg/controller/vmip/vmip_controller.go @@ -20,10 +20,12 @@ import ( "context" "time" + "k8s.io/client-go/util/workqueue" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/deckhouse/deckhouse/pkg/log" "github.com/deckhouse/virtualization-controller/pkg/controller/vmip/internal" @@ -68,6 +70,10 @@ func NewController( RecoverPanic: ptr.To(true), LogConstructor: logger.NewConstructor(log), CacheSyncTimeout: 10 * time.Minute, + RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request]( + 5*time.Second, + 5*time.Second, + ), }) if err != nil { return nil, err diff --git a/images/virtualization-artifact/pkg/logger/logger.go b/images/virtualization-artifact/pkg/logger/logger.go index 069e3d336d..5d9b29cd5a 100644 --- a/images/virtualization-artifact/pkg/logger/logger.go +++ b/images/virtualization-artifact/pkg/logger/logger.go @@ -49,21 +49,25 @@ func NewLogger(level, output string, debugVerbosity int) *log.Logger { }) } -func NewControllerLogger(controllerName, level, output string, debugVerbosity int, controllerDebugList []string) *log.Logger { +type Factory func(controllerName string) *log.Logger + +func NewFactory(level, output string, debugVerbosity int, controllerDebugList []string) Factory { slogLevel := detectLogLevel(level, debugVerbosity) - if slices.Contains(controllerDebugList, controllerName) { - if debugVerbosity != 0 { - slogLevel = slog.Level(-1 * debugVerbosity) - } else { - slogLevel = log.LevelDebug.Level() + return func(controllerName string) *log.Logger { + if slices.Contains(controllerDebugList, controllerName) { + if debugVerbosity != 0 { + slogLevel = slog.Level(-1 * debugVerbosity) + } else { + slogLevel = log.LevelDebug.Level() + } } - } - return log.NewLogger(log.Options{ - Level: slogLevel, - Output: detectLogOutput(output), - }).With(SlogController(controllerName)) + return log.NewLogger(log.Options{ + Level: slogLevel, + Output: detectLogOutput(output), + }).With(SlogController(controllerName)) + } } func detectLogLevel(level string, debugVerbosity int) slog.Level {