k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/apis/resource/structured/namedresources/validation/validation.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package validation
    18  
    19  import (
    20  	"fmt"
    21  	"regexp"
    22  
    23  	"k8s.io/apimachinery/pkg/util/sets"
    24  	"k8s.io/apimachinery/pkg/util/validation/field"
    25  	"k8s.io/apiserver/pkg/cel"
    26  	"k8s.io/apiserver/pkg/cel/environment"
    27  	namedresourcescel "k8s.io/dynamic-resource-allocation/structured/namedresources/cel"
    28  	corevalidation "k8s.io/kubernetes/pkg/apis/core/validation"
    29  	"k8s.io/kubernetes/pkg/apis/resource"
    30  )
    31  
    32  var (
    33  	validateInstanceName  = corevalidation.ValidateDNS1123Subdomain
    34  	validateAttributeName = corevalidation.ValidateDNS1123Subdomain
    35  )
    36  
    37  type Options struct {
    38  	// StoredExpressions must be true if and only if validating CEL
    39  	// expressions that were already stored persistently. This makes
    40  	// validation more permissive by enabling CEL definitions that are not
    41  	// valid yet for new expressions.
    42  	StoredExpressions bool
    43  }
    44  
    45  func ValidateResources(resources *resource.NamedResourcesResources, fldPath *field.Path) field.ErrorList {
    46  	allErrs := validateInstances(resources.Instances, fldPath.Child("instances"))
    47  	return allErrs
    48  }
    49  
    50  func validateInstances(instances []resource.NamedResourcesInstance, fldPath *field.Path) field.ErrorList {
    51  	var allErrs field.ErrorList
    52  	instanceNames := sets.New[string]()
    53  	for i, instance := range instances {
    54  		idxPath := fldPath.Index(i)
    55  		instanceName := instance.Name
    56  		allErrs = append(allErrs, validateInstanceName(instanceName, idxPath.Child("name"))...)
    57  		if instanceNames.Has(instanceName) {
    58  			allErrs = append(allErrs, field.Duplicate(idxPath.Child("name"), instanceName))
    59  		} else {
    60  			instanceNames.Insert(instanceName)
    61  		}
    62  		allErrs = append(allErrs, validateAttributes(instance.Attributes, idxPath.Child("attributes"))...)
    63  	}
    64  	return allErrs
    65  }
    66  
    67  var (
    68  	numericIdentifier = `(0|[1-9]\d*)`
    69  
    70  	preReleaseIdentifier = `(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)`
    71  
    72  	buildIdentifier = `[0-9a-zA-Z-]+`
    73  
    74  	semverRe = regexp.MustCompile(`^` +
    75  
    76  		// dot-separated version segments (e.g. 1.2.3)
    77  		numericIdentifier + `\.` + numericIdentifier + `\.` + numericIdentifier +
    78  
    79  		// optional dot-separated prerelease segments (e.g. -alpha.PRERELEASE.1)
    80  		`(-` + preReleaseIdentifier + `(\.` + preReleaseIdentifier + `)*)?` +
    81  
    82  		// optional dot-separated build identifier segments (e.g. +build.id.20240305)
    83  		`(\+` + buildIdentifier + `(\.` + buildIdentifier + `)*)?` +
    84  
    85  		`$`)
    86  )
    87  
    88  func validateAttributes(attributes []resource.NamedResourcesAttribute, fldPath *field.Path) field.ErrorList {
    89  	var allErrs field.ErrorList
    90  	attributeNames := sets.New[string]()
    91  	for i, attribute := range attributes {
    92  		idxPath := fldPath.Index(i)
    93  		attributeName := attribute.Name
    94  		allErrs = append(allErrs, validateAttributeName(attributeName, idxPath.Child("name"))...)
    95  		if attributeNames.Has(attributeName) {
    96  			allErrs = append(allErrs, field.Duplicate(idxPath.Child("name"), attributeName))
    97  		} else {
    98  			attributeNames.Insert(attributeName)
    99  		}
   100  
   101  		entries := sets.New[string]()
   102  		if attribute.QuantityValue != nil {
   103  			entries.Insert("quantity")
   104  		}
   105  		if attribute.BoolValue != nil {
   106  			entries.Insert("bool")
   107  		}
   108  		if attribute.IntValue != nil {
   109  			entries.Insert("int")
   110  		}
   111  		if attribute.IntSliceValue != nil {
   112  			entries.Insert("intSlice")
   113  		}
   114  		if attribute.StringValue != nil {
   115  			entries.Insert("string")
   116  		}
   117  		if attribute.StringSliceValue != nil {
   118  			entries.Insert("stringSlice")
   119  		}
   120  		if attribute.VersionValue != nil {
   121  			entries.Insert("version")
   122  			if !semverRe.MatchString(*attribute.VersionValue) {
   123  				allErrs = append(allErrs, field.Invalid(idxPath.Child("version"), *attribute.VersionValue, "must be a string compatible with semver.org spec 2.0.0"))
   124  			}
   125  		}
   126  
   127  		switch len(entries) {
   128  		case 0:
   129  			allErrs = append(allErrs, field.Required(idxPath, "exactly one value must be set"))
   130  		case 1:
   131  			// Okay.
   132  		default:
   133  			allErrs = append(allErrs, field.Invalid(idxPath, sets.List(entries), "exactly one field must be set, not several"))
   134  		}
   135  	}
   136  	return allErrs
   137  }
   138  
   139  func ValidateRequest(opts Options, request *resource.NamedResourcesRequest, fldPath *field.Path) field.ErrorList {
   140  	return validateSelector(opts, request.Selector, fldPath.Child("selector"))
   141  }
   142  
   143  func ValidateFilter(opts Options, filter *resource.NamedResourcesFilter, fldPath *field.Path) field.ErrorList {
   144  	return validateSelector(opts, filter.Selector, fldPath.Child("selector"))
   145  }
   146  
   147  func validateSelector(opts Options, selector string, fldPath *field.Path) field.ErrorList {
   148  	var allErrs field.ErrorList
   149  	if selector == "" {
   150  		allErrs = append(allErrs, field.Required(fldPath, ""))
   151  	} else {
   152  		envType := environment.NewExpressions
   153  		if opts.StoredExpressions {
   154  			envType = environment.StoredExpressions
   155  		}
   156  		result := namedresourcescel.Compiler.CompileCELExpression(selector, envType)
   157  		if result.Error != nil {
   158  			allErrs = append(allErrs, convertCELErrorToValidationError(fldPath, selector, result.Error))
   159  		}
   160  	}
   161  	return allErrs
   162  }
   163  
   164  func convertCELErrorToValidationError(fldPath *field.Path, expression string, err *cel.Error) *field.Error {
   165  	switch err.Type {
   166  	case cel.ErrorTypeRequired:
   167  		return field.Required(fldPath, err.Detail)
   168  	case cel.ErrorTypeInvalid:
   169  		return field.Invalid(fldPath, expression, err.Detail)
   170  	case cel.ErrorTypeInternal:
   171  		return field.InternalError(fldPath, err)
   172  	}
   173  	return field.InternalError(fldPath, fmt.Errorf("unsupported error type: %w", err))
   174  }
   175  
   176  func ValidateAllocationResult(result *resource.NamedResourcesAllocationResult, fldPath *field.Path) field.ErrorList {
   177  	return validateInstanceName(result.Name, fldPath.Child("name"))
   178  }