diff --git a/images/virtualization-artifact/pkg/controller/vmclass/internal/validators/sizing_policies_validator.go b/images/virtualization-artifact/pkg/controller/vmclass/internal/validators/sizing_policies_validator.go new file mode 100644 index 000000000..f89b3aa97 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmclass/internal/validators/sizing_policies_validator.go @@ -0,0 +1,80 @@ +/* +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 validators + +import ( + "context" + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type SizingPoliciesValidator struct { + client client.Client +} + +func NewSizingPoliciesValidator(client client.Client) *SizingPoliciesValidator { + return &SizingPoliciesValidator{client: client} +} + +func (v *SizingPoliciesValidator) ValidateCreate(_ context.Context, vmclass *v1alpha2.VirtualMachineClass) (admission.Warnings, error) { + if HasCpuSizePoliciesCrosses(&vmclass.Spec) { + return nil, fmt.Errorf("vmclass %s has size policy cpu crosses", vmclass.Name) + } + + return nil, nil +} + +func (v *SizingPoliciesValidator) ValidateUpdate(_ context.Context, _, newVMClass *v1alpha2.VirtualMachineClass) (admission.Warnings, error) { + if HasCpuSizePoliciesCrosses(&newVMClass.Spec) { + return nil, fmt.Errorf("vmclass %s has size policy cpu crosses", newVMClass.Name) + } + + return nil, nil +} + +func HasCpuSizePoliciesCrosses(vmclass *v1alpha2.VirtualMachineClassSpec) bool { + usedPairs := make(map[[2]int]bool) + + for i, policy1 := range vmclass.SizingPolicies { + for j, policy2 := range vmclass.SizingPolicies { + if i == j { + continue + } + _, ok := usedPairs[[2]int{i, j}] + if ok { + continue + } + + if policy1.Cores.Min >= policy2.Cores.Min && policy1.Cores.Min <= policy2.Cores.Max { + return true + } + + if policy1.Cores.Max >= policy2.Cores.Min && policy1.Cores.Max <= policy2.Cores.Max { + return true + } + + usedPairs[[2]int{i, j}] = true + usedPairs[[2]int{j, i}] = true + } + } + + return false +} diff --git a/images/virtualization-artifact/pkg/controller/vmclass/internal/validators/validators_suite_test.go b/images/virtualization-artifact/pkg/controller/vmclass/internal/validators/validators_suite_test.go new file mode 100644 index 000000000..fc6a5ea04 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmclass/internal/validators/validators_suite_test.go @@ -0,0 +1,92 @@ +package validators_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + . "github.com/deckhouse/virtualization-controller/pkg/controller/vmclass/internal/validators" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +func TestValidators(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Validators Suite") +} + +var _ = Describe("Spec policies validator", func() { + var vmclass v1alpha2.VirtualMachineClass + + Context("empty vmclass", func() { + BeforeEach(func() { + vmclass = v1alpha2.VirtualMachineClass{} + }) + + It("Should return no problem when empty value", func() { + Expect(HasCpuSizePoliciesCrosses(&vmclass.Spec)).Should(BeFalse()) + }) + }) + + Context("vmclass with no cpu size policies crosses", func() { + BeforeEach(func() { + vmclass = v1alpha2.VirtualMachineClass{} + vmclass.Spec.SizingPolicies = append(vmclass.Spec.SizingPolicies, v1alpha2.SizingPolicy{ + Cores: &v1alpha2.SizingPolicyCores{ + Min: 1, + Max: 4, + Step: 1, + }, + }) + vmclass.Spec.SizingPolicies = append(vmclass.Spec.SizingPolicies, v1alpha2.SizingPolicy{ + Cores: &v1alpha2.SizingPolicyCores{ + Min: 5, + Max: 9, + Step: 1, + }, + }) + vmclass.Spec.SizingPolicies = append(vmclass.Spec.SizingPolicies, v1alpha2.SizingPolicy{ + Cores: &v1alpha2.SizingPolicyCores{ + Min: 10, + Max: 15, + Step: 1, + }, + }) + }) + + It("Should return no problem with correct values", func() { + Expect(HasCpuSizePoliciesCrosses(&vmclass.Spec)).Should(BeFalse()) + }) + }) + + Context("vmclass with cpu size policies crosses", func() { + BeforeEach(func() { + vmclass = v1alpha2.VirtualMachineClass{} + vmclass.Spec.SizingPolicies = append(vmclass.Spec.SizingPolicies, v1alpha2.SizingPolicy{ + Cores: &v1alpha2.SizingPolicyCores{ + Min: 1, + Max: 4, + Step: 1, + }, + }) + vmclass.Spec.SizingPolicies = append(vmclass.Spec.SizingPolicies, v1alpha2.SizingPolicy{ + Cores: &v1alpha2.SizingPolicyCores{ + Min: 4, + Max: 9, + Step: 1, + }, + }) + vmclass.Spec.SizingPolicies = append(vmclass.Spec.SizingPolicies, v1alpha2.SizingPolicy{ + Cores: &v1alpha2.SizingPolicyCores{ + Min: 10, + Max: 15, + Step: 1, + }, + }) + }) + + It("Should return problem with incorrect values", func() { + Expect(HasCpuSizePoliciesCrosses(&vmclass.Spec)).Should(BeTrue()) + }) + }) +}) diff --git a/images/virtualization-artifact/pkg/controller/vmclass/vmclass_controller.go b/images/virtualization-artifact/pkg/controller/vmclass/vmclass_controller.go index 242664e5d..4375a7aaa 100644 --- a/images/virtualization-artifact/pkg/controller/vmclass/vmclass_controller.go +++ b/images/virtualization-artifact/pkg/controller/vmclass/vmclass_controller.go @@ -21,11 +21,13 @@ import ( "log/slog" "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" "github.com/deckhouse/virtualization-controller/pkg/controller/vmclass/internal" "github.com/deckhouse/virtualization-controller/pkg/logger" + "github.com/deckhouse/virtualization/api/core/v1alpha2" ) const ( @@ -61,6 +63,13 @@ func NewController( return nil, err } + if err = builder.WebhookManagedBy(mgr). + For(&v1alpha2.VirtualMachineClass{}). + WithValidator(NewValidator(mgr.GetClient(), log)). + Complete(); err != nil { + return nil, err + } + log.Info("Initialized VirtualMachineClass controller") return c, nil } diff --git a/images/virtualization-artifact/pkg/controller/vmclass/vmclass_webhook.go b/images/virtualization-artifact/pkg/controller/vmclass/vmclass_webhook.go new file mode 100644 index 000000000..c28550dd7 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmclass/vmclass_webhook.go @@ -0,0 +1,95 @@ +/* +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 vmclass + +import ( + "context" + "fmt" + "log/slog" + + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/deckhouse/virtualization-controller/pkg/controller/vmclass/internal/validators" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type VirtualMachineClassValidator interface { + ValidateCreate(ctx context.Context, vm *virtv2.VirtualMachineClass) (admission.Warnings, error) + ValidateUpdate(ctx context.Context, oldVM, newVM *virtv2.VirtualMachineClass) (admission.Warnings, error) +} + +type Validator struct { + validators []VirtualMachineClassValidator + log *slog.Logger +} + +func NewValidator(client client.Client, log *slog.Logger) *Validator { + return &Validator{ + validators: []VirtualMachineClassValidator{ + validators.NewSizingPoliciesValidator(client), + }, + log: log.With("webhook", "validation"), + } +} + +func (v *Validator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + vmclass, ok := obj.(*virtv2.VirtualMachineClass) + if !ok { + return nil, fmt.Errorf("expected a new VirtualMachine but got a %T", obj) + } + + var warnings admission.Warnings + + for _, validator := range v.validators { + warn, err := validator.ValidateCreate(ctx, vmclass) + if err != nil { + return nil, err + } + warnings = append(warnings, warn...) + } + + return warnings, nil +} + +func (v *Validator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + oldVMClass, ok := oldObj.(*virtv2.VirtualMachineClass) + if !ok { + return nil, fmt.Errorf("expected an old VirtualMachineClass but got a %T", oldObj) + } + + newVMClass, ok := newObj.(*virtv2.VirtualMachineClass) + if !ok { + return nil, fmt.Errorf("expected a new VirtualMachineClass but got a %T", newObj) + } + + var warnings admission.Warnings + + for _, validator := range v.validators { + warn, err := validator.ValidateUpdate(ctx, oldVMClass, newVMClass) + if err != nil { + return nil, err + } + warnings = append(warnings, warn...) + } + + return warnings, nil +} + +func (v *Validator) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { + err := fmt.Errorf("misconfigured webhook rules: delete operation not implemented") + v.log.Error("Ensure the correctness of ValidatingWebhookConfiguration", "err", err.Error()) + return nil, nil +} diff --git a/templates/virtualization-controller/validation-webhook.yaml b/templates/virtualization-controller/validation-webhook.yaml index 2efad7d83..2c6035af4 100644 --- a/templates/virtualization-controller/validation-webhook.yaml +++ b/templates/virtualization-controller/validation-webhook.yaml @@ -156,3 +156,20 @@ webhooks: {{ .Values.virtualization.internal.controller.cert.ca }} admissionReviewVersions: ["v1"] sideEffects: None + - name: "vmclass.virtualization-controller.validate.d8-virtualization" + rules: + - apiGroups: ["virtualization.deckhouse.io"] + apiVersions: ["v1alpha2"] + operations: ["CREATE", "UPDATE"] + resources: ["virtualmachineclasses"] + scope: "Cluster" + clientConfig: + service: + namespace: d8-{{ .Chart.Name }} + name: virtualization-controller + path: /validate-virtualization-deckhouse-io-v1alpha2-virtualmachineclass + port: 443 + caBundle: | + {{ .Values.virtualization.internal.controller.cert.ca }} + admissionReviewVersions: ["v1"] + sideEffects: None \ No newline at end of file