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  }