istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/validate/validate.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package validate
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"reflect"
    21  
    22  	"google.golang.org/protobuf/types/known/structpb"
    23  
    24  	"istio.io/api/operator/v1alpha1"
    25  	operator_v1alpha1 "istio.io/istio/operator/pkg/apis/istio/v1alpha1"
    26  	"istio.io/istio/operator/pkg/metrics"
    27  	"istio.io/istio/operator/pkg/tpath"
    28  	"istio.io/istio/operator/pkg/util"
    29  	"istio.io/istio/pkg/config/labels"
    30  	"istio.io/istio/pkg/config/mesh"
    31  	"istio.io/istio/pkg/util/protomarshal"
    32  )
    33  
    34  var (
    35  	// DefaultValidations maps a data path to a validation function.
    36  	DefaultValidations = map[string]ValidatorFunc{
    37  		"Values": func(path util.Path, i any) util.Errors {
    38  			return CheckValues(i)
    39  		},
    40  		"MeshConfig":                 validateMeshConfig,
    41  		"Hub":                        validateHub,
    42  		"Tag":                        validateTag,
    43  		"Revision":                   validateRevision,
    44  		"Components.IngressGateways": validateGatewayName,
    45  		"Components.EgressGateways":  validateGatewayName,
    46  	}
    47  	// requiredValues lists all the values that must be non-empty.
    48  	requiredValues = map[string]bool{}
    49  )
    50  
    51  // CheckIstioOperator validates the operator CR.
    52  func CheckIstioOperator(iop *operator_v1alpha1.IstioOperator, checkRequiredFields bool) error {
    53  	if iop == nil {
    54  		return nil
    55  	}
    56  
    57  	errs := CheckIstioOperatorSpec(iop.Spec, checkRequiredFields)
    58  	return errs.ToError()
    59  }
    60  
    61  // CheckIstioOperatorSpec validates the values in the given Installer spec, using the field map DefaultValidations to
    62  // call the appropriate validation function. checkRequiredFields determines whether missing mandatory fields generate
    63  // errors.
    64  func CheckIstioOperatorSpec(is *v1alpha1.IstioOperatorSpec, checkRequiredFields bool) (errs util.Errors) {
    65  	if is == nil {
    66  		return util.Errors{}
    67  	}
    68  
    69  	return Validate2(DefaultValidations, is)
    70  }
    71  
    72  func Validate2(validations map[string]ValidatorFunc, iop *v1alpha1.IstioOperatorSpec) (errs util.Errors) {
    73  	for path, validator := range validations {
    74  		v, f, _ := tpath.GetFromStructPath(iop, path)
    75  		if f {
    76  			errs = append(errs, validator(util.PathFromString(path), v)...)
    77  		}
    78  	}
    79  	return
    80  }
    81  
    82  // Validate function below is used by third party for integrations and has to be public
    83  
    84  // Validate validates the values of the tree using the supplied Func.
    85  func Validate(validations map[string]ValidatorFunc, structPtr any, path util.Path, checkRequired bool) (errs util.Errors) {
    86  	scope.Debugf("validate with path %s, %v (%T)", path, structPtr, structPtr)
    87  	if structPtr == nil {
    88  		return nil
    89  	}
    90  	if util.IsStruct(structPtr) {
    91  		scope.Debugf("validate path %s, skipping struct type %T", path, structPtr)
    92  		return nil
    93  	}
    94  	if !util.IsPtr(structPtr) {
    95  		metrics.CRValidationErrorTotal.Increment()
    96  		return util.NewErrs(fmt.Errorf("validate path %s, value: %v, expected ptr, got %T", path, structPtr, structPtr))
    97  	}
    98  	structElems := reflect.ValueOf(structPtr).Elem()
    99  	if !util.IsStruct(structElems) {
   100  		metrics.CRValidationErrorTotal.Increment()
   101  		return util.NewErrs(fmt.Errorf("validate path %s, value: %v, expected struct, got %T", path, structElems, structElems))
   102  	}
   103  
   104  	if util.IsNilOrInvalidValue(structElems) {
   105  		return
   106  	}
   107  
   108  	for i := 0; i < structElems.NumField(); i++ {
   109  		fieldName := structElems.Type().Field(i).Name
   110  		fieldValue := structElems.Field(i)
   111  		if !fieldValue.CanInterface() {
   112  			continue
   113  		}
   114  		kind := structElems.Type().Field(i).Type.Kind()
   115  		if a, ok := structElems.Type().Field(i).Tag.Lookup("json"); ok && a == "-" {
   116  			continue
   117  		}
   118  
   119  		scope.Debugf("Checking field %s", fieldName)
   120  		switch kind {
   121  		case reflect.Struct:
   122  			errs = util.AppendErrs(errs, Validate(validations, fieldValue.Addr().Interface(), append(path, fieldName), checkRequired))
   123  		case reflect.Map:
   124  			newPath := append(path, fieldName)
   125  			errs = util.AppendErrs(errs, validateLeaf(validations, newPath, fieldValue.Interface(), checkRequired))
   126  			for _, key := range fieldValue.MapKeys() {
   127  				nnp := append(newPath, key.String())
   128  				errs = util.AppendErrs(errs, validateLeaf(validations, nnp, fieldValue.MapIndex(key), checkRequired))
   129  			}
   130  		case reflect.Slice:
   131  			for i := 0; i < fieldValue.Len(); i++ {
   132  				newValue := fieldValue.Index(i).Interface()
   133  				newPath := append(path, indexPathForSlice(fieldName, i))
   134  				if util.IsStruct(newValue) || util.IsPtr(newValue) {
   135  					errs = util.AppendErrs(errs, Validate(validations, newValue, newPath, checkRequired))
   136  				} else {
   137  					errs = util.AppendErrs(errs, validateLeaf(validations, newPath, newValue, checkRequired))
   138  				}
   139  			}
   140  		case reflect.Ptr:
   141  			if util.IsNilOrInvalidValue(fieldValue.Elem()) {
   142  				continue
   143  			}
   144  			newPath := append(path, fieldName)
   145  			if fieldValue.Elem().Kind() == reflect.Struct {
   146  				errs = util.AppendErrs(errs, Validate(validations, fieldValue.Interface(), newPath, checkRequired))
   147  			} else {
   148  				errs = util.AppendErrs(errs, validateLeaf(validations, newPath, fieldValue, checkRequired))
   149  			}
   150  		default:
   151  			if structElems.Field(i).CanInterface() {
   152  				errs = util.AppendErrs(errs, validateLeaf(validations, append(path, fieldName), fieldValue.Interface(), checkRequired))
   153  			}
   154  		}
   155  	}
   156  	if len(errs) > 0 {
   157  		metrics.CRValidationErrorTotal.Increment()
   158  	}
   159  	return errs
   160  }
   161  
   162  func validateLeaf(validations map[string]ValidatorFunc, path util.Path, val any, checkRequired bool) util.Errors {
   163  	pstr := path.String()
   164  	msg := fmt.Sprintf("validate %s:%v(%T) ", pstr, val, val)
   165  	if util.IsValueNil(val) || util.IsEmptyString(val) {
   166  		if checkRequired && requiredValues[pstr] {
   167  			return util.NewErrs(fmt.Errorf("field %s is required but not set", util.ToYAMLPathString(pstr)))
   168  		}
   169  		msg += fmt.Sprintf("validate %s: OK (empty value)", pstr)
   170  		scope.Debug(msg)
   171  		return nil
   172  	}
   173  
   174  	vf, ok := getValidationFuncForPath(validations, path)
   175  	if !ok {
   176  		msg += fmt.Sprintf("validate %s: OK (no validation)", pstr)
   177  		scope.Debug(msg)
   178  		// No validation defined.
   179  		return nil
   180  	}
   181  	scope.Debug(msg)
   182  	return vf(path, val)
   183  }
   184  
   185  func validateMeshConfig(path util.Path, root any) util.Errors {
   186  	vs, err := util.ToYAMLGeneric(root)
   187  	if err != nil {
   188  		return util.Errors{err}
   189  	}
   190  	// ApplyMeshConfigDefaults allows unknown fields, so we first check for unknown fields
   191  	if err := protomarshal.ApplyYAMLStrict(string(vs), mesh.DefaultMeshConfig()); err != nil {
   192  		return util.Errors{fmt.Errorf("failed to unmarshall mesh config: %v", err)}
   193  	}
   194  	// This method will also perform validation automatically
   195  	if _, validErr := mesh.ApplyMeshConfigDefaults(string(vs)); validErr != nil {
   196  		return util.Errors{validErr}
   197  	}
   198  	return nil
   199  }
   200  
   201  func validateHub(path util.Path, val any) util.Errors {
   202  	if val == "" {
   203  		return nil
   204  	}
   205  	return validateWithRegex(path, val, ReferenceRegexp)
   206  }
   207  
   208  func validateTag(path util.Path, val any) util.Errors {
   209  	return validateWithRegex(path, val.(*structpb.Value).GetStringValue(), TagRegexp)
   210  }
   211  
   212  func validateRevision(_ util.Path, val any) util.Errors {
   213  	if val == "" {
   214  		return nil
   215  	}
   216  	if !labels.IsDNS1123Label(val.(string)) {
   217  		err := fmt.Errorf("invalid revision specified: %s", val.(string))
   218  		return util.Errors{err}
   219  	}
   220  	return nil
   221  }
   222  
   223  func validateGatewayName(path util.Path, val any) (errs util.Errors) {
   224  	v := val.([]*v1alpha1.GatewaySpec)
   225  	for _, n := range v {
   226  		if n == nil {
   227  			errs = append(errs, util.NewErrs(errors.New("badly formatted gateway configuration")))
   228  		} else {
   229  			errs = append(errs, validateWithRegex(path, n.Name, ObjectNameRegexp)...)
   230  		}
   231  	}
   232  	return
   233  }