istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/validate/common.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 "fmt" 19 "net/netip" 20 "reflect" 21 "regexp" 22 "strconv" 23 "strings" 24 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 27 "sigs.k8s.io/yaml" 28 29 "istio.io/istio/operator/pkg/apis/istio/v1alpha1" 30 "istio.io/istio/operator/pkg/util" 31 "istio.io/istio/pkg/log" 32 ) 33 34 var ( 35 scope = log.RegisterScope("validation", "API validation") 36 37 // alphaNumericRegexp defines the alpha numeric atom, typically a 38 // component of names. This only allows lower case characters and digits. 39 alphaNumericRegexp = match(`[a-z0-9]+`) 40 41 // separatorRegexp defines the separators allowed to be embedded in name 42 // components. This allow one period, one or two underscore and multiple 43 // dashes. 44 separatorRegexp = match(`(?:[._]|__|[-]*)`) 45 46 // nameComponentRegexp restricts registry path component names to start 47 // with at least one letter or number, with following parts able to be 48 // separated by one period, one or two underscore and multiple dashes. 49 nameComponentRegexp = expression( 50 alphaNumericRegexp, 51 optional(repeated(separatorRegexp, alphaNumericRegexp))) 52 53 // domainComponentRegexp restricts the registry domain component of a 54 // repository name to start with a component as defined by DomainRegexp 55 // and followed by an optional port. 56 domainComponentRegexp = match(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`) 57 58 // DomainRegexp defines the structure of potential domain components 59 // that may be part of image names. This is purposely a subset of what is 60 // allowed by DNS to ensure backwards compatibility with Docker image 61 // names. 62 DomainRegexp = expression( 63 domainComponentRegexp, 64 optional(repeated(literal(`.`), domainComponentRegexp)), 65 optional(literal(`:`), match(`[0-9]+`))) 66 67 // TagRegexp matches valid tag names. From docker/docker:graph/tags.go. 68 TagRegexp = match(`[\w][\w.-]{0,127}`) 69 70 // DigestRegexp matches valid digests. 71 DigestRegexp = match(`[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`) 72 73 // NameRegexp is the format for the name component of references. The 74 // regexp has capturing groups for the domain and name part omitting 75 // the separating forward slash from either. 76 NameRegexp = expression( 77 optional(DomainRegexp, literal(`/`)), 78 nameComponentRegexp, 79 optional(repeated(literal(`/`), nameComponentRegexp))) 80 81 // ReferenceRegexp is the full supported format of a reference. The regexp 82 // is anchored and has capturing groups for name, tag, and digest 83 // components. 84 ReferenceRegexp = anchored(capture(NameRegexp), 85 optional(literal(":"), capture(TagRegexp)), 86 optional(literal("@"), capture(DigestRegexp))) 87 88 // ObjectNameRegexp is a legal name for a k8s object. 89 ObjectNameRegexp = match(`[a-z0-9.-]{1,254}`) 90 ) 91 92 // validateWithRegex checks whether the given value matches the regexp r. 93 func validateWithRegex(path util.Path, val any, r *regexp.Regexp) (errs util.Errors) { 94 valStr := fmt.Sprint(val) 95 if len(r.FindString(valStr)) != len(valStr) { 96 errs = util.AppendErr(errs, fmt.Errorf("invalid value %s: %v", path, val)) 97 printError(errs.ToError()) 98 } 99 return errs 100 } 101 102 // validateStringList returns a validator function that works on a string list, using the supplied ValidatorFunc vf on 103 // each element. 104 func validateStringList(vf ValidatorFunc) ValidatorFunc { 105 return func(path util.Path, val any) util.Errors { 106 msg := fmt.Sprintf("validateStringList %v", val) 107 if !util.IsString(val) { 108 err := fmt.Errorf("validateStringList %s got %T, want string", path, val) 109 printError(err) 110 return util.NewErrs(err) 111 } 112 var errs util.Errors 113 for _, s := range strings.Split(val.(string), ",") { 114 errs = util.AppendErrs(errs, vf(path, s)) 115 scope.Debugf("\nerrors(%d): %v", len(errs), errs) 116 msg += fmt.Sprintf("\nerrors(%d): %v", len(errs), errs) 117 } 118 logWithError(errs.ToError(), msg) 119 return errs 120 } 121 } 122 123 // validatePortNumberString checks if val is a string with a valid port number. 124 func validatePortNumberString(path util.Path, val any) util.Errors { 125 scope.Debugf("validatePortNumberString %v:", val) 126 if !util.IsString(val) { 127 return util.NewErrs(fmt.Errorf("validatePortNumberString(%s) bad type %T, want string", path, val)) 128 } 129 if val.(string) == "*" || val.(string) == "" { 130 return nil 131 } 132 intV, err := strconv.ParseInt(val.(string), 10, 32) 133 if err != nil { 134 return util.NewErrs(fmt.Errorf("%s : %s", path, err)) 135 } 136 return validatePortNumber(path, intV) 137 } 138 139 // validatePortNumber checks whether val is an integer representing a valid port number. 140 func validatePortNumber(path util.Path, val any) util.Errors { 141 return validateIntRange(path, val, 0, 65535) 142 } 143 144 // validateIPRangesOrStar validates IP ranges and also allow star, examples: "1.1.0.256/16,2.2.0.257/16", "*" 145 func validateIPRangesOrStar(path util.Path, val any) (errs util.Errors) { 146 scope.Debugf("validateIPRangesOrStar at %v: %v", path, val) 147 148 if !util.IsString(val) { 149 err := fmt.Errorf("validateIPRangesOrStar %s got %T, want string", path, val) 150 printError(err) 151 return util.NewErrs(err) 152 } 153 154 if val.(string) == "*" || val.(string) == "" { 155 return errs 156 } 157 158 return validateStringList(validateCIDR)(path, val) 159 } 160 161 // validateIntRange checks whether val is an integer in [min, max]. 162 func validateIntRange(path util.Path, val any, min, max int64) util.Errors { 163 k := reflect.TypeOf(val).Kind() 164 var err error 165 switch { 166 case util.IsIntKind(k): 167 v := reflect.ValueOf(val).Int() 168 if v < min || v > max { 169 err = fmt.Errorf("value %s:%v falls outside range [%v, %v]", path, v, min, max) 170 } 171 case util.IsUintKind(k): 172 v := reflect.ValueOf(val).Uint() 173 if int64(v) < min || int64(v) > max { 174 err = fmt.Errorf("value %s:%v falls out side range [%v, %v]", path, v, min, max) 175 } 176 default: 177 err = fmt.Errorf("validateIntRange %s unexpected type %T, want int type", path, val) 178 } 179 logWithError(err, "validateIntRange %s:%v in [%d, %d]?: ", path, val, min, max) 180 return util.NewErrs(err) 181 } 182 183 // validateCIDR checks whether val is a string with a valid CIDR. 184 func validateCIDR(path util.Path, val any) util.Errors { 185 var err error 186 if !util.IsString(val) { 187 err = fmt.Errorf("validateCIDR %s got %T, want string", path, val) 188 } else { 189 if _, err = netip.ParsePrefix(val.(string)); err != nil { 190 err = fmt.Errorf("%s %s", path, err) 191 } 192 } 193 logWithError(err, "validateCIDR (%s): ", val) 194 return util.NewErrs(err) 195 } 196 197 func printError(err error) { 198 if err == nil { 199 scope.Debug("OK") 200 return 201 } 202 scope.Debugf("%v", err) 203 } 204 205 // logWithError prints debug log with err message 206 func logWithError(err error, format string, args ...any) { 207 msg := fmt.Sprintf(format, args...) 208 if err == nil { 209 msg += ": OK\n" 210 } else { 211 msg += fmt.Sprintf(": %v\n", err) 212 } 213 scope.Debug(msg) 214 } 215 216 // match compiles the string to a regular expression. 217 var match = regexp.MustCompile 218 219 // literal compiles s into a literal regular expression, escaping any regexp 220 // reserved characters. 221 func literal(s string) *regexp.Regexp { 222 re := match(regexp.QuoteMeta(s)) 223 224 if _, complete := re.LiteralPrefix(); !complete { 225 panic("must be a literal") 226 } 227 228 return re 229 } 230 231 // expression defines a full expression, where each regular expression must 232 // follow the previous. 233 func expression(res ...*regexp.Regexp) *regexp.Regexp { 234 var s string 235 for _, re := range res { 236 s += re.String() 237 } 238 239 return match(s) 240 } 241 242 // optional wraps the expression in a non-capturing group and makes the 243 // production optional. 244 func optional(res ...*regexp.Regexp) *regexp.Regexp { 245 return match(group(expression(res...)).String() + `?`) 246 } 247 248 // repeated wraps the regexp in a non-capturing group to get one or more 249 // matches. 250 func repeated(res ...*regexp.Regexp) *regexp.Regexp { 251 return match(group(expression(res...)).String() + `+`) 252 } 253 254 // group wraps the regexp in a non-capturing group. 255 func group(res ...*regexp.Regexp) *regexp.Regexp { 256 return match(`(?:` + expression(res...).String() + `)`) 257 } 258 259 // capture wraps the expression in a capturing group. 260 func capture(res ...*regexp.Regexp) *regexp.Regexp { 261 return match(`(` + expression(res...).String() + `)`) 262 } 263 264 // anchored anchors the regular expression by adding start and end delimiters. 265 func anchored(res ...*regexp.Regexp) *regexp.Regexp { 266 return match(`^` + expression(res...).String() + `$`) 267 } 268 269 // ValidatorFunc validates a value. 270 type ValidatorFunc func(path util.Path, i any) util.Errors 271 272 // UnmarshalIOP unmarshals a string containing IstioOperator as YAML. 273 func UnmarshalIOP(iopYAML string) (*v1alpha1.IstioOperator, error) { 274 // Remove creationDate (util.UnmarshalWithJSONPB fails if present) 275 mapIOP := make(map[string]any) 276 if err := yaml.Unmarshal([]byte(iopYAML), &mapIOP); err != nil { 277 return nil, err 278 } 279 // Don't bother trying to remove the timestamp if there are no fields. 280 // This also preserves iopYAML if it is ""; we don't want iopYAML to be the string "null" 281 if len(mapIOP) > 0 { 282 un := &unstructured.Unstructured{Object: mapIOP} 283 un.SetCreationTimestamp(metav1.Time{}) // UnmarshalIstioOperator chokes on these 284 iopYAML = util.ToYAML(un) 285 } 286 iop := &v1alpha1.IstioOperator{} 287 288 if err := yaml.UnmarshalStrict([]byte(iopYAML), iop); err != nil { 289 return nil, fmt.Errorf("%s:\n\nYAML:\n%s", err, iopYAML) 290 } 291 return iop, nil 292 } 293 294 // ValidIOP validates the given IstioOperator object. 295 func ValidIOP(iop *v1alpha1.IstioOperator) error { 296 errs := CheckIstioOperatorSpec(iop.Spec, false) 297 return errs.ToError() 298 } 299 300 // compose path for slice s with index i 301 func indexPathForSlice(s string, i int) string { 302 return fmt.Sprintf("%s[%d]", s, i) 303 } 304 305 // get validation function for specified path 306 func getValidationFuncForPath(validations map[string]ValidatorFunc, path util.Path) (ValidatorFunc, bool) { 307 pstr := path.String() 308 // fast match 309 if !strings.Contains(pstr, "[") && !strings.Contains(pstr, "]") { 310 vf, ok := validations[pstr] 311 return vf, ok 312 } 313 for p, vf := range validations { 314 ps := strings.Split(p, ".") 315 if len(ps) != len(path) { 316 continue 317 } 318 for i, v := range ps { 319 if !matchPathNode(v, path[i]) { 320 break 321 } 322 if i == len(ps)-1 { 323 return vf, true 324 } 325 } 326 } 327 return nil, false 328 } 329 330 // check whether the pn path node match pattern. 331 // pattern may contain '*', e.g. [1] match [*]. 332 func matchPathNode(pattern, pn string) bool { 333 if !strings.Contains(pattern, "[") && !strings.Contains(pattern, "]") { 334 return pattern == pn 335 } 336 if !strings.Contains(pn, "[") && !strings.Contains(pn, "]") { 337 return false 338 } 339 indexPattern := pattern[strings.IndexByte(pattern, '[')+1 : strings.IndexByte(pattern, ']')] 340 if indexPattern == "*" { 341 return true 342 } 343 index := pn[strings.IndexByte(pn, '[')+1 : strings.IndexByte(pn, ']')] 344 return indexPattern == index 345 }