diff --git a/hooks/module_config_validator.py b/hooks/module_config_validator.py index bfd63c8867..b11e889fda 100755 --- a/hooks/module_config_validator.py +++ b/hooks/module_config_validator.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2024 Flant JSC +# 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. @@ -14,18 +14,20 @@ # See the License for the specific language governing permissions and # limitations under the License. +from ipaddress import IPv4Address, IPv4Network, ip_address, ip_network from typing import Callable + +import common from deckhouse import hook from lib.hooks.hook import Hook -from ipaddress import ip_network,ip_address,IPv4Network,IPv4Address -import common class ModuleConfigValidateHook(Hook): - KIND="ModuleConfig" - API_VERSION="deckhouse.io/v1alpha1" + KIND = "ModuleConfig" + API_VERSION = "deckhouse.io/v1alpha1" SNAPSHOT_MODULE_CONFIG = "module-config" SNAPSHOT_NODES = "nodes" + SNAPSHOT_STORAGE_PROFILES = "internalvirtualizationstorageprofiles" def __init__(self, module_name: str): self.module_name = module_name @@ -42,13 +44,11 @@ def generate_config(self) -> dict: "executeHookOnEvent": [], "apiVersion": self.API_VERSION, "kind": self.KIND, - "nameSelector": { - "matchNames": [self.module_name] - }, + "nameSelector": {"matchNames": [self.module_name]}, "group": "main", - "jqFilter": '{"cidrs": .spec.settings.virtualMachineCIDRs}', + "jqFilter": '{"settings": .spec.settings}', "queue": self.queue, - "keepFullObjectsInMemory": False + "keepFullObjectsInMemory": False, }, { "name": self.SNAPSHOT_NODES, @@ -59,32 +59,82 @@ def generate_config(self) -> dict: "group": "main", "jqFilter": '{"addresses": .status.addresses}', "queue": self.queue, - "keepFullObjectsInMemory": False - } - ] + "keepFullObjectsInMemory": False, + }, + { + "name": self.SNAPSHOT_STORAGE_PROFILES, + "executeHookOnSynchronization": True, + "executeHookOnEvent": [], + "apiVersion": "cdi.internal.virtualization.deckhouse.io/v1beta1", + "kind": "InternalVirtualizationStorageProfile", + "group": "main", + "jqFilter": '{"profiles": .}', + "queue": self.queue, + "keepFullObjectsInMemory": False, + }, + ], } def check_overlaps_cidrs(self, networks: list[IPv4Network]) -> None: """Check for overlapping CIDRs in a list of networks.""" for i, net1 in enumerate(networks): - for net2 in networks[i + 1:]: + for net2 in networks[i + 1 :]: if net1.overlaps(net2): raise ValueError(f"Overlapping CIDRs {net1} and {net2}") - def check_node_addresses_overlap(self, networks: list[IPv4Network], node_addresses: list[IPv4Address]) -> None: + def check_node_addresses_overlap( + self, networks: list[IPv4Network], node_addresses: list[IPv4Address] + ) -> None: """Check if node addresses overlap with any subnet.""" for addr in node_addresses: for net in networks: if addr in net: raise ValueError(f"Node address {addr} overlaps with subnet {net}") + def validate_virtual_images_storage_class(self, vi_settings: dict) -> None: + """Check that the StorageClass's `PersistentVolumeMode` is not the `Filesystem`.""" + for profile in vi_settings["storageProfiles"]: + name = profile.get("metadata", dict()).get("name", "") + if name != "" and name == vi_settings["defaultStorageClassName"]: + claim_property_sets = profile.get("status", dict()).get( + "claimPropertySets", list() + ) + try: + claim_property_set = claim_property_sets[0] + if claim_property_set["volumeMode"] == "Filesystem": + raise ValueError( + f"a `StorageClass` with the `PersistentVolumeFilesystem` mode cannot be used for `VirtualImages` currently: {name}" + ) + except (IndexError, KeyError): + raise ValueError( + f"failed to validate the `PersistentVolumeMode` of the `StorageProfile`: {name}" + ) + for profile in vi_settings["storageProfiles"]: + name = profile.get("metadata", dict()).get("name", "") + allowed_storage_classes = vi_settings["allowedStorageClassSelector"]["matchNames"] + if name != "" and name in allowed_storage_classes: + claim_property_sets = profile.get("status", dict()).get( + "claimPropertySets", list() + ) + try: + claim_property_set = claim_property_sets[0] + if claim_property_set["volumeMode"] == "Filesystem": + raise ValueError( + f"a `StorageClass` with the `PersistentVolumeFilesystem` mode cannot be used for `VirtualImages` currently: {name}" + ) + except (IndexError, KeyError): + raise ValueError( + f"failed to validate the `PersistentVolumeMode` of the `StorageProfile`: {name}" + ) + def reconcile(self) -> Callable[[hook.Context], None]: def r(ctx: hook.Context): cidrs: list[IPv4Network] = [ ip_network(c) - for c in ctx.snapshots.get(self.SNAPSHOT_MODULE_CONFIG, [{}])[0] - .get("filterResult", {}) - .get("cidrs", []) + for c in ctx.snapshots.get(self.SNAPSHOT_MODULE_CONFIG, [dict()])[0] + .get("filterResult", dict()) + .get("settings", dict()) + .get("virtualMachineCIDRs", list()) ] self.check_overlaps_cidrs(cidrs) @@ -96,6 +146,35 @@ def r(ctx: hook.Context): ] self.check_node_addresses_overlap(cidrs, node_addresses) + vi_default_storage_class: str = ( + ctx.snapshots.get(self.SNAPSHOT_MODULE_CONFIG, [dict()])[0] + .get("filterResult", dict()) + .get("settings", dict()) + .get("virtualImages", dict()) + .get("defaultStorageClassName", "") + ) + storage_profiles: list[dict] = [ + profile.get("filterResult", dict()).get("profiles", dict()) + for profile in ctx.snapshots.get(self.SNAPSHOT_STORAGE_PROFILES, list()) + ] + vi_allowedStorageClassSelector: list[str] = [ + sc + for sc in ctx.snapshots.get(self.SNAPSHOT_MODULE_CONFIG, [dict()])[0] + .get("filterResult", dict()) + .get("settings", dict()) + .get("virtualImages", dict()) + .get("allowedStorageClassSelector", dict()) + .get("matchNames", list()) + ] + vi_settings: dict[str, any] = { + "defaultStorageClassName": vi_default_storage_class, + "allowedStorageClassSelector": { + "matchNames": vi_allowedStorageClassSelector + }, + "storageProfiles": storage_profiles, + } + self.validate_virtual_images_storage_class(vi_settings) + return r diff --git a/images/virtualization-artifact/pkg/controller/moduleconfig/moduleconfig_webhook.go b/images/virtualization-artifact/pkg/controller/moduleconfig/moduleconfig_webhook.go index 82514cad57..39c23c00bd 100644 --- a/images/virtualization-artifact/pkg/controller/moduleconfig/moduleconfig_webhook.go +++ b/images/virtualization-artifact/pkg/controller/moduleconfig/moduleconfig_webhook.go @@ -46,6 +46,7 @@ func NewModuleConfigValidator(client client.Client) *validator.Validator[*mcapi. cidrs := newCIDRsValidator(client) reduceCIDRs := newRemoveCIDRsValidator(client) + viStorageClasses := newViStorageClassValidator(client) return validator.NewValidator[*mcapi.ModuleConfig](logger). WithPredicate(&validator.Predicate[*mcapi.ModuleConfig]{ @@ -54,5 +55,5 @@ func NewModuleConfigValidator(client client.Client) *validator.Validator[*mcapi. oldMC.GetGeneration() != newMC.GetGeneration() }, }). - WithUpdateValidators(cidrs, reduceCIDRs) + WithUpdateValidators(cidrs, reduceCIDRs, viStorageClasses) } diff --git a/images/virtualization-artifact/pkg/controller/moduleconfig/util.go b/images/virtualization-artifact/pkg/controller/moduleconfig/util.go index fbaabc791f..6187ba2f1a 100644 --- a/images/virtualization-artifact/pkg/controller/moduleconfig/util.go +++ b/images/virtualization-artifact/pkg/controller/moduleconfig/util.go @@ -91,3 +91,35 @@ func convertToStringSlice(input []interface{}) ([]string, error) { } return result, nil } + +type viStorageClassSettings struct { + DefaultStorageClassName string + AllowedStorageClassSelector AllowedStorageClassSelector +} + +type AllowedStorageClassSelector struct { + MatchNames []string +} + +func parseViStorageClass(settings mcapi.SettingsValues) *viStorageClassSettings { + viScSettings := &viStorageClassSettings{} + if virtualImages, ok := settings["virtualImages"].(map[string]interface{}); ok { + if defaultClass, ok := virtualImages["defaultStorageClassName"].(string); ok { + viScSettings.DefaultStorageClassName = defaultClass + } + + if allowedSelector, ok := virtualImages["allowedStorageClassSelector"].(map[string]interface{}); ok { + if matchNames, ok := allowedSelector["matchNames"].([]interface{}); ok { + var matchNameStrings []string + for _, name := range matchNames { + if strName, ok := name.(string); ok { + matchNameStrings = append(matchNameStrings, strName) + } + } + viScSettings.AllowedStorageClassSelector.MatchNames = matchNameStrings + } + } + } + + return viScSettings +} diff --git a/images/virtualization-artifact/pkg/controller/moduleconfig/vi_storageclass_validator.go b/images/virtualization-artifact/pkg/controller/moduleconfig/vi_storageclass_validator.go new file mode 100644 index 0000000000..6bd47e4224 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/moduleconfig/vi_storageclass_validator.go @@ -0,0 +1,84 @@ +/* +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 moduleconfig + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + mcapi "github.com/deckhouse/virtualization-controller/pkg/controller/moduleconfig/api" +) + +type viStorageClassValidator struct { + client client.Client +} + +func newViStorageClassValidator(client client.Client) *viStorageClassValidator { + return &viStorageClassValidator{ + client: client, + } +} + +func (v viStorageClassValidator) ValidateUpdate(ctx context.Context, _, newMC *mcapi.ModuleConfig) (admission.Warnings, error) { + warnings := make([]string, 0) + + viScSettings := parseViStorageClass(newMC.Spec.Settings) + if viScSettings.DefaultStorageClassName != "" { + scWarnings, err := v.validateStorageClass(ctx, viScSettings.DefaultStorageClassName) + if err != nil { + return warnings, err + } + if len(scWarnings) != 0 { + warnings = append(warnings, scWarnings...) + } + } + + if len(viScSettings.AllowedStorageClassSelector.MatchNames) != 0 { + for _, sc := range viScSettings.AllowedStorageClassSelector.MatchNames { + scWarnings, err := v.validateStorageClass(ctx, sc) + if err != nil { + return warnings, err + } + if len(scWarnings) != 0 { + warnings = append(warnings, scWarnings...) + } + } + } + + return admission.Warnings{}, nil +} + +func (v viStorageClassValidator) validateStorageClass(ctx context.Context, scName string) (admission.Warnings, error) { + scProfile := &cdiv1.StorageProfile{} + err := v.client.Get(ctx, client.ObjectKey{Name: scName}, scProfile, &client.GetOptions{}) + if err != nil { + return admission.Warnings{}, fmt.Errorf("failed to obtain the `StorageProfile` %s: %w", scName, err) + } + if len(scProfile.Status.ClaimPropertySets) == 0 { + return admission.Warnings{}, fmt.Errorf("failed to validate the `PersistentVolumeMode` of the `StorageProfile`: %s", scName) + } + if *scProfile.Status.ClaimPropertySets[0].VolumeMode == corev1.PersistentVolumeFilesystem { + return admission.Warnings{}, fmt.Errorf("a `StorageClass` with the `PersistentVolumeFilesystem` mode cannot be used for `VirtualImages` currently: %s", scName) + } + + return admission.Warnings{}, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/interfaces.go b/images/virtualization-artifact/pkg/controller/vi/internal/interfaces.go index f7d90c6460..19ed141ba2 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/interfaces.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/interfaces.go @@ -21,6 +21,7 @@ import ( corev1 "k8s.io/api/core/v1" storev1 "k8s.io/api/storage/v1" + cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/source" @@ -37,5 +38,6 @@ type Sources interface { type DiskService interface { GetStorageClass(ctx context.Context, storageClassName *string) (*storev1.StorageClass, error) + GetStorageProfile(ctx context.Context, name string) (*cdiv1.StorageProfile, error) GetPersistentVolumeClaim(ctx context.Context, sup *supplements.Generator) (*corev1.PersistentVolumeClaim, error) } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/mock.go b/images/virtualization-artifact/pkg/controller/vi/internal/mock.go index a7c7a563aa..03fae91a8a 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/mock.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/mock.go @@ -1,5 +1,5 @@ /* -Copyright 2024 Flant JSC +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. @@ -26,6 +26,7 @@ import ( virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" corev1 "k8s.io/api/core/v1" storev1 "k8s.io/api/storage/v1" + cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sync" ) @@ -45,6 +46,9 @@ var _ DiskService = &DiskServiceMock{} // GetStorageClassFunc: func(ctx context.Context, storageClassName *string) (*storev1.StorageClass, error) { // panic("mock out the GetStorageClass method") // }, +// GetStorageProfileFunc: func(ctx context.Context, name string) (*cdiv1.StorageProfile, error) { +// panic("mock out the GetStorageProfile method") +// }, // } // // // use mockedDiskService in code that requires DiskService @@ -58,6 +62,9 @@ type DiskServiceMock struct { // GetStorageClassFunc mocks the GetStorageClass method. GetStorageClassFunc func(ctx context.Context, storageClassName *string) (*storev1.StorageClass, error) + // GetStorageProfileFunc mocks the GetStorageProfile method. + GetStorageProfileFunc func(ctx context.Context, name string) (*cdiv1.StorageProfile, error) + // calls tracks calls to the methods. calls struct { // GetPersistentVolumeClaim holds details about calls to the GetPersistentVolumeClaim method. @@ -74,9 +81,17 @@ type DiskServiceMock struct { // StorageClassName is the storageClassName argument value. StorageClassName *string } + // GetStorageProfile holds details about calls to the GetStorageProfile method. + GetStorageProfile []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Name is the name argument value. + Name string + } } lockGetPersistentVolumeClaim sync.RWMutex lockGetStorageClass sync.RWMutex + lockGetStorageProfile sync.RWMutex } // GetPersistentVolumeClaim calls GetPersistentVolumeClaimFunc. @@ -151,6 +166,42 @@ func (mock *DiskServiceMock) GetStorageClassCalls() []struct { return calls } +// GetStorageProfile calls GetStorageProfileFunc. +func (mock *DiskServiceMock) GetStorageProfile(ctx context.Context, name string) (*cdiv1.StorageProfile, error) { + if mock.GetStorageProfileFunc == nil { + panic("DiskServiceMock.GetStorageProfileFunc: method is nil but DiskService.GetStorageProfile was just called") + } + callInfo := struct { + Ctx context.Context + Name string + }{ + Ctx: ctx, + Name: name, + } + mock.lockGetStorageProfile.Lock() + mock.calls.GetStorageProfile = append(mock.calls.GetStorageProfile, callInfo) + mock.lockGetStorageProfile.Unlock() + return mock.GetStorageProfileFunc(ctx, name) +} + +// GetStorageProfileCalls gets all the calls that were made to GetStorageProfile. +// Check the length with: +// +// len(mockedDiskService.GetStorageProfileCalls()) +func (mock *DiskServiceMock) GetStorageProfileCalls() []struct { + Ctx context.Context + Name string +} { + var calls []struct { + Ctx context.Context + Name string + } + mock.lockGetStorageProfile.RLock() + calls = mock.calls.GetStorageProfile + mock.lockGetStorageProfile.RUnlock() + return calls +} + // Ensure, that SourcesMock does implement Sources. // If this is not the case, regenerate this file with moq. var _ Sources = &SourcesMock{} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/storageclass_ready.go b/images/virtualization-artifact/pkg/controller/vi/internal/storageclass_ready.go index ddabfe5802..9575cfdf91 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/storageclass_ready.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/storageclass_ready.go @@ -96,10 +96,24 @@ func (h StorageClassReadyHandler) Handle(ctx context.Context, vi *virtv2.Virtual switch { case sc != nil: - cb. - Status(metav1.ConditionTrue). - Reason(vicondition.StorageClassReady). - Message("") + scProfile, err := h.service.GetStorageProfile(ctx, sc.Name) + if err != nil { + return reconcile.Result{}, err + } + if len(scProfile.Status.ClaimPropertySets) == 0 { + return reconcile.Result{}, fmt.Errorf("failed to validate the `PersistentVolumeMode` of the `StorageProfile`: %s", sc.Name) + } + if *scProfile.Status.ClaimPropertySets[0].VolumeMode == corev1.PersistentVolumeFilesystem { + cb. + Status(metav1.ConditionFalse). + Reason(vicondition.StorageClassNotReady). + Message("a `StorageClass` with the `PersistentVolumeFilesystem` mode cannot be used for `VirtualImages` currently") + } else { + cb. + Status(metav1.ConditionTrue). + Reason(vicondition.StorageClassReady). + Message("") + } case hasNoStorageClassInSpec: h.recorder.Event( vi, diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/storageclass_ready_test.go b/images/virtualization-artifact/pkg/controller/vi/internal/storageclass_ready_test.go index 3b3bc419c7..47f93c11d9 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/storageclass_ready_test.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/storageclass_ready_test.go @@ -1,5 +1,5 @@ /* -Copyright 2024 Flant JSC +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. @@ -25,6 +25,7 @@ import ( storagev1 "k8s.io/api/storage/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" + cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" @@ -109,6 +110,20 @@ func newDiskServiceMock(existedStorageClass *string) *DiskServiceMock { return nil, nil } + diskServiceMock.GetStorageProfileFunc = func(ctx context.Context, name string) (*cdiv1.StorageProfile, error) { + persistentVolumeBlock := corev1.PersistentVolumeBlock + return &cdiv1.StorageProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: *existedStorageClass, + }, + Status: cdiv1.StorageProfileStatus{ + ClaimPropertySets: []cdiv1.ClaimPropertySet{ + {VolumeMode: &persistentVolumeBlock}, + }, + }, + }, nil + } + diskServiceMock.GetStorageClassFunc = func(ctx context.Context, storageClassName *string) (*storagev1.StorageClass, error) { switch { case existedStorageClass == nil: diff --git a/images/virtualization-artifact/pkg/controller/vi/vi_controller.go b/images/virtualization-artifact/pkg/controller/vi/vi_controller.go index 755d738e26..6a8a350fca 100644 --- a/images/virtualization-artifact/pkg/controller/vi/vi_controller.go +++ b/images/virtualization-artifact/pkg/controller/vi/vi_controller.go @@ -100,7 +100,7 @@ func NewController( if err = builder.WebhookManagedBy(mgr). For(&virtv2.VirtualImage{}). - WithValidator(NewValidator(log)). + WithValidator(NewValidator(log, mgr.GetClient())). Complete(); err != nil { return nil, err } diff --git a/images/virtualization-artifact/pkg/controller/vi/vi_webhook.go b/images/virtualization-artifact/pkg/controller/vi/vi_webhook.go index 375cf4598d..2de069eb7c 100644 --- a/images/virtualization-artifact/pkg/controller/vi/vi_webhook.go +++ b/images/virtualization-artifact/pkg/controller/vi/vi_webhook.go @@ -22,8 +22,12 @@ import ( "reflect" "strings" + corev1 "k8s.io/api/core/v1" + storev1 "k8s.io/api/storage/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" "github.com/deckhouse/deckhouse/pkg/log" @@ -35,18 +39,20 @@ import ( type Validator struct { logger *log.Logger + client client.Client } -func NewValidator(logger *log.Logger) *Validator { +func NewValidator(logger *log.Logger, client client.Client) *Validator { return &Validator{ logger: logger.With("webhook", "validator"), + client: client, } } -func (v *Validator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { vi, ok := obj.(*virtv2.VirtualImage) if !ok { - return nil, fmt.Errorf("expected a new VirtualMachine but got a %T", obj) + return nil, fmt.Errorf("expected a new VirtualImage but got a %T", obj) } if strings.Contains(vi.Name, ".") { @@ -57,18 +63,23 @@ func (v *Validator) ValidateCreate(_ context.Context, obj runtime.Object) (admis return nil, fmt.Errorf("the VirtualImage name %q is too long: it must be no more than %d characters", vi.Name, blockdevice.MaxVirtualImageNameLen) } - if vi.Spec.Storage == virtv2.StorageKubernetes { - warnings := admission.Warnings{ - fmt.Sprintf("Using the `%s` storage type is deprecated. It is recommended to use `%s` instead.", - virtv2.StorageKubernetes, virtv2.StoragePersistentVolumeClaim), - } + warnings, err := v.validateStorageClass(ctx, vi) + if err != nil { + return nil, err + } + if len(warnings) != 0 { return warnings, nil } return nil, nil } -func (v *Validator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + var ( + warnings admission.Warnings = make([]string, 0) + err error + ) + oldVI, ok := oldObj.(*virtv2.VirtualImage) if !ok { return nil, fmt.Errorf("expected an old VirtualImage but got a %T", newObj) @@ -81,12 +92,18 @@ func (v *Validator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Obj v.logger.Info("Validating VirtualImage") - var warnings admission.Warnings - if oldVI.Generation == newVI.Generation { return nil, nil } + scWarnings, err := v.validateStorageClass(ctx, newVI) + if err != nil { + return nil, err + } + if len(warnings) != 0 { + warnings = append(warnings, scWarnings...) + } + ready, _ := conditions.GetCondition(vicondition.ReadyType, newVI.Status.Conditions) if ready.Status == metav1.ConditionTrue || newVI.Status.Phase == virtv2.ImageReady || newVI.Status.Phase == virtv2.ImageLost || newVI.Status.Phase == virtv2.ImageTerminating { if !reflect.DeepEqual(oldVI.Spec.DataSource, newVI.Spec.DataSource) { @@ -99,7 +116,7 @@ func (v *Validator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Obj } if strings.Contains(newVI.Name, ".") { - warnings = append(warnings, fmt.Sprintf(" the VirtualImage name %q is invalid as it contains now forbidden symbol '.', allowed symbols for name are [0-9a-zA-Z-]. Create another image with valid name to avoid problems with future updates.", newVI.Name)) + warnings = append(warnings, fmt.Sprintf("the VirtualImage name %q is invalid as it contains now forbidden symbol '.', allowed symbols for name are [0-9a-zA-Z-]. Create another image with valid name to avoid problems with future updates.", newVI.Name)) } if len(newVI.Name) > blockdevice.MaxVirtualImageNameLen { @@ -114,3 +131,36 @@ func (v *Validator) ValidateDelete(_ context.Context, _ runtime.Object) (admissi v.logger.Error("Ensure the correctness of ValidatingWebhookConfiguration", "err", err) return nil, nil } + +func (v *Validator) validateStorageClass(ctx context.Context, vi *virtv2.VirtualImage) (admission.Warnings, error) { + if vi.Spec.PersistentVolumeClaim != (virtv2.VirtualImagePersistentVolumeClaim{}) { + switch vi.Spec.Storage { + case virtv2.StoragePersistentVolumeClaim: + scFromSpec := vi.Spec.PersistentVolumeClaim.StorageClass + sc := &storev1.StorageClass{} + err := v.client.Get(ctx, client.ObjectKey{Name: *scFromSpec}, sc, &client.GetOptions{}) + if err != nil { + return admission.Warnings{}, fmt.Errorf("failed to obtain the `StorageClass` %s", *scFromSpec) + } + scProfile := &cdiv1.StorageProfile{} + err = v.client.Get(ctx, client.ObjectKey{Name: sc.Name}, scProfile, &client.GetOptions{}) + if err != nil { + return admission.Warnings{}, fmt.Errorf("failed to obtain the `StorageProfile` %s", *scFromSpec) + } + if len(scProfile.Status.ClaimPropertySets) == 0 { + return admission.Warnings{}, fmt.Errorf("failed to validate the `PersistentVolumeMode` of the `StorageProfile`: %s", sc.Name) + } + if *scProfile.Status.ClaimPropertySets[0].VolumeMode == corev1.PersistentVolumeFilesystem { + return admission.Warnings{}, fmt.Errorf("a `StorageClass` with the `PersistentVolumeFilesystem` mode cannot be used currently") + } + case virtv2.StorageKubernetes: + warnings := admission.Warnings{ + fmt.Sprintf("Using the `%s` storage type is deprecated. It is recommended to use `%s` instead.", + virtv2.StorageKubernetes, virtv2.StoragePersistentVolumeClaim), + } + return warnings, nil + } + } + + return admission.Warnings{}, nil +} diff --git a/werf.yaml b/werf.yaml index 8eabf7c21c..36a669e6e6 100644 --- a/werf.yaml +++ b/werf.yaml @@ -129,7 +129,7 @@ shell: --- image: python-dependencies from: {{ .Images.BASE_ALT_P11 }} -fromCacheVersion: "2024-11-07.1" +fromCacheVersion: "2025-03-14.3" git: - add: /lib/python/requirements.txt to: /requirements.txt