diff --git a/images/virtualization-artifact/pkg/controller/vmop/vmop_webhook.go b/images/virtualization-artifact/pkg/controller/vmop/vmop_webhook.go index 34a415d6f4..7889c376ae 100644 --- a/images/virtualization-artifact/pkg/controller/vmop/vmop_webhook.go +++ b/images/virtualization-artifact/pkg/controller/vmop/vmop_webhook.go @@ -20,14 +20,19 @@ import ( "context" "errors" "fmt" + "slices" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" "github.com/deckhouse/deckhouse/pkg/log" + commonvmop "github.com/deckhouse/virtualization-controller/pkg/common/vmop" "github.com/deckhouse/virtualization-controller/pkg/controller/validator" "github.com/deckhouse/virtualization-controller/pkg/featuregates" + "github.com/deckhouse/virtualization-controller/pkg/version" "github.com/deckhouse/virtualization/api/core/v1alpha2" ) @@ -35,7 +40,10 @@ func NewValidator(c client.Client, log *log.Logger) admission.CustomValidator { return validator.NewValidator[*v1alpha2.VirtualMachineOperation](log. With("controller", "vmop-controller"). With("webhook", "validation"), - ).WithCreateValidators(&nodeSelectorValidator{}) + ).WithCreateValidators( + &nodeSelectorValidator{}, + &localStorageMigrationValidator{client: c}, + ) } type nodeSelectorValidator struct{} @@ -68,3 +76,51 @@ func (n *nodeSelectorValidator) validateNodeSelector(nodeSelector map[string]str return nil } + +type localStorageMigrationValidator struct { + client client.Client +} + +func (v *localStorageMigrationValidator) ValidateCreate(ctx context.Context, vmop *v1alpha2.VirtualMachineOperation) (admission.Warnings, error) { + if version.GetEdition() != version.EditionCE { + return nil, nil + } + + if !commonvmop.IsMigration(vmop) { + return nil, nil + } + + vm := &v1alpha2.VirtualMachine{} + err := v.client.Get(ctx, types.NamespacedName{Name: vmop.Spec.VirtualMachine, Namespace: vmop.Namespace}, vm) + if err != nil { + return nil, fmt.Errorf("failed to get VirtualMachine: %w", err) + } + + for _, bda := range vm.Status.BlockDeviceRefs { + if bda.Kind != v1alpha2.DiskDevice { + continue + } + + vd := &v1alpha2.VirtualDisk{} + err := v.client.Get(ctx, types.NamespacedName{Name: bda.Name, Namespace: vmop.Namespace}, vd) + if err != nil { + return nil, fmt.Errorf("failed to get VirtualDisk %s: %w", bda.Name, err) + } + + if vd.Status.Target.PersistentVolumeClaim == "" { + continue + } + + pvc := &corev1.PersistentVolumeClaim{} + err = v.client.Get(ctx, types.NamespacedName{Name: vd.Status.Target.PersistentVolumeClaim, Namespace: vmop.Namespace}, pvc) + if err != nil { + return nil, fmt.Errorf("failed to get PersistentVolumeClaim %s: %w", vd.Status.Target.PersistentVolumeClaim, err) + } + + if !slices.Contains(pvc.Spec.AccessModes, corev1.ReadWriteMany) { + return nil, fmt.Errorf("migration of virtual machines with local (RWO) virtual disks is available only in the Enterprise Edition (EE)") + } + } + + return nil, nil +}