diff --git a/images/virtualization-artifact/pkg/controller/moduleconfig/cidrs_validator.go b/images/virtualization-artifact/pkg/controller/moduleconfig/cidrs_validator.go new file mode 100644 index 000000000..dc166aca6 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/moduleconfig/cidrs_validator.go @@ -0,0 +1,75 @@ +/* +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 moduleconfig + +import ( + "context" + "fmt" + "net/netip" + + corev1 "k8s.io/api/core/v1" + "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 cidrsValidator struct { + client client.Client +} + +func newCIDRsValidator(client client.Client) *cidrsValidator { + return &cidrsValidator{ + client: client, + } +} + +func (v cidrsValidator) ValidateCreate(ctx context.Context, mc *mcapi.ModuleConfig) (admission.Warnings, error) { + return v.validate(ctx, mc) +} + +func (v cidrsValidator) ValidateUpdate(ctx context.Context, _, newMC *mcapi.ModuleConfig) (admission.Warnings, error) { + return v.validate(ctx, newMC) +} + +func (v cidrsValidator) validate(ctx context.Context, mc *mcapi.ModuleConfig) (admission.Warnings, error) { + CIDRs, err := parseCIDRs(mc.Spec.Settings) + if err != nil { + return admission.Warnings{}, err + } + + err = checkOverlapsCIDRs(CIDRs) + if err != nil { + return admission.Warnings{}, err + } + + err = v.checkNodeSubnets(ctx, CIDRs) + if err != nil { + return admission.Warnings{}, err + } + + return admission.Warnings{}, nil +} + +func (v cidrsValidator) checkNodeSubnets(ctx context.Context, excludedPrefixes []netip.Prefix) error { + nodes := &corev1.NodeList{} + err := v.client.List(ctx, nodes) + if err != nil { + return fmt.Errorf("error listing nodes: %w", err) + } + return checkNodeAddressesOverlap(nodes.Items, excludedPrefixes) +} diff --git a/images/virtualization-artifact/pkg/controller/moduleconfig/moduleconfig_webhook.go b/images/virtualization-artifact/pkg/controller/moduleconfig/moduleconfig_webhook.go index b40cd2233..0d8c35736 100644 --- a/images/virtualization-artifact/pkg/controller/moduleconfig/moduleconfig_webhook.go +++ b/images/virtualization-artifact/pkg/controller/moduleconfig/moduleconfig_webhook.go @@ -44,10 +44,20 @@ func SetupWebhookWithManager(mgr manager.Manager) error { func NewModuleConfigValidator(client client.Client) *validator.Validator[*mcapi.ModuleConfig] { lg := log.Default().With(slog.String("validator", "moduleconfig")) - v := newReduceCIDRsValidator(client) + reduceCIDRs := newReduceCIDRsValidator(client) + cidrs := newCIDRsValidator(client) + return validator.NewValidator[*mcapi.ModuleConfig](lg). - WithPredicates(func(mc *mcapi.ModuleConfig) bool { - return mc.GetName() == moduleConfigName + WithPredicate(&validator.Predicate[*mcapi.ModuleConfig]{ + Create: func(mc *mcapi.ModuleConfig) bool { + return mc.GetName() == moduleConfigName + }, + Update: func(oldMC, newMC *mcapi.ModuleConfig) bool { + return newMC.GetName() == moduleConfigName && + oldMC.GetGeneration() != newMC.GetGeneration() + }, }). - WithUpdateValidators(v) + WithUpdateValidators(reduceCIDRs). + WithCreateValidators(cidrs). + WithUpdateValidators(cidrs) } diff --git a/images/virtualization-artifact/pkg/controller/moduleconfig/reduce_cidrs_validator.go b/images/virtualization-artifact/pkg/controller/moduleconfig/reduce_cidrs_validator.go index d03c5fcbf..c6cc60379 100644 --- a/images/virtualization-artifact/pkg/controller/moduleconfig/reduce_cidrs_validator.go +++ b/images/virtualization-artifact/pkg/controller/moduleconfig/reduce_cidrs_validator.go @@ -1,3 +1,19 @@ +/* +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 moduleconfig import ( @@ -25,23 +41,33 @@ func newReduceCIDRsValidator(client client.Client) *reduceCIDRsValidator { } } -func (v *reduceCIDRsValidator) ValidateUpdate(ctx context.Context, oldMC, newMC *mcapi.ModuleConfig) (admission.Warnings, error) { - oldCIDRs := oldMC.Spec.Settings[virtualMachineCIDRs].([]string) - newCIDRs := newMC.Spec.Settings[virtualMachineCIDRs].([]string) +func (v reduceCIDRsValidator) ValidateUpdate(ctx context.Context, oldMC, newMC *mcapi.ModuleConfig) (admission.Warnings, error) { + if oldMC.GetGeneration() == newMC.GetGeneration() { + return admission.Warnings{}, nil + } + + oldCIDRs, err := parseCIDRs(oldMC.Spec.Settings) + if err != nil { + return admission.Warnings{}, err + } + newCIDRs, err := parseCIDRs(newMC.Spec.Settings) + if err != nil { + return admission.Warnings{}, err + } - var validateCIDRs []string + var validateCIDRs []netip.Prefix loop: for _, oldCIDR := range oldCIDRs { for _, newCIDR := range newCIDRs { - if oldCIDR == newCIDR { + if isEqualCIDRs(oldCIDR, newCIDR) { continue loop } } validateCIDRs = append(validateCIDRs, oldCIDR) } - if len(validateCIDRs) != 0 { + if len(validateCIDRs) == 0 { return nil, nil } @@ -50,21 +76,12 @@ loop: return nil, fmt.Errorf("failed to list VirtualMachineIPAddressLeases: %w", err) } - parseCIDRs := make([]netip.Prefix, len(validateCIDRs)) - for i, validateCIDR := range validateCIDRs { - parsedCIDR, err := netip.ParsePrefix(validateCIDR) - if err != nil { - return nil, fmt.Errorf("failed to parse CIDR %s: %w", validateCIDR, err) - } - parseCIDRs[i] = parsedCIDR - } - for _, lease := range leases.Items { leaseIP, err := netip.ParseAddr(ip.LeaseNameToIP(lease.Name)) if err != nil { return nil, fmt.Errorf("failed to parse lease ip: %w", err) } - for _, CIDR := range parseCIDRs { + for _, CIDR := range validateCIDRs { if CIDR.Contains(leaseIP) { return nil, fmt.Errorf("CIDR %q is in use by one or more IP addresses", CIDR) } diff --git a/images/virtualization-artifact/pkg/controller/moduleconfig/util.go b/images/virtualization-artifact/pkg/controller/moduleconfig/util.go new file mode 100644 index 000000000..3cefedf6f --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/moduleconfig/util.go @@ -0,0 +1,90 @@ +/* +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 moduleconfig + +import ( + "fmt" + "net/netip" + + corev1 "k8s.io/api/core/v1" + + mcapi "github.com/deckhouse/virtualization-controller/pkg/controller/moduleconfig/api" +) + +func isEqualCIDRs(a, b netip.Prefix) bool { + return a.Addr() == b.Addr() && a.Bits() == b.Bits() +} + +func checkOverlapsCIDRs(prefixes []netip.Prefix) error { + for i := 0; i < len(prefixes); i++ { + for j := i + 1; j < len(prefixes); j++ { + if prefixes[i].Overlaps(prefixes[j]) { + return fmt.Errorf("overlapping CIDRs %v and %v", prefixes[i], prefixes[j]) + } + } + } + return nil +} + +func checkNodeAddressesOverlap(nodes []corev1.Node, excludedPrefixes []netip.Prefix) error { + for _, node := range nodes { + for _, address := range node.Status.Addresses { + if address.Type == corev1.NodeInternalIP || address.Type == corev1.NodeExternalIP { + ip, err := netip.ParseAddr(address.Address) + if err != nil { + return fmt.Errorf("error parsing node IP: %w", err) + } + + for _, prefix := range excludedPrefixes { + if prefix.Contains(ip) { + return fmt.Errorf("node address %s may overlap with subnet %s", ip, prefix) + } + } + } + } + } + return nil +} + +func parseCIDRs(settings mcapi.SettingsValues) ([]netip.Prefix, error) { + raw := settings[virtualMachineCIDRs].([]interface{}) + CIDRs, err := convertToStringSlice(raw) + if err != nil { + return nil, err + } + parseCIDRs := make([]netip.Prefix, len(CIDRs)) + for i, validateCIDR := range CIDRs { + parsedCIDR, err := netip.ParsePrefix(validateCIDR) + if err != nil { + return nil, fmt.Errorf("failed to parse CIDR %s: %w", validateCIDR, err) + } + parseCIDRs[i] = parsedCIDR + } + return parseCIDRs, nil +} + +func convertToStringSlice(input []interface{}) ([]string, error) { + result := make([]string, len(input)) + for i, v := range input { + strVal, ok := v.(string) + if !ok { + return nil, fmt.Errorf("failed to convert value %v to a string", v) + } + result[i] = strVal + } + return result, nil +} diff --git a/images/virtualization-artifact/pkg/controller/validator/validator.go b/images/virtualization-artifact/pkg/controller/validator/validator.go index 21ddfb8dc..c49ded586 100644 --- a/images/virtualization-artifact/pkg/controller/validator/validator.go +++ b/images/virtualization-artifact/pkg/controller/validator/validator.go @@ -1,3 +1,19 @@ +/* +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 ( @@ -24,7 +40,17 @@ type DeleteValidator[T client.Object] interface { ValidateDelete(ctx context.Context, obj T) (admission.Warnings, error) } -type Predicate[T client.Object] func(obj T) bool +type ( + PredicateCreateFunc[T client.Object] func(obj T) bool + PredicateUpdateFunc[T client.Object] func(oldObj, newObj T) bool + PredicateDeleteFunc[T client.Object] func(obj T) bool +) + +type Predicate[T client.Object] struct { + Create PredicateCreateFunc[T] + Update PredicateUpdateFunc[T] + Delete PredicateDeleteFunc[T] +} var _ admission.CustomValidator = &Validator[client.Object]{} @@ -33,7 +59,7 @@ type Validator[T client.Object] struct { update []UpdateValidator[T] delete []DeleteValidator[T] - predicates []Predicate[T] + predicate *Predicate[T] log *log.Logger } @@ -57,8 +83,8 @@ func (v *Validator[T]) WithDeleteValidators(validators ...DeleteValidator[T]) *V return v } -func (v *Validator[T]) WithPredicates(predicates ...Predicate[T]) *Validator[T] { - v.predicates = append(v.predicates, predicates...) +func (v *Validator[T]) WithPredicate(predicate *Predicate[T]) *Validator[T] { + v.predicate = predicate return v } @@ -73,7 +99,7 @@ func (v *Validator[T]) ValidateCreate(ctx context.Context, obj runtime.Object) ( if err != nil { return nil, err } - if !v.needValidate(o) { + if !v.needCreateValidate(o) { return nil, nil } @@ -105,7 +131,7 @@ func (v *Validator[T]) ValidateUpdate(ctx context.Context, oldObj, newObj runtim if err != nil { return nil, err } - if !v.needValidate(newO) { + if !v.needUpdateValidate(oldO, newO) { return nil, nil } var warnings admission.Warnings @@ -132,7 +158,7 @@ func (v *Validator[T]) ValidateDelete(ctx context.Context, obj runtime.Object) ( if err != nil { return nil, err } - if !v.needValidate(o) { + if !v.needDeleteValidate(o) { return nil, nil } @@ -158,11 +184,23 @@ func (v *Validator[T]) newObject(obj runtime.Object) (T, error) { return newObj, nil } -func (v *Validator[T]) needValidate(obj T) bool { - for _, predicate := range v.predicates { - if !predicate(obj) { - return false - } +func (v *Validator[T]) needCreateValidate(obj T) bool { + if v.predicate != nil && v.predicate.Create != nil { + return v.predicate.Create(obj) + } + return true +} + +func (v *Validator[T]) needUpdateValidate(oldObj, newObj T) bool { + if v.predicate != nil && v.predicate.Update != nil { + return v.predicate.Update(oldObj, newObj) + } + return true +} + +func (v *Validator[T]) needDeleteValidate(obj T) bool { + if v.predicate != nil && v.predicate.Delete != nil { + return v.predicate.Delete(obj) } return true }