github.com/nginxinc/kubernetes-ingress@v1.12.5/pkg/apis/configuration/validation/policy.go (about)

     1  package validation
     2  
     3  import (
     4  	"fmt"
     5  	"net"
     6  	"net/url"
     7  	"regexp"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/nginxinc/kubernetes-ingress/internal/k8s/appprotect"
    12  	v1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1"
    13  	"k8s.io/apimachinery/pkg/util/validation"
    14  	"k8s.io/apimachinery/pkg/util/validation/field"
    15  )
    16  
    17  // ValidatePolicy validates a Policy.
    18  func ValidatePolicy(policy *v1.Policy, isPlus, enablePreviewPolicies, enableAppProtect bool) error {
    19  	allErrs := validatePolicySpec(&policy.Spec, field.NewPath("spec"), isPlus, enablePreviewPolicies, enableAppProtect)
    20  	return allErrs.ToAggregate()
    21  }
    22  
    23  func validatePolicySpec(spec *v1.PolicySpec, fieldPath *field.Path, isPlus, enablePreviewPolicies, enableAppProtect bool) field.ErrorList {
    24  	allErrs := field.ErrorList{}
    25  
    26  	fieldCount := 0
    27  
    28  	if spec.AccessControl != nil {
    29  		allErrs = append(allErrs, validateAccessControl(spec.AccessControl, fieldPath.Child("accessControl"))...)
    30  		fieldCount++
    31  	}
    32  
    33  	if spec.RateLimit != nil {
    34  		if !enablePreviewPolicies {
    35  			return append(allErrs, field.Forbidden(fieldPath.Child("rateLimit"),
    36  				"rateLimit is a preview policy. Preview policies must be enabled to use via cli argument -enable-preview-policies"))
    37  		}
    38  		allErrs = append(allErrs, validateRateLimit(spec.RateLimit, fieldPath.Child("rateLimit"), isPlus)...)
    39  		fieldCount++
    40  	}
    41  
    42  	if spec.JWTAuth != nil {
    43  		if !enablePreviewPolicies {
    44  			allErrs = append(allErrs, field.Forbidden(fieldPath.Child("jwt"),
    45  				"jwt is a preview policy. Preview policies must be enabled to use via cli argument -enable-preview-policies"))
    46  		}
    47  		if !isPlus {
    48  			return append(allErrs, field.Forbidden(fieldPath.Child("jwt"), "jwt secrets are only supported in NGINX Plus"))
    49  		}
    50  
    51  		allErrs = append(allErrs, validateJWT(spec.JWTAuth, fieldPath.Child("jwt"))...)
    52  		fieldCount++
    53  	}
    54  
    55  	if spec.IngressMTLS != nil {
    56  		if !enablePreviewPolicies {
    57  			return append(allErrs, field.Forbidden(fieldPath.Child("ingressMTLS"),
    58  				"ingressMTLS is a preview policy. Preview policies must be enabled to use via cli argument -enable-preview-policies"))
    59  		}
    60  		allErrs = append(allErrs, validateIngressMTLS(spec.IngressMTLS, fieldPath.Child("ingressMTLS"))...)
    61  		fieldCount++
    62  	}
    63  
    64  	if spec.EgressMTLS != nil {
    65  		if !enablePreviewPolicies {
    66  			return append(allErrs, field.Forbidden(fieldPath.Child("egressMTLS"),
    67  				"egressMTLS is a preview policy. Preview policies must be enabled to use via cli argument -enable-preview-policies"))
    68  		}
    69  		allErrs = append(allErrs, validateEgressMTLS(spec.EgressMTLS, fieldPath.Child("egressMTLS"))...)
    70  		fieldCount++
    71  	}
    72  
    73  	if spec.OIDC != nil {
    74  		if !enablePreviewPolicies {
    75  			allErrs = append(allErrs, field.Forbidden(fieldPath.Child("oidc"),
    76  				"oidc is a preview policy. Preview policies must be enabled to use via cli argument -enable-preview-policies"))
    77  		}
    78  		if !isPlus {
    79  			return append(allErrs, field.Forbidden(fieldPath.Child("oidc"), "OIDC is only supported in NGINX Plus"))
    80  		}
    81  
    82  		allErrs = append(allErrs, validateOIDC(spec.OIDC, fieldPath.Child("oidc"))...)
    83  		fieldCount++
    84  	}
    85  
    86  	if spec.WAF != nil {
    87  		if !enablePreviewPolicies {
    88  			allErrs = append(allErrs, field.Forbidden(fieldPath.Child("waf"),
    89  				"waf is a preview policy. Preview policies must be enabled to use via cli argument -enable-preview-policies"))
    90  		}
    91  		if !isPlus {
    92  			allErrs = append(allErrs, field.Forbidden(fieldPath.Child("waf"), "WAF is only supported in NGINX Plus"))
    93  		}
    94  		if !enableAppProtect {
    95  			allErrs = append(allErrs, field.Forbidden(fieldPath.Child("waf"),
    96  				"App Protect must be enabled via cli argument -enable-appprotect to use WAF policy"))
    97  		}
    98  
    99  		allErrs = append(allErrs, validateWAF(spec.WAF, fieldPath.Child("waf"))...)
   100  		fieldCount++
   101  	}
   102  
   103  	if fieldCount != 1 {
   104  		msg := "must specify exactly one of: `accessControl`, `rateLimit`, `ingressMTLS`, `egressMTLS`"
   105  		if isPlus {
   106  			msg = fmt.Sprint(msg, ", `jwt`, `oidc`, `waf`")
   107  		}
   108  		allErrs = append(allErrs, field.Invalid(fieldPath, "", msg))
   109  	}
   110  
   111  	return allErrs
   112  }
   113  
   114  func validateAccessControl(accessControl *v1.AccessControl, fieldPath *field.Path) field.ErrorList {
   115  	allErrs := field.ErrorList{}
   116  
   117  	fieldCount := 0
   118  
   119  	if accessControl.Allow != nil {
   120  		for i, ipOrCIDR := range accessControl.Allow {
   121  			allErrs = append(allErrs, validateIPorCIDR(ipOrCIDR, fieldPath.Child("allow").Index(i))...)
   122  		}
   123  		fieldCount++
   124  	}
   125  
   126  	if accessControl.Deny != nil {
   127  		for i, ipOrCIDR := range accessControl.Deny {
   128  			allErrs = append(allErrs, validateIPorCIDR(ipOrCIDR, fieldPath.Child("deny").Index(i))...)
   129  		}
   130  		fieldCount++
   131  	}
   132  
   133  	if fieldCount != 1 {
   134  		allErrs = append(allErrs, field.Invalid(fieldPath, "", "must specify exactly one of: `allow` or `deny`"))
   135  	}
   136  
   137  	return allErrs
   138  }
   139  
   140  func validateRateLimit(rateLimit *v1.RateLimit, fieldPath *field.Path, isPlus bool) field.ErrorList {
   141  	allErrs := field.ErrorList{}
   142  
   143  	allErrs = append(allErrs, validateRateLimitZoneSize(rateLimit.ZoneSize, fieldPath.Child("zoneSize"))...)
   144  	allErrs = append(allErrs, validateRate(rateLimit.Rate, fieldPath.Child("rate"))...)
   145  	allErrs = append(allErrs, validateRateLimitKey(rateLimit.Key, fieldPath.Child("key"), isPlus)...)
   146  
   147  	if rateLimit.Delay != nil {
   148  		allErrs = append(allErrs, validatePositiveInt(*rateLimit.Delay, fieldPath.Child("delay"))...)
   149  	}
   150  
   151  	if rateLimit.Burst != nil {
   152  		allErrs = append(allErrs, validatePositiveInt(*rateLimit.Burst, fieldPath.Child("burst"))...)
   153  	}
   154  
   155  	if rateLimit.LogLevel != "" {
   156  		allErrs = append(allErrs, validateRateLimitLogLevel(rateLimit.LogLevel, fieldPath.Child("logLevel"))...)
   157  	}
   158  
   159  	if rateLimit.RejectCode != nil {
   160  		if *rateLimit.RejectCode < 400 || *rateLimit.RejectCode > 599 {
   161  			allErrs = append(allErrs, field.Invalid(fieldPath.Child("rejectCode"), rateLimit.RejectCode,
   162  				"must be within the range [400-599]"))
   163  		}
   164  	}
   165  
   166  	return allErrs
   167  }
   168  
   169  func validateJWT(jwt *v1.JWTAuth, fieldPath *field.Path) field.ErrorList {
   170  	allErrs := field.ErrorList{}
   171  
   172  	allErrs = append(allErrs, validateJWTRealm(jwt.Realm, fieldPath.Child("realm"))...)
   173  
   174  	if jwt.Secret == "" {
   175  		return append(allErrs, field.Required(fieldPath.Child("secret"), ""))
   176  	}
   177  	allErrs = append(allErrs, validateSecretName(jwt.Secret, fieldPath.Child("secret"))...)
   178  
   179  	allErrs = append(allErrs, validateJWTToken(jwt.Token, fieldPath.Child("token"))...)
   180  
   181  	return allErrs
   182  }
   183  
   184  func validateIngressMTLS(ingressMTLS *v1.IngressMTLS, fieldPath *field.Path) field.ErrorList {
   185  	allErrs := field.ErrorList{}
   186  
   187  	if ingressMTLS.ClientCertSecret == "" {
   188  		return append(allErrs, field.Required(fieldPath.Child("clientCertSecret"), ""))
   189  	}
   190  	allErrs = append(allErrs, validateSecretName(ingressMTLS.ClientCertSecret, fieldPath.Child("clientCertSecret"))...)
   191  
   192  	allErrs = append(allErrs, validateIngressMTLSVerifyClient(ingressMTLS.VerifyClient, fieldPath.Child("verifyClient"))...)
   193  
   194  	if ingressMTLS.VerifyDepth != nil {
   195  		allErrs = append(allErrs, validatePositiveIntOrZero(*ingressMTLS.VerifyDepth, fieldPath.Child("verifyDepth"))...)
   196  	}
   197  	return allErrs
   198  }
   199  
   200  func validateEgressMTLS(egressMTLS *v1.EgressMTLS, fieldPath *field.Path) field.ErrorList {
   201  	allErrs := field.ErrorList{}
   202  
   203  	allErrs = append(allErrs, validateSecretName(egressMTLS.TLSSecret, fieldPath.Child("tlsSecret"))...)
   204  
   205  	if egressMTLS.VerifyServer && egressMTLS.TrustedCertSecret == "" {
   206  		return append(allErrs, field.Required(fieldPath.Child("trustedCertSecret"), "must be set when verifyServer is 'true'"))
   207  	}
   208  	allErrs = append(allErrs, validateSecretName(egressMTLS.TrustedCertSecret, fieldPath.Child("trustedCertSecret"))...)
   209  
   210  	if egressMTLS.VerifyDepth != nil {
   211  		allErrs = append(allErrs, validatePositiveIntOrZero(*egressMTLS.VerifyDepth, fieldPath.Child("verifyDepth"))...)
   212  	}
   213  
   214  	allErrs = append(allErrs, validateSSLName(egressMTLS.SSLName, fieldPath.Child("sslName"))...)
   215  
   216  	return allErrs
   217  }
   218  
   219  func validateOIDC(oidc *v1.OIDC, fieldPath *field.Path) field.ErrorList {
   220  	allErrs := field.ErrorList{}
   221  
   222  	if oidc.AuthEndpoint == "" {
   223  		return append(allErrs, field.Required(fieldPath.Child("authEndpoint"), ""))
   224  	}
   225  	if oidc.TokenEndpoint == "" {
   226  		return append(allErrs, field.Required(fieldPath.Child("tokenEndpoint"), ""))
   227  	}
   228  	if oidc.JWKSURI == "" {
   229  		return append(allErrs, field.Required(fieldPath.Child("jwksURI"), ""))
   230  	}
   231  	if oidc.ClientID == "" {
   232  		return append(allErrs, field.Required(fieldPath.Child("clientID"), ""))
   233  	}
   234  	if oidc.ClientSecret == "" {
   235  		return append(allErrs, field.Required(fieldPath.Child("clientSecret"), ""))
   236  	}
   237  
   238  	if oidc.Scope != "" {
   239  		allErrs = append(allErrs, validateOIDCScope(oidc.Scope, fieldPath.Child("scope"))...)
   240  	}
   241  
   242  	if oidc.RedirectURI != "" {
   243  		allErrs = append(allErrs, validatePath(oidc.RedirectURI, fieldPath.Child("redirectURI"))...)
   244  	}
   245  
   246  	allErrs = append(allErrs, validateURL(oidc.AuthEndpoint, fieldPath.Child("authEndpoint"))...)
   247  	allErrs = append(allErrs, validateURL(oidc.TokenEndpoint, fieldPath.Child("tokenEndpoint"))...)
   248  	allErrs = append(allErrs, validateURL(oidc.JWKSURI, fieldPath.Child("jwksURI"))...)
   249  	allErrs = append(allErrs, validateSecretName(oidc.ClientSecret, fieldPath.Child("clientSecret"))...)
   250  	allErrs = append(allErrs, validateClientID(oidc.ClientID, fieldPath.Child("clientID"))...)
   251  
   252  	return allErrs
   253  }
   254  
   255  func validateWAF(waf *v1.WAF, fieldPath *field.Path) field.ErrorList {
   256  	allErrs := field.ErrorList{}
   257  
   258  	if waf.ApPolicy != "" {
   259  		for _, msg := range validation.IsQualifiedName(waf.ApPolicy) {
   260  			allErrs = append(allErrs, field.Invalid(fieldPath.Child("apPolicy"), waf.ApPolicy, msg))
   261  		}
   262  	}
   263  
   264  	if waf.SecurityLog != nil {
   265  		allErrs = append(allErrs, validateLogConf(waf.SecurityLog.ApLogConf, waf.SecurityLog.LogDest, fieldPath.Child("securityLog"))...)
   266  	}
   267  
   268  	return allErrs
   269  }
   270  
   271  func validateLogConf(logConf, logDest string, fieldPath *field.Path) field.ErrorList {
   272  	allErrs := field.ErrorList{}
   273  
   274  	if logConf != "" {
   275  		for _, msg := range validation.IsQualifiedName(logConf) {
   276  			allErrs = append(allErrs, field.Invalid(fieldPath.Child("apLogConf"), logConf, msg))
   277  		}
   278  	}
   279  
   280  	err := appprotect.ValidateAppProtectLogDestination(logDest)
   281  	if err != nil {
   282  		allErrs = append(allErrs, field.Invalid(fieldPath.Child("logDest"), logDest, err.Error()))
   283  	}
   284  	return allErrs
   285  }
   286  
   287  func validateClientID(client string, fieldPath *field.Path) field.ErrorList {
   288  	allErrs := field.ErrorList{}
   289  
   290  	// isValidHeaderValue checks for $ and " in the string
   291  	if isValidHeaderValue(client) != nil {
   292  		allErrs = append(allErrs, field.Invalid(
   293  			fieldPath,
   294  			client,
   295  			`invalid string. String must contain valid ASCII characters, must have all '"' escaped and must not contain any '$' or end with an unescaped '\'
   296  		`))
   297  	}
   298  
   299  	return allErrs
   300  }
   301  
   302  var validScopes = map[string]bool{
   303  	"openid":  true,
   304  	"profile": true,
   305  	"email":   true,
   306  	"address": true,
   307  	"phone":   true,
   308  }
   309  
   310  // https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
   311  func validateOIDCScope(scope string, fieldPath *field.Path) field.ErrorList {
   312  	allErrs := field.ErrorList{}
   313  
   314  	if !strings.Contains(scope, "openid") {
   315  		return append(allErrs, field.Required(fieldPath, "openid scope"))
   316  	}
   317  
   318  	s := strings.Split(scope, "+")
   319  	for _, v := range s {
   320  		if !validScopes[v] {
   321  			msg := fmt.Sprintf("invalid Scope. Accepted scopes are: %v", mapToPrettyString(validScopes))
   322  			allErrs = append(allErrs, field.Invalid(fieldPath, v, msg))
   323  		}
   324  
   325  	}
   326  
   327  	return allErrs
   328  }
   329  
   330  func validateURL(name string, fieldPath *field.Path) field.ErrorList {
   331  	allErrs := field.ErrorList{}
   332  
   333  	u, err := url.Parse(name)
   334  	if err != nil {
   335  		return append(allErrs, field.Invalid(fieldPath, name, err.Error()))
   336  	}
   337  	var msg string
   338  	if u.Scheme == "" {
   339  		msg = "scheme required, please use the prefix http(s)://"
   340  		return append(allErrs, field.Invalid(fieldPath, name, msg))
   341  	}
   342  	if u.Host == "" {
   343  		msg = "hostname required"
   344  		return append(allErrs, field.Invalid(fieldPath, name, msg))
   345  	}
   346  	if u.Path == "" {
   347  		msg = "path required"
   348  		return append(allErrs, field.Invalid(fieldPath, name, msg))
   349  	}
   350  
   351  	host, port, err := net.SplitHostPort(u.Host)
   352  	if err != nil {
   353  		host = u.Host
   354  	}
   355  
   356  	allErrs = append(allErrs, validateSSLName(host, fieldPath)...)
   357  	if port != "" {
   358  		allErrs = append(allErrs, validatePortNumber(port, fieldPath)...)
   359  	}
   360  
   361  	return allErrs
   362  }
   363  
   364  func validatePortNumber(port string, fieldPath *field.Path) field.ErrorList {
   365  	allErrs := field.ErrorList{}
   366  	portInt, _ := strconv.Atoi(port)
   367  	msg := validation.IsValidPortNum(portInt)
   368  	if msg != nil {
   369  		allErrs = append(allErrs, field.Invalid(fieldPath, port, msg[0]))
   370  	}
   371  	return allErrs
   372  }
   373  
   374  func validateSSLName(name string, fieldPath *field.Path) field.ErrorList {
   375  	allErrs := field.ErrorList{}
   376  
   377  	if name != "" {
   378  		for _, msg := range validation.IsDNS1123Subdomain(name) {
   379  			allErrs = append(allErrs, field.Invalid(fieldPath, name, msg))
   380  		}
   381  	}
   382  	return allErrs
   383  }
   384  
   385  var validateVerifyClientKeyParameters = map[string]bool{
   386  	"on":             true,
   387  	"off":            true,
   388  	"optional":       true,
   389  	"optional_no_ca": true,
   390  }
   391  
   392  func validateIngressMTLSVerifyClient(verifyClient string, fieldPath *field.Path) field.ErrorList {
   393  	allErrs := field.ErrorList{}
   394  	if verifyClient != "" {
   395  		allErrs = append(allErrs, validateParameter(verifyClient, validateVerifyClientKeyParameters, fieldPath)...)
   396  	}
   397  	return allErrs
   398  }
   399  
   400  const (
   401  	rateFmt    = `[1-9]\d*r/[sSmM]`
   402  	rateErrMsg = "must consist of numeric characters followed by a valid rate suffix. 'r/s|r/m"
   403  )
   404  
   405  var rateRegexp = regexp.MustCompile("^" + rateFmt + "$")
   406  
   407  func validateRate(rate string, fieldPath *field.Path) field.ErrorList {
   408  	allErrs := field.ErrorList{}
   409  
   410  	if rate == "" {
   411  		return append(allErrs, field.Required(fieldPath, ""))
   412  	}
   413  
   414  	if !rateRegexp.MatchString(rate) {
   415  		msg := validation.RegexError(rateErrMsg, rateFmt, "16r/s", "32r/m", "64r/s")
   416  		return append(allErrs, field.Invalid(fieldPath, rate, msg))
   417  	}
   418  	return allErrs
   419  }
   420  
   421  func validateRateLimitZoneSize(zoneSize string, fieldPath *field.Path) field.ErrorList {
   422  	allErrs := field.ErrorList{}
   423  
   424  	if zoneSize == "" {
   425  		return append(allErrs, field.Required(fieldPath, ""))
   426  	}
   427  
   428  	allErrs = append(allErrs, validateSize(zoneSize, fieldPath)...)
   429  
   430  	kbZoneSize := strings.TrimSuffix(strings.ToLower(zoneSize), "k")
   431  	kbZoneSizeNum, err := strconv.Atoi(kbZoneSize)
   432  
   433  	mbZoneSize := strings.TrimSuffix(strings.ToLower(zoneSize), "m")
   434  	mbZoneSizeNum, mbErr := strconv.Atoi(mbZoneSize)
   435  
   436  	if err == nil && kbZoneSizeNum < 32 || mbErr == nil && mbZoneSizeNum == 0 {
   437  		allErrs = append(allErrs, field.Invalid(fieldPath, zoneSize, "must be greater than 31k"))
   438  	}
   439  
   440  	return allErrs
   441  }
   442  
   443  var rateLimitKeySpecialVariables = []string{"arg_", "http_", "cookie_"}
   444  
   445  // rateLimitKeyVariables includes NGINX variables allowed to be used in a rateLimit policy key.
   446  var rateLimitKeyVariables = map[string]bool{
   447  	"binary_remote_addr": true,
   448  	"request_uri":        true,
   449  	"uri":                true,
   450  	"args":               true,
   451  }
   452  
   453  func validateRateLimitKey(key string, fieldPath *field.Path, isPlus bool) field.ErrorList {
   454  	allErrs := field.ErrorList{}
   455  
   456  	if key == "" {
   457  		return append(allErrs, field.Required(fieldPath, ""))
   458  	}
   459  
   460  	if !escapedStringsFmtRegexp.MatchString(key) {
   461  		msg := validation.RegexError(escapedStringsErrMsg, escapedStringsFmt, `Hello World! \n`, `\"${request_uri}\" is unavailable. \n`)
   462  		allErrs = append(allErrs, field.Invalid(fieldPath, key, msg))
   463  	}
   464  
   465  	allErrs = append(allErrs, validateStringWithVariables(key, fieldPath, rateLimitKeySpecialVariables, rateLimitKeyVariables, isPlus)...)
   466  
   467  	return allErrs
   468  }
   469  
   470  var jwtTokenSpecialVariables = []string{"arg_", "http_", "cookie_"}
   471  
   472  func validateJWTToken(token string, fieldPath *field.Path) field.ErrorList {
   473  	allErrs := field.ErrorList{}
   474  
   475  	if token == "" {
   476  		return allErrs
   477  	}
   478  
   479  	nginxVars := strings.Split(token, "$")
   480  	if len(nginxVars) != 2 {
   481  		return append(allErrs, field.Invalid(fieldPath, token, "must have 1 var"))
   482  	}
   483  	nVar := token[1:]
   484  
   485  	special := false
   486  	for _, specialVar := range jwtTokenSpecialVariables {
   487  		if strings.HasPrefix(nVar, specialVar) {
   488  			special = true
   489  			break
   490  		}
   491  	}
   492  
   493  	if special {
   494  		// validateJWTToken is called only when NGINX Plus is running
   495  		isPlus := true
   496  		allErrs = append(allErrs, validateSpecialVariable(nVar, fieldPath, isPlus)...)
   497  	} else {
   498  		return append(allErrs, field.Invalid(fieldPath, token, "must only have special vars"))
   499  	}
   500  
   501  	return allErrs
   502  }
   503  
   504  var validLogLevels = map[string]bool{
   505  	"info":   true,
   506  	"notice": true,
   507  	"warn":   true,
   508  	"error":  true,
   509  }
   510  
   511  func validateRateLimitLogLevel(logLevel string, fieldPath *field.Path) field.ErrorList {
   512  	allErrs := field.ErrorList{}
   513  
   514  	if !validLogLevels[logLevel] {
   515  		allErrs = append(allErrs, field.Invalid(fieldPath, logLevel, fmt.Sprintf("Accepted values: %s",
   516  			mapToPrettyString(validLogLevels))))
   517  	}
   518  
   519  	return allErrs
   520  }
   521  
   522  const (
   523  	jwtRealmFmt              = `([^"$\\]|\\[^$])*`
   524  	jwtRealmFmtErrMsg string = `a valid realm must have all '"' escaped and must not contain any '$' or end with an unescaped '\'`
   525  )
   526  
   527  var jwtRealmFmtRegexp = regexp.MustCompile("^" + jwtRealmFmt + "$")
   528  
   529  func validateJWTRealm(realm string, fieldPath *field.Path) field.ErrorList {
   530  	allErrs := field.ErrorList{}
   531  
   532  	if realm == "" {
   533  		return append(allErrs, field.Required(fieldPath, ""))
   534  	}
   535  
   536  	if !jwtRealmFmtRegexp.MatchString(realm) {
   537  		msg := validation.RegexError(jwtRealmFmtErrMsg, jwtRealmFmt, "MyAPI", "My Product API")
   538  		allErrs = append(allErrs, field.Invalid(fieldPath, realm, msg))
   539  	}
   540  
   541  	return allErrs
   542  }
   543  
   544  func validateIPorCIDR(ipOrCIDR string, fieldPath *field.Path) field.ErrorList {
   545  	allErrs := field.ErrorList{}
   546  
   547  	_, _, err := net.ParseCIDR(ipOrCIDR)
   548  	if err == nil {
   549  		// valid CIDR
   550  		return allErrs
   551  	}
   552  
   553  	ip := net.ParseIP(ipOrCIDR)
   554  	if ip != nil {
   555  		// valid IP
   556  		return allErrs
   557  	}
   558  
   559  	return append(allErrs, field.Invalid(fieldPath, ipOrCIDR, "must be a CIDR or IP"))
   560  }
   561  
   562  func validatePositiveInt(n int, fieldPath *field.Path) field.ErrorList {
   563  	allErrs := field.ErrorList{}
   564  
   565  	if n <= 0 {
   566  		return append(allErrs, field.Invalid(fieldPath, n, "must be positive"))
   567  	}
   568  
   569  	return allErrs
   570  }