diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/validator/storage_class_match_validator.go b/images/virtualization-artifact/pkg/controller/vd/internal/validator/storage_class_match_validator.go new file mode 100644 index 0000000000..98daa1ba5f --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vd/internal/validator/storage_class_match_validator.go @@ -0,0 +1,109 @@ +/* +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" + "fmt" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/deckhouse/virtualization-controller/pkg/common/object" + intsvc "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/service" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type StorageClassMatchValidator struct { + client client.Client + scService *intsvc.VirtualDiskStorageClassService +} + +func NewStorageClassMatchValidator(client client.Client, scService *intsvc.VirtualDiskStorageClassService) *StorageClassMatchValidator { + return &StorageClassMatchValidator{client: client, scService: scService} +} + +func (v *StorageClassMatchValidator) ValidateCreate(ctx context.Context, vd *v1alpha2.VirtualDisk) (admission.Warnings, error) { + return v.Validate(ctx, vd) +} + +func (v *StorageClassMatchValidator) ValidateUpdate(ctx context.Context, _, newVD *v1alpha2.VirtualDisk) (admission.Warnings, error) { + if newVD.Status.Phase == v1alpha2.DiskReady { + return nil, nil + } + + return v.Validate(ctx, newVD) +} + +func (v *StorageClassMatchValidator) Validate(ctx context.Context, vd *v1alpha2.VirtualDisk) (admission.Warnings, error) { + if vd.Spec.DataSource == nil || vd.Spec.DataSource.Type != v1alpha2.DataSourceTypeObjectRef || vd.Spec.DataSource.ObjectRef == nil { + return nil, nil + } + + if vd.Spec.DataSource.ObjectRef.Kind != v1alpha2.VirtualDiskObjectRefKindVirtualImage { + return nil, nil + } + + vi, err := object.FetchObject(ctx, types.NamespacedName{ + Name: vd.Spec.DataSource.ObjectRef.Name, + Namespace: vd.Namespace, + }, v.client, &v1alpha2.VirtualImage{}) + if err != nil { + return nil, fmt.Errorf("failed to fetch VirtualImage %q: %w", vd.Spec.DataSource.ObjectRef.Name, err) + } + + if vi == nil { + return nil, nil + } + + if vi.Spec.Storage != v1alpha2.StoragePersistentVolumeClaim && vi.Spec.Storage != v1alpha2.StorageKubernetes { + return nil, nil + } + + defaultStorageClass, err := v.scService.GetDefaultStorageClass(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get default storage class: %w", err) + } + + var vdStorageClass string + if vd.Spec.PersistentVolumeClaim.StorageClass != nil && *vd.Spec.PersistentVolumeClaim.StorageClass != "" { + vdStorageClass = *vd.Spec.PersistentVolumeClaim.StorageClass + } else { + vdStorageClass = defaultStorageClass.Name + } + + var viStorageClass string + switch { + case vi.Status.StorageClassName != "": + viStorageClass = vi.Status.StorageClassName + case vi.Spec.PersistentVolumeClaim.StorageClass != nil && *vi.Spec.PersistentVolumeClaim.StorageClass != "": + viStorageClass = *vi.Spec.PersistentVolumeClaim.StorageClass + default: + viStorageClass = defaultStorageClass.Name // if VI only created and not ready yet, it will use default StorageClass + } + + if vdStorageClass != viStorageClass { + return nil, fmt.Errorf( + "cannot create VirtualDisk from VirtualImage %q with different storage classes: "+ + "VirtualImage uses %q, VirtualDisk specifies %q", + vi.Name, viStorageClass, vdStorageClass, + ) + } + + return nil, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vd/vd_webhook.go b/images/virtualization-artifact/pkg/controller/vd/vd_webhook.go index 1b6ea816f6..f6d11e00ae 100644 --- a/images/virtualization-artifact/pkg/controller/vd/vd_webhook.go +++ b/images/virtualization-artifact/pkg/controller/vd/vd_webhook.go @@ -46,6 +46,7 @@ func NewValidator(client client.Client, scService *intsvc.VirtualDiskStorageClas validator.NewSpecChangesValidator(client, scService), validator.NewISOSourceValidator(client), validator.NewNameValidator(), + validator.NewStorageClassMatchValidator(client, scService), }, } } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/validator/storage_class_match_validator.go b/images/virtualization-artifact/pkg/controller/vi/internal/validator/storage_class_match_validator.go new file mode 100644 index 0000000000..692f5978be --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/validator/storage_class_match_validator.go @@ -0,0 +1,109 @@ +/* +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" + "fmt" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/deckhouse/virtualization-controller/pkg/common/object" + intsvc "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/service" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type StorageClassMatchValidator struct { + client client.Client + scService *intsvc.VirtualImageStorageClassService +} + +func NewStorageClassMatchValidator(client client.Client, scService *intsvc.VirtualImageStorageClassService) *StorageClassMatchValidator { + return &StorageClassMatchValidator{client: client, scService: scService} +} + +func (v *StorageClassMatchValidator) ValidateCreate(ctx context.Context, vi *v1alpha2.VirtualImage) (admission.Warnings, error) { + return v.Validate(ctx, vi) +} + +func (v *StorageClassMatchValidator) ValidateUpdate(ctx context.Context, _, newVI *v1alpha2.VirtualImage) (admission.Warnings, error) { + if newVI.Status.Phase == v1alpha2.ImageReady { + return nil, nil + } + + return v.Validate(ctx, newVI) +} + +func (v *StorageClassMatchValidator) Validate(ctx context.Context, vi *v1alpha2.VirtualImage) (admission.Warnings, error) { + if vi.Spec.Storage != v1alpha2.StoragePersistentVolumeClaim && vi.Spec.Storage != v1alpha2.StorageKubernetes { + return nil, nil + } + + if vi.Spec.DataSource.Type != v1alpha2.DataSourceTypeObjectRef || vi.Spec.DataSource.ObjectRef == nil { + return nil, nil + } + + if vi.Spec.DataSource.ObjectRef.Kind != v1alpha2.VirtualImageObjectRefKindVirtualDisk { + return nil, nil + } + + vd, err := object.FetchObject(ctx, types.NamespacedName{ + Name: vi.Spec.DataSource.ObjectRef.Name, + Namespace: vi.Namespace, + }, v.client, &v1alpha2.VirtualDisk{}) + if err != nil { + return nil, fmt.Errorf("failed to fetch VirtualDisk %q: %w", vi.Spec.DataSource.ObjectRef.Name, err) + } + + if vd == nil { + return nil, nil + } + + defaultStorageClass, err := v.scService.GetDefaultStorageClass(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get default storage class: %w", err) + } + + var viStorageClass string + if vi.Spec.PersistentVolumeClaim.StorageClass != nil && *vi.Spec.PersistentVolumeClaim.StorageClass != "" { + viStorageClass = *vi.Spec.PersistentVolumeClaim.StorageClass + } else { + viStorageClass = defaultStorageClass.Name + } + + var vdStorageClass string + switch { + case vd.Status.StorageClassName != "": + vdStorageClass = vd.Status.StorageClassName + case vd.Spec.PersistentVolumeClaim.StorageClass != nil && *vd.Spec.PersistentVolumeClaim.StorageClass != "": + vdStorageClass = *vd.Spec.PersistentVolumeClaim.StorageClass + default: + vdStorageClass = defaultStorageClass.Name // if VD only created and not ready yet, it will use default StorageClass + } + + if viStorageClass != vdStorageClass { + return nil, fmt.Errorf( + "cannot create VirtualImage from VirtualDisk %q with different storage classes: "+ + "VirtualDisk uses %q, VirtualImage specifies %q", + vd.Name, vdStorageClass, viStorageClass, + ) + } + + return nil, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vi/vi_webhook.go b/images/virtualization-artifact/pkg/controller/vi/vi_webhook.go index 63702c1de8..8c9f8d8db2 100644 --- a/images/virtualization-artifact/pkg/controller/vi/vi_webhook.go +++ b/images/virtualization-artifact/pkg/controller/vi/vi_webhook.go @@ -33,21 +33,24 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/common/validate" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" intsvc "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/validator" "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" ) type Validator struct { - logger *log.Logger - client client.Client - scService *intsvc.VirtualImageStorageClassService + logger *log.Logger + client client.Client + scService *intsvc.VirtualImageStorageClassService + storageClassMatchValidator *validator.StorageClassMatchValidator } func NewValidator(logger *log.Logger, client client.Client, scService *intsvc.VirtualImageStorageClassService) *Validator { return &Validator{ - logger: logger.With("webhook", "validator"), - client: client, - scService: scService, + logger: logger.With("webhook", "validator"), + client: client, + scService: scService, + storageClassMatchValidator: validator.NewStorageClassMatchValidator(client, scService), } } @@ -107,6 +110,11 @@ func (v *Validator) ValidateCreate(ctx context.Context, obj runtime.Object) (adm } } + _, err := v.storageClassMatchValidator.ValidateCreate(ctx, vi) + if err != nil { + return nil, err + } + return nil, nil } @@ -187,6 +195,11 @@ func (v *Validator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.O warnings = append(warnings, fmt.Sprintf("the VirtualImage name %q is too long: it must be no more than %d characters", newVI.Name, validate.MaxVirtualImageNameLen)) } + _, err := v.storageClassMatchValidator.ValidateUpdate(ctx, oldVI, newVI) + if err != nil { + return warnings, err + } + return warnings, nil }