/* Copyright 2024 The Kubernetes Authors. 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 validate import ( "context" "sort" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/util/validation/field" ) // MatchFunc is a function that compares two values of the same type, // according to some criteria, and returns true if they match. type MatchFunc[T any] func(T, T) bool // EachSliceVal performs validation on each element of newSlice using the provided validation function. // // For update operations, the match function finds corresponding values in oldSlice for each // value in newSlice. This comparison can be either full or partial (e.g., matching only // specific struct fields that serve as a unique identifier). If match is nil, validation // proceeds without considering old values, and the equiv function is not used. // // For update operations, the equiv function checks if a new value is equivalent to its // corresponding old value, enabling validation ratcheting. If equiv is nil but match is // provided, the match function is assumed to perform full value comparison. // // Note: The slice element type must be non-nilable. func EachSliceVal[T any](ctx context.Context, op operation.Operation, fldPath *field.Path, newSlice, oldSlice []T, match, equiv MatchFunc[T], validator ValidateFunc[*T]) field.ErrorList { var errs field.ErrorList for i, val := range newSlice { var old *T if match != nil && len(oldSlice) > 0 { old = lookup(oldSlice, val, match) } // If the operation is an update, for validation ratcheting, skip re-validating if the old // value exists and either: // 1. The match function provides full comparison (equiv is nil) // 2. The equiv function confirms the values are equivalent (either directly or semantically) // // The equiv function provides equality comparison when match uses partial comparison. if op.Type == operation.Update && old != nil && (equiv == nil || equiv(val, *old)) { continue } errs = append(errs, validator(ctx, op, fldPath.Index(i), &val, old)...) } return errs } // lookup returns a pointer to the first element in the list that matches the // target, according to the provided comparison function, or else nil. func lookup[T any](list []T, target T, match MatchFunc[T]) *T { for i := range list { if match(list[i], target) { return &list[i] } } return nil } // EachMapVal validates each value in newMap using the specified validation // function, passing the corresponding old value from oldMap if the key exists in oldMap. // For update operations, it implements validation ratcheting by skipping validation // when the old value exists and the equiv function confirms the values are equivalent. // The value-type of the map is assumed to not be nilable. // If equiv is nil, value-based ratcheting is disabled and all values will be validated. func EachMapVal[K ~string, V any](ctx context.Context, op operation.Operation, fldPath *field.Path, newMap, oldMap map[K]V, equiv MatchFunc[V], validator ValidateFunc[*V]) field.ErrorList { var errs field.ErrorList for key, val := range newMap { var old *V if o, found := oldMap[key]; found { old = &o } // If the operation is an update, for validation ratcheting, skip re-validating if the old // value is found and the equiv function confirms the values are equivalent. if op.Type == operation.Update && old != nil && equiv != nil && equiv(val, *old) { continue } errs = append(errs, validator(ctx, op, fldPath.Key(string(key)), &val, old)...) } return errs } // EachMapKey validates each element of newMap with the specified // validation function. func EachMapKey[K ~string, T any](ctx context.Context, op operation.Operation, fldPath *field.Path, newMap, oldMap map[K]T, validator ValidateFunc[*K]) field.ErrorList { var errs field.ErrorList for key := range newMap { var old *K if _, found := oldMap[key]; found { old = &key } // If the operation is an update, for validation ratcheting, skip re-validating if // the key is found in oldMap. if op.Type == operation.Update && old != nil { continue } // Note: the field path is the field, not the key. errs = append(errs, validator(ctx, op, fldPath, &key, nil)...) } return errs } // Unique verifies that each element of newSlice is unique, according to the // match function. It compares every element of the slice with every other // element and returns errors for non-unique items. func Unique[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, newSlice, _ []T, match MatchFunc[T]) field.ErrorList { var dups []int for i, val := range newSlice { for j := i + 1; j < len(newSlice); j++ { other := newSlice[j] if match(val, other) { if dups == nil { dups = make([]int, 0, len(newSlice)) } if lookup(dups, j, func(a, b int) bool { return a == b }) == nil { dups = append(dups, j) } } } } var errs field.ErrorList sort.Ints(dups) for _, i := range dups { var val any = newSlice[i] // TODO: we don't want the whole item to be logged in the error, just // the key(s). Unfortunately, the way errors are rendered, it comes out // as something like "map[string]any{...}" which is not very nice. Once // that is fixed, we can consider adding a way for this function to // specify that just the keys should be rendered in the error. errs = append(errs, field.Duplicate(fldPath.Index(i), val)) } return errs } // SemanticDeepEqual is a MatchFunc that uses equality.Semantic.DeepEqual to // compare two values. // This wrapper is needed because MatchFunc requires a function that takes two // arguments of specific type T, while equality.Semantic.DeepEqual takes // arguments of type interface{}/any. The wrapper satisfies the type // constraints of MatchFunc while leveraging the underlying semantic equality // logic. It can be used by any other function that needs to call DeepEqual. func SemanticDeepEqual[T any](a, b T) bool { return equality.Semantic.DeepEqual(a, b) } // DirectEqual is a MatchFunc that uses the == operator to compare two values. // It can be used by any other function that needs to compare two values // directly. func DirectEqual[T comparable](a, b T) bool { return a == b } // DirectEqualPtr is a MatchFunc that dereferences two pointers and uses the == // operator to compare the values. If both pointers are nil, it returns true. // If one pointer is nil and the other is not, it returns false. // It can be used by any other function that needs to compare two pointees // directly. func DirectEqualPtr[T comparable](a, b *T) bool { if a == b { return true } if a == nil || b == nil { return false } return *a == *b }