| // Copyright Istio 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 ( |
| "fmt" |
| "net" |
| "reflect" |
| "regexp" |
| "strconv" |
| "strings" |
| ) |
| |
| import ( |
| "istio.io/pkg/log" |
| meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" |
| "sigs.k8s.io/yaml" |
| ) |
| |
| import ( |
| "github.com/apache/dubbo-go-pixiu/operator/pkg/apis/istio/v1alpha1" |
| "github.com/apache/dubbo-go-pixiu/operator/pkg/util" |
| ) |
| |
| var ( |
| scope = log.RegisterScope("validation", "API validation", 0) |
| |
| // alphaNumericRegexp defines the alpha numeric atom, typically a |
| // component of names. This only allows lower case characters and digits. |
| alphaNumericRegexp = match(`[a-z0-9]+`) |
| |
| // separatorRegexp defines the separators allowed to be embedded in name |
| // components. This allow one period, one or two underscore and multiple |
| // dashes. |
| separatorRegexp = match(`(?:[._]|__|[-]*)`) |
| |
| // nameComponentRegexp restricts registry path component names to start |
| // with at least one letter or number, with following parts able to be |
| // separated by one period, one or two underscore and multiple dashes. |
| nameComponentRegexp = expression( |
| alphaNumericRegexp, |
| optional(repeated(separatorRegexp, alphaNumericRegexp))) |
| |
| // domainComponentRegexp restricts the registry domain component of a |
| // repository name to start with a component as defined by DomainRegexp |
| // and followed by an optional port. |
| domainComponentRegexp = match(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`) |
| |
| // DomainRegexp defines the structure of potential domain components |
| // that may be part of image names. This is purposely a subset of what is |
| // allowed by DNS to ensure backwards compatibility with Docker image |
| // names. |
| DomainRegexp = expression( |
| domainComponentRegexp, |
| optional(repeated(literal(`.`), domainComponentRegexp)), |
| optional(literal(`:`), match(`[0-9]+`))) |
| |
| // TagRegexp matches valid tag names. From docker/docker:graph/tags.go. |
| TagRegexp = match(`[\w][\w.-]{0,127}`) |
| |
| // DigestRegexp matches valid digests. |
| DigestRegexp = match(`[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`) |
| |
| // NameRegexp is the format for the name component of references. The |
| // regexp has capturing groups for the domain and name part omitting |
| // the separating forward slash from either. |
| NameRegexp = expression( |
| optional(DomainRegexp, literal(`/`)), |
| nameComponentRegexp, |
| optional(repeated(literal(`/`), nameComponentRegexp))) |
| |
| // ReferenceRegexp is the full supported format of a reference. The regexp |
| // is anchored and has capturing groups for name, tag, and digest |
| // components. |
| ReferenceRegexp = anchored(capture(NameRegexp), |
| optional(literal(":"), capture(TagRegexp)), |
| optional(literal("@"), capture(DigestRegexp))) |
| |
| // ObjectNameRegexp is a legal name for a k8s object. |
| ObjectNameRegexp = match(`[a-z0-9.-]{1,254}`) |
| ) |
| |
| // validateWithRegex checks whether the given value matches the regexp r. |
| func validateWithRegex(path util.Path, val interface{}, r *regexp.Regexp) (errs util.Errors) { |
| valStr := fmt.Sprint(val) |
| if len(r.FindString(valStr)) != len(valStr) { |
| errs = util.AppendErr(errs, fmt.Errorf("invalid value %s: %v", path, val)) |
| printError(errs.ToError()) |
| } |
| return errs |
| } |
| |
| // validateStringList returns a validator function that works on a string list, using the supplied ValidatorFunc vf on |
| // each element. |
| func validateStringList(vf ValidatorFunc) ValidatorFunc { |
| return func(path util.Path, val interface{}) util.Errors { |
| msg := fmt.Sprintf("validateStringList %v", val) |
| if !util.IsString(val) { |
| err := fmt.Errorf("validateStringList %s got %T, want string", path, val) |
| printError(err) |
| return util.NewErrs(err) |
| } |
| var errs util.Errors |
| for _, s := range strings.Split(val.(string), ",") { |
| errs = util.AppendErrs(errs, vf(path, strings.TrimSpace(s))) |
| scope.Debugf("\nerrors(%d): %v", len(errs), errs) |
| msg += fmt.Sprintf("\nerrors(%d): %v", len(errs), errs) |
| } |
| logWithError(errs.ToError(), msg) |
| return errs |
| } |
| } |
| |
| // validatePortNumberString checks if val is a string with a valid port number. |
| func validatePortNumberString(path util.Path, val interface{}) util.Errors { |
| scope.Debugf("validatePortNumberString %v:", val) |
| if !util.IsString(val) { |
| return util.NewErrs(fmt.Errorf("validatePortNumberString(%s) bad type %T, want string", path, val)) |
| } |
| if val.(string) == "*" || val.(string) == "" { |
| return nil |
| } |
| intV, err := strconv.ParseInt(val.(string), 10, 32) |
| if err != nil { |
| return util.NewErrs(fmt.Errorf("%s : %s", path, err)) |
| } |
| return validatePortNumber(path, intV) |
| } |
| |
| // validatePortNumber checks whether val is an integer representing a valid port number. |
| func validatePortNumber(path util.Path, val interface{}) util.Errors { |
| return validateIntRange(path, val, 0, 65535) |
| } |
| |
| // validateIPRangesOrStar validates IP ranges and also allow star, examples: "1.1.0.256/16,2.2.0.257/16", "*" |
| func validateIPRangesOrStar(path util.Path, val interface{}) (errs util.Errors) { |
| scope.Debugf("validateIPRangesOrStar at %v: %v", path, val) |
| |
| if !util.IsString(val) { |
| err := fmt.Errorf("validateIPRangesOrStar %s got %T, want string", path, val) |
| printError(err) |
| return util.NewErrs(err) |
| } |
| |
| if val.(string) == "*" || val.(string) == "" { |
| return errs |
| } |
| |
| return validateStringList(validateCIDR)(path, val) |
| } |
| |
| // validateIntRange checks whether val is an integer in [min, max]. |
| func validateIntRange(path util.Path, val interface{}, min, max int64) util.Errors { |
| k := reflect.TypeOf(val).Kind() |
| var err error |
| switch { |
| case util.IsIntKind(k): |
| v := reflect.ValueOf(val).Int() |
| if v < min || v > max { |
| err = fmt.Errorf("value %s:%v falls outside range [%v, %v]", path, v, min, max) |
| } |
| case util.IsUintKind(k): |
| v := reflect.ValueOf(val).Uint() |
| if int64(v) < min || int64(v) > max { |
| err = fmt.Errorf("value %s:%v falls out side range [%v, %v]", path, v, min, max) |
| } |
| default: |
| err = fmt.Errorf("validateIntRange %s unexpected type %T, want int type", path, val) |
| } |
| logWithError(err, "validateIntRange %s:%v in [%d, %d]?: ", path, val, min, max) |
| return util.NewErrs(err) |
| } |
| |
| // validateCIDR checks whether val is a string with a valid CIDR. |
| func validateCIDR(path util.Path, val interface{}) util.Errors { |
| var err error |
| if !util.IsString(val) { |
| err = fmt.Errorf("validateCIDR %s got %T, want string", path, val) |
| } else { |
| _, _, err = net.ParseCIDR(val.(string)) |
| if err != nil { |
| err = fmt.Errorf("%s %s", path, err) |
| } |
| } |
| logWithError(err, "validateCIDR (%s): ", val) |
| return util.NewErrs(err) |
| } |
| |
| func printError(err error) { |
| if err == nil { |
| scope.Debug("OK") |
| return |
| } |
| scope.Debugf("%v", err) |
| } |
| |
| // logWithError prints debug log with err message |
| func logWithError(err error, format string, args ...interface{}) { |
| msg := fmt.Sprintf(format, args...) |
| if err == nil { |
| msg += ": OK\n" |
| } else { |
| msg += fmt.Sprintf(": %v\n", err) |
| } |
| scope.Debug(msg) |
| } |
| |
| // match compiles the string to a regular expression. |
| var match = regexp.MustCompile |
| |
| // literal compiles s into a literal regular expression, escaping any regexp |
| // reserved characters. |
| func literal(s string) *regexp.Regexp { |
| re := match(regexp.QuoteMeta(s)) |
| |
| if _, complete := re.LiteralPrefix(); !complete { |
| panic("must be a literal") |
| } |
| |
| return re |
| } |
| |
| // expression defines a full expression, where each regular expression must |
| // follow the previous. |
| func expression(res ...*regexp.Regexp) *regexp.Regexp { |
| var s string |
| for _, re := range res { |
| s += re.String() |
| } |
| |
| return match(s) |
| } |
| |
| // optional wraps the expression in a non-capturing group and makes the |
| // production optional. |
| func optional(res ...*regexp.Regexp) *regexp.Regexp { |
| return match(group(expression(res...)).String() + `?`) |
| } |
| |
| // repeated wraps the regexp in a non-capturing group to get one or more |
| // matches. |
| func repeated(res ...*regexp.Regexp) *regexp.Regexp { |
| return match(group(expression(res...)).String() + `+`) |
| } |
| |
| // group wraps the regexp in a non-capturing group. |
| func group(res ...*regexp.Regexp) *regexp.Regexp { |
| return match(`(?:` + expression(res...).String() + `)`) |
| } |
| |
| // capture wraps the expression in a capturing group. |
| func capture(res ...*regexp.Regexp) *regexp.Regexp { |
| return match(`(` + expression(res...).String() + `)`) |
| } |
| |
| // anchored anchors the regular expression by adding start and end delimiters. |
| func anchored(res ...*regexp.Regexp) *regexp.Regexp { |
| return match(`^` + expression(res...).String() + `$`) |
| } |
| |
| // ValidatorFunc validates a value. |
| type ValidatorFunc func(path util.Path, i interface{}) util.Errors |
| |
| // UnmarshalIOP unmarshals a string containing IstioOperator as YAML. |
| func UnmarshalIOP(iopYAML string) (*v1alpha1.IstioOperator, error) { |
| // Remove creationDate (util.UnmarshalWithJSONPB fails if present) |
| mapIOP := make(map[string]interface{}) |
| if err := yaml.Unmarshal([]byte(iopYAML), &mapIOP); err != nil { |
| return nil, err |
| } |
| // Don't bother trying to remove the timestamp if there are no fields. |
| // This also preserves iopYAML if it is ""; we don't want iopYAML to be the string "null" |
| if len(mapIOP) > 0 { |
| un := &unstructured.Unstructured{Object: mapIOP} |
| un.SetCreationTimestamp(meta_v1.Time{}) // UnmarshalIstioOperator chokes on these |
| iopYAML = util.ToYAML(un) |
| } |
| iop := &v1alpha1.IstioOperator{} |
| |
| if err := yaml.UnmarshalStrict([]byte(iopYAML), iop); err != nil { |
| return nil, fmt.Errorf("%s:\n\nYAML:\n%s", err, iopYAML) |
| } |
| return iop, nil |
| } |
| |
| // ValidIOP validates the given IstioOperator object. |
| func ValidIOP(iop *v1alpha1.IstioOperator) error { |
| errs := CheckIstioOperatorSpec(iop.Spec, false) |
| return errs.ToError() |
| } |
| |
| // compose path for slice s with index i |
| func indexPathForSlice(s string, i int) string { |
| return fmt.Sprintf("%s[%d]", s, i) |
| } |
| |
| // get validation function for specified path |
| func getValidationFuncForPath(validations map[string]ValidatorFunc, path util.Path) (ValidatorFunc, bool) { |
| pstr := path.String() |
| // fast match |
| if !strings.Contains(pstr, "[") && !strings.Contains(pstr, "]") { |
| vf, ok := validations[pstr] |
| return vf, ok |
| } |
| for p, vf := range validations { |
| ps := strings.Split(p, ".") |
| if len(ps) != len(path) { |
| continue |
| } |
| for i, v := range ps { |
| if !matchPathNode(v, path[i]) { |
| break |
| } |
| if i == len(ps)-1 { |
| return vf, true |
| } |
| } |
| } |
| return nil, false |
| } |
| |
| // check whether the pn path node match pattern. |
| // pattern may container '*', eg. [1] match [*]. |
| func matchPathNode(pattern, pn string) bool { |
| if !strings.Contains(pattern, "[") && !strings.Contains(pattern, "]") { |
| return pattern == pn |
| } |
| if !strings.Contains(pn, "[") && !strings.Contains(pn, "]") { |
| return false |
| } |
| indexPattern := pattern[strings.IndexByte(pattern, '[')+1 : strings.IndexByte(pattern, ']')] |
| if indexPattern == "*" { |
| return true |
| } |
| index := pn[strings.IndexByte(pn, '[')+1 : strings.IndexByte(pn, ']')] |
| return indexPattern == index |
| } |