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 }