github.com/nginxinc/kubernetes-ingress@v1.12.5/internal/k8s/validation.go (about)

     1  package k8s
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"sort"
     7  	"strings"
     8  
     9  	"github.com/nginxinc/kubernetes-ingress/internal/configs"
    10  	networking "k8s.io/api/networking/v1beta1"
    11  	"k8s.io/apimachinery/pkg/util/sets"
    12  	"k8s.io/apimachinery/pkg/util/validation/field"
    13  )
    14  
    15  const (
    16  	mergeableIngressTypeAnnotation        = "nginx.org/mergeable-ingress-type"
    17  	lbMethodAnnotation                    = "nginx.org/lb-method"
    18  	healthChecksAnnotation                = "nginx.com/health-checks"
    19  	healthChecksMandatoryAnnotation       = "nginx.com/health-checks-mandatory"
    20  	healthChecksMandatoryQueueAnnotation  = "nginx.com/health-checks-mandatory-queue"
    21  	slowStartAnnotation                   = "nginx.com/slow-start"
    22  	serverTokensAnnotation                = "nginx.org/server-tokens" // #nosec G101
    23  	serverSnippetsAnnotation              = "nginx.org/server-snippets"
    24  	locationSnippetsAnnotation            = "nginx.org/location-snippets"
    25  	proxyConnectTimeoutAnnotation         = "nginx.org/proxy-connect-timeout"
    26  	proxyReadTimeoutAnnotation            = "nginx.org/proxy-read-timeout"
    27  	proxySendTimeoutAnnotation            = "nginx.org/proxy-send-timeout"
    28  	proxyHideHeadersAnnotation            = "nginx.org/proxy-hide-headers"
    29  	proxyPassHeadersAnnotation            = "nginx.org/proxy-pass-headers" // #nosec G101
    30  	clientMaxBodySizeAnnotation           = "nginx.org/client-max-body-size"
    31  	redirectToHTTPSAnnotation             = "nginx.org/redirect-to-https"
    32  	sslRedirectAnnotation                 = "ingress.kubernetes.io/ssl-redirect"
    33  	proxyBufferingAnnotation              = "nginx.org/proxy-buffering"
    34  	hstsAnnotation                        = "nginx.org/hsts"
    35  	hstsMaxAgeAnnotation                  = "nginx.org/hsts-max-age"
    36  	hstsIncludeSubdomainsAnnotation       = "nginx.org/hsts-include-subdomains"
    37  	hstsBehindProxyAnnotation             = "nginx.org/hsts-behind-proxy"
    38  	proxyBuffersAnnotation                = "nginx.org/proxy-buffers"
    39  	proxyBufferSizeAnnotation             = "nginx.org/proxy-buffer-size"
    40  	proxyMaxTempFileSizeAnnotation        = "nginx.org/proxy-max-temp-file-size"
    41  	upstreamZoneSizeAnnotation            = "nginx.org/upstream-zone-size"
    42  	jwtRealmAnnotation                    = "nginx.com/jwt-realm"
    43  	jwtKeyAnnotation                      = "nginx.com/jwt-key"
    44  	jwtTokenAnnotation                    = "nginx.com/jwt-token" // #nosec G101
    45  	jwtLoginURLAnnotation                 = "nginx.com/jwt-login-url"
    46  	listenPortsAnnotation                 = "nginx.org/listen-ports"
    47  	listenPortsSSLAnnotation              = "nginx.org/listen-ports-ssl"
    48  	keepaliveAnnotation                   = "nginx.org/keepalive"
    49  	maxFailsAnnotation                    = "nginx.org/max-fails"
    50  	maxConnsAnnotation                    = "nginx.org/max-conns"
    51  	failTimeoutAnnotation                 = "nginx.org/fail-timeout"
    52  	appProtectEnableAnnotation            = "appprotect.f5.com/app-protect-enable"
    53  	appProtectSecurityLogEnableAnnotation = "appprotect.f5.com/app-protect-security-log-enable"
    54  	internalRouteAnnotation               = "nsm.nginx.com/internal-route"
    55  	websocketServicesAnnotation           = "nginx.org/websocket-services"
    56  	sslServicesAnnotation                 = "nginx.org/ssl-services"
    57  	grpcServicesAnnotation                = "nginx.org/grpc-services"
    58  	rewritesAnnotation                    = "nginx.org/rewrites"
    59  	stickyCookieServicesAnnotation        = "nginx.com/sticky-cookie-services"
    60  )
    61  
    62  type annotationValidationContext struct {
    63  	annotations           map[string]string
    64  	specServices          map[string]bool
    65  	name                  string
    66  	value                 string
    67  	isPlus                bool
    68  	appProtectEnabled     bool
    69  	internalRoutesEnabled bool
    70  	fieldPath             *field.Path
    71  	snippetsEnabled       bool
    72  }
    73  
    74  type (
    75  	annotationValidationFunc   func(context *annotationValidationContext) field.ErrorList
    76  	annotationValidationConfig map[string][]annotationValidationFunc
    77  	validatorFunc              func(val string) error
    78  )
    79  
    80  var (
    81  	// annotationValidations defines the various validations which will be applied in order to each ingress annotation.
    82  	// If any specified validation fails, the remaining validations for that annotation will not be run.
    83  	annotationValidations = annotationValidationConfig{
    84  		mergeableIngressTypeAnnotation: {
    85  			validateRequiredAnnotation,
    86  			validateMergeableIngressTypeAnnotation,
    87  		},
    88  		lbMethodAnnotation: {
    89  			validateRequiredAnnotation,
    90  			validateLBMethodAnnotation,
    91  		},
    92  		healthChecksAnnotation: {
    93  			validatePlusOnlyAnnotation,
    94  			validateRequiredAnnotation,
    95  			validateBoolAnnotation,
    96  		},
    97  		healthChecksMandatoryAnnotation: {
    98  			validatePlusOnlyAnnotation,
    99  			validateRelatedAnnotation(healthChecksAnnotation, validateIsTrue),
   100  			validateRequiredAnnotation,
   101  			validateBoolAnnotation,
   102  		},
   103  		healthChecksMandatoryQueueAnnotation: {
   104  			validatePlusOnlyAnnotation,
   105  			validateRelatedAnnotation(healthChecksMandatoryAnnotation, validateIsTrue),
   106  			validateRequiredAnnotation,
   107  			validateUint64Annotation,
   108  		},
   109  		slowStartAnnotation: {
   110  			validatePlusOnlyAnnotation,
   111  			validateRequiredAnnotation,
   112  			validateTimeAnnotation,
   113  		},
   114  		serverTokensAnnotation: {
   115  			validateRequiredAnnotation,
   116  			validateServerTokensAnnotation,
   117  		},
   118  		serverSnippetsAnnotation: {
   119  			validateSnippetsAnnotation,
   120  		},
   121  		locationSnippetsAnnotation: {
   122  			validateSnippetsAnnotation,
   123  		},
   124  		proxyConnectTimeoutAnnotation: {
   125  			validateRequiredAnnotation,
   126  			validateTimeAnnotation,
   127  		},
   128  		proxyReadTimeoutAnnotation: {
   129  			validateRequiredAnnotation,
   130  			validateTimeAnnotation,
   131  		},
   132  		proxySendTimeoutAnnotation: {
   133  			validateRequiredAnnotation,
   134  			validateTimeAnnotation,
   135  		},
   136  		proxyHideHeadersAnnotation: {},
   137  		proxyPassHeadersAnnotation: {},
   138  		clientMaxBodySizeAnnotation: {
   139  			validateRequiredAnnotation,
   140  			validateOffsetAnnotation,
   141  		},
   142  		redirectToHTTPSAnnotation: {
   143  			validateRequiredAnnotation,
   144  			validateBoolAnnotation,
   145  		},
   146  		sslRedirectAnnotation: {
   147  			validateRequiredAnnotation,
   148  			validateBoolAnnotation,
   149  		},
   150  		proxyBufferingAnnotation: {
   151  			validateRequiredAnnotation,
   152  			validateBoolAnnotation,
   153  		},
   154  		hstsAnnotation: {
   155  			validateRequiredAnnotation,
   156  			validateBoolAnnotation,
   157  		},
   158  		hstsMaxAgeAnnotation: {
   159  			validateRelatedAnnotation(hstsAnnotation, validateIsBool),
   160  			validateRequiredAnnotation,
   161  			validateInt64Annotation,
   162  		},
   163  		hstsIncludeSubdomainsAnnotation: {
   164  			validateRelatedAnnotation(hstsAnnotation, validateIsBool),
   165  			validateRequiredAnnotation,
   166  			validateBoolAnnotation,
   167  		},
   168  		hstsBehindProxyAnnotation: {
   169  			validateRelatedAnnotation(hstsAnnotation, validateIsBool),
   170  			validateRequiredAnnotation,
   171  			validateBoolAnnotation,
   172  		},
   173  		proxyBuffersAnnotation: {
   174  			validateRequiredAnnotation,
   175  			validateProxyBuffersAnnotation,
   176  		},
   177  		proxyBufferSizeAnnotation: {
   178  			validateRequiredAnnotation,
   179  			validateSizeAnnotation,
   180  		},
   181  		proxyMaxTempFileSizeAnnotation: {
   182  			validateRequiredAnnotation,
   183  			validateSizeAnnotation,
   184  		},
   185  		upstreamZoneSizeAnnotation: {
   186  			validateRequiredAnnotation,
   187  			validateSizeAnnotation,
   188  		},
   189  		jwtRealmAnnotation: {
   190  			validatePlusOnlyAnnotation,
   191  		},
   192  		jwtKeyAnnotation: {
   193  			validatePlusOnlyAnnotation,
   194  		},
   195  		jwtTokenAnnotation: {
   196  			validatePlusOnlyAnnotation,
   197  		},
   198  		jwtLoginURLAnnotation: {
   199  			validatePlusOnlyAnnotation,
   200  		},
   201  		listenPortsAnnotation: {
   202  			validateRequiredAnnotation,
   203  			validatePortListAnnotation,
   204  		},
   205  		listenPortsSSLAnnotation: {
   206  			validateRequiredAnnotation,
   207  			validatePortListAnnotation,
   208  		},
   209  		keepaliveAnnotation: {
   210  			validateRequiredAnnotation,
   211  			validateIntAnnotation,
   212  		},
   213  		maxFailsAnnotation: {
   214  			validateRequiredAnnotation,
   215  			validateUint64Annotation,
   216  		},
   217  		maxConnsAnnotation: {
   218  			validateRequiredAnnotation,
   219  			validateUint64Annotation,
   220  		},
   221  		failTimeoutAnnotation: {
   222  			validateRequiredAnnotation,
   223  			validateTimeAnnotation,
   224  		},
   225  		appProtectEnableAnnotation: {
   226  			validateAppProtectOnlyAnnotation,
   227  			validateRequiredAnnotation,
   228  			validateBoolAnnotation,
   229  		},
   230  		appProtectSecurityLogEnableAnnotation: {
   231  			validateAppProtectOnlyAnnotation,
   232  			validateRequiredAnnotation,
   233  			validateBoolAnnotation,
   234  		},
   235  		internalRouteAnnotation: {
   236  			validateInternalRoutesOnlyAnnotation,
   237  			validateRequiredAnnotation,
   238  			validateBoolAnnotation,
   239  		},
   240  		websocketServicesAnnotation: {
   241  			validateRequiredAnnotation,
   242  			validateServiceListAnnotation,
   243  		},
   244  		sslServicesAnnotation: {
   245  			validateRequiredAnnotation,
   246  			validateServiceListAnnotation,
   247  		},
   248  		grpcServicesAnnotation: {
   249  			validateRequiredAnnotation,
   250  			validateServiceListAnnotation,
   251  		},
   252  		rewritesAnnotation: {
   253  			validateRequiredAnnotation,
   254  			validateRewriteListAnnotation,
   255  		},
   256  		stickyCookieServicesAnnotation: {
   257  			validatePlusOnlyAnnotation,
   258  			validateRequiredAnnotation,
   259  			validateStickyServiceListAnnotation,
   260  		},
   261  	}
   262  	annotationNames = sortedAnnotationNames(annotationValidations)
   263  )
   264  
   265  func sortedAnnotationNames(annotationValidations annotationValidationConfig) []string {
   266  	sortedNames := make([]string, 0)
   267  	for annotationName := range annotationValidations {
   268  		sortedNames = append(sortedNames, annotationName)
   269  	}
   270  	sort.Strings(sortedNames)
   271  	return sortedNames
   272  }
   273  
   274  // validateIngress validate an Ingress resource with rules that our Ingress Controller enforces.
   275  // Note that the full validation of Ingress resources is done by Kubernetes.
   276  func validateIngress(
   277  	ing *networking.Ingress,
   278  	isPlus bool,
   279  	appProtectEnabled bool,
   280  	internalRoutesEnabled bool,
   281  	snippetsEnabled bool,
   282  ) field.ErrorList {
   283  	allErrs := field.ErrorList{}
   284  	allErrs = append(allErrs, validateIngressAnnotations(
   285  		ing.Annotations,
   286  		getSpecServices(ing.Spec),
   287  		isPlus,
   288  		appProtectEnabled,
   289  		internalRoutesEnabled,
   290  		field.NewPath("annotations"),
   291  		snippetsEnabled,
   292  	)...)
   293  
   294  	allErrs = append(allErrs, validateIngressSpec(&ing.Spec, field.NewPath("spec"))...)
   295  
   296  	if isMaster(ing) {
   297  		allErrs = append(allErrs, validateMasterSpec(&ing.Spec, field.NewPath("spec"))...)
   298  	} else if isMinion(ing) {
   299  		allErrs = append(allErrs, validateMinionSpec(&ing.Spec, field.NewPath("spec"))...)
   300  	}
   301  
   302  	return allErrs
   303  }
   304  
   305  func validateIngressAnnotations(
   306  	annotations map[string]string,
   307  	specServices map[string]bool,
   308  	isPlus bool,
   309  	appProtectEnabled bool,
   310  	internalRoutesEnabled bool,
   311  	fieldPath *field.Path,
   312  	snippetsEnabled bool,
   313  ) field.ErrorList {
   314  	allErrs := field.ErrorList{}
   315  
   316  	for _, name := range annotationNames {
   317  		if value, exists := annotations[name]; exists {
   318  			context := &annotationValidationContext{
   319  				annotations:           annotations,
   320  				specServices:          specServices,
   321  				name:                  name,
   322  				value:                 value,
   323  				isPlus:                isPlus,
   324  				appProtectEnabled:     appProtectEnabled,
   325  				internalRoutesEnabled: internalRoutesEnabled,
   326  				fieldPath:             fieldPath.Child(name),
   327  				snippetsEnabled:       snippetsEnabled,
   328  			}
   329  			allErrs = append(allErrs, validateIngressAnnotation(context)...)
   330  		}
   331  	}
   332  
   333  	return allErrs
   334  }
   335  
   336  func validateIngressAnnotation(context *annotationValidationContext) field.ErrorList {
   337  	allErrs := field.ErrorList{}
   338  	if validationFuncs, exists := annotationValidations[context.name]; exists {
   339  		for _, validationFunc := range validationFuncs {
   340  			valErrors := validationFunc(context)
   341  			if len(valErrors) > 0 {
   342  				allErrs = append(allErrs, valErrors...)
   343  				break
   344  			}
   345  		}
   346  	}
   347  	return allErrs
   348  }
   349  
   350  func validateRelatedAnnotation(name string, validator validatorFunc) annotationValidationFunc {
   351  	return func(context *annotationValidationContext) field.ErrorList {
   352  		allErrs := field.ErrorList{}
   353  		val, exists := context.annotations[name]
   354  		if !exists {
   355  			return append(allErrs, field.Forbidden(context.fieldPath, fmt.Sprintf("related annotation %s: must be set", name)))
   356  		}
   357  
   358  		if err := validator(val); err != nil {
   359  			return append(allErrs, field.Forbidden(context.fieldPath, fmt.Sprintf("related annotation %s: %s", name, err.Error())))
   360  		}
   361  		return allErrs
   362  	}
   363  }
   364  
   365  func validateMergeableIngressTypeAnnotation(context *annotationValidationContext) field.ErrorList {
   366  	allErrs := field.ErrorList{}
   367  	if context.value != "master" && context.value != "minion" {
   368  		return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be one of: 'master' or 'minion'"))
   369  	}
   370  	return allErrs
   371  }
   372  
   373  func validateLBMethodAnnotation(context *annotationValidationContext) field.ErrorList {
   374  	allErrs := field.ErrorList{}
   375  
   376  	parseFunc := configs.ParseLBMethod
   377  	if context.isPlus {
   378  		parseFunc = configs.ParseLBMethodForPlus
   379  	}
   380  
   381  	if _, err := parseFunc(context.value); err != nil {
   382  		return append(allErrs, field.Invalid(context.fieldPath, context.value, err.Error()))
   383  	}
   384  	return allErrs
   385  }
   386  
   387  func validateServerTokensAnnotation(context *annotationValidationContext) field.ErrorList {
   388  	allErrs := field.ErrorList{}
   389  	if !context.isPlus {
   390  		if _, err := configs.ParseBool(context.value); err != nil {
   391  			return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be a boolean"))
   392  		}
   393  	}
   394  	return allErrs
   395  }
   396  
   397  func validateRequiredAnnotation(context *annotationValidationContext) field.ErrorList {
   398  	allErrs := field.ErrorList{}
   399  	if context.value == "" {
   400  		return append(allErrs, field.Required(context.fieldPath, ""))
   401  	}
   402  	return allErrs
   403  }
   404  
   405  func validatePlusOnlyAnnotation(context *annotationValidationContext) field.ErrorList {
   406  	allErrs := field.ErrorList{}
   407  	if !context.isPlus {
   408  		return append(allErrs, field.Forbidden(context.fieldPath, "annotation requires NGINX Plus"))
   409  	}
   410  	return allErrs
   411  }
   412  
   413  func validateAppProtectOnlyAnnotation(context *annotationValidationContext) field.ErrorList {
   414  	allErrs := field.ErrorList{}
   415  	if !context.appProtectEnabled {
   416  		return append(allErrs, field.Forbidden(context.fieldPath, "annotation requires AppProtect"))
   417  	}
   418  	return allErrs
   419  }
   420  
   421  func validateInternalRoutesOnlyAnnotation(context *annotationValidationContext) field.ErrorList {
   422  	allErrs := field.ErrorList{}
   423  	if !context.internalRoutesEnabled {
   424  		return append(allErrs, field.Forbidden(context.fieldPath, "annotation requires Internal Routes enabled"))
   425  	}
   426  	return allErrs
   427  }
   428  
   429  func validateBoolAnnotation(context *annotationValidationContext) field.ErrorList {
   430  	allErrs := field.ErrorList{}
   431  	if _, err := configs.ParseBool(context.value); err != nil {
   432  		return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be a boolean"))
   433  	}
   434  	return allErrs
   435  }
   436  
   437  func validateTimeAnnotation(context *annotationValidationContext) field.ErrorList {
   438  	allErrs := field.ErrorList{}
   439  	if _, err := configs.ParseTime(context.value); err != nil {
   440  		return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be a time"))
   441  	}
   442  	return allErrs
   443  }
   444  
   445  func validateOffsetAnnotation(context *annotationValidationContext) field.ErrorList {
   446  	allErrs := field.ErrorList{}
   447  	if _, err := configs.ParseOffset(context.value); err != nil {
   448  		return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be an offset"))
   449  	}
   450  	return allErrs
   451  }
   452  
   453  func validateSizeAnnotation(context *annotationValidationContext) field.ErrorList {
   454  	allErrs := field.ErrorList{}
   455  	if _, err := configs.ParseSize(context.value); err != nil {
   456  		return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be a size"))
   457  	}
   458  	return allErrs
   459  }
   460  
   461  func validateProxyBuffersAnnotation(context *annotationValidationContext) field.ErrorList {
   462  	allErrs := field.ErrorList{}
   463  	if _, err := configs.ParseProxyBuffersSpec(context.value); err != nil {
   464  		return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be a proxy buffer spec"))
   465  	}
   466  	return allErrs
   467  }
   468  
   469  func validateUint64Annotation(context *annotationValidationContext) field.ErrorList {
   470  	allErrs := field.ErrorList{}
   471  	if _, err := configs.ParseUint64(context.value); err != nil {
   472  		return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be a non-negative integer"))
   473  	}
   474  	return allErrs
   475  }
   476  
   477  func validateInt64Annotation(context *annotationValidationContext) field.ErrorList {
   478  	allErrs := field.ErrorList{}
   479  	if _, err := configs.ParseInt64(context.value); err != nil {
   480  		return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be an integer"))
   481  	}
   482  	return allErrs
   483  }
   484  
   485  func validateIntAnnotation(context *annotationValidationContext) field.ErrorList {
   486  	allErrs := field.ErrorList{}
   487  	if _, err := configs.ParseInt(context.value); err != nil {
   488  		return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be an integer"))
   489  	}
   490  	return allErrs
   491  }
   492  
   493  func validatePortListAnnotation(context *annotationValidationContext) field.ErrorList {
   494  	allErrs := field.ErrorList{}
   495  	if _, err := configs.ParsePortList(context.value); err != nil {
   496  		return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be a comma-separated list of port numbers"))
   497  	}
   498  	return allErrs
   499  }
   500  
   501  func validateServiceListAnnotation(context *annotationValidationContext) field.ErrorList {
   502  	allErrs := field.ErrorList{}
   503  	var unknownServices []string
   504  	annotationServices := configs.ParseServiceList(context.value)
   505  	for svc := range annotationServices {
   506  		if _, exists := context.specServices[svc]; !exists {
   507  			unknownServices = append(unknownServices, svc)
   508  		}
   509  	}
   510  	if len(unknownServices) > 0 {
   511  		errorMsg := fmt.Sprintf(
   512  			"must be a comma-separated list of services. The following services were not found: %s",
   513  			strings.Join(unknownServices, ","),
   514  		)
   515  		return append(allErrs, field.Invalid(context.fieldPath, context.value, errorMsg))
   516  	}
   517  	return allErrs
   518  }
   519  
   520  func validateStickyServiceListAnnotation(context *annotationValidationContext) field.ErrorList {
   521  	allErrs := field.ErrorList{}
   522  	if _, err := configs.ParseStickyServiceList(context.value); err != nil {
   523  		return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be a semicolon-separated list of sticky services"))
   524  	}
   525  	return allErrs
   526  }
   527  
   528  func validateRewriteListAnnotation(context *annotationValidationContext) field.ErrorList {
   529  	allErrs := field.ErrorList{}
   530  	if _, err := configs.ParseRewriteList(context.value); err != nil {
   531  		return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be a semicolon-separated list of rewrites"))
   532  	}
   533  	return allErrs
   534  }
   535  
   536  func validateSnippetsAnnotation(context *annotationValidationContext) field.ErrorList {
   537  	allErrs := field.ErrorList{}
   538  
   539  	if !context.snippetsEnabled {
   540  		return append(allErrs, field.Forbidden(context.fieldPath, "snippet specified but snippets feature is not enabled"))
   541  	}
   542  	return allErrs
   543  }
   544  
   545  func validateIsBool(v string) error {
   546  	_, err := configs.ParseBool(v)
   547  	return err
   548  }
   549  
   550  func validateIsTrue(v string) error {
   551  	b, err := configs.ParseBool(v)
   552  	if err != nil {
   553  		return err
   554  	}
   555  	if !b {
   556  		return errors.New("must be true")
   557  	}
   558  	return nil
   559  }
   560  
   561  func validateIngressSpec(spec *networking.IngressSpec, fieldPath *field.Path) field.ErrorList {
   562  	allErrs := field.ErrorList{}
   563  
   564  	allHosts := sets.String{}
   565  
   566  	if len(spec.Rules) == 0 {
   567  		return append(allErrs, field.Required(fieldPath.Child("rules"), ""))
   568  	}
   569  
   570  	for i, r := range spec.Rules {
   571  		idxPath := fieldPath.Child("rules").Index(i)
   572  
   573  		if r.Host == "" {
   574  			allErrs = append(allErrs, field.Required(idxPath.Child("host"), ""))
   575  		} else if allHosts.Has(r.Host) {
   576  			allErrs = append(allErrs, field.Duplicate(idxPath.Child("host"), r.Host))
   577  		} else {
   578  			allHosts.Insert(r.Host)
   579  		}
   580  	}
   581  
   582  	return allErrs
   583  }
   584  
   585  func validateMasterSpec(spec *networking.IngressSpec, fieldPath *field.Path) field.ErrorList {
   586  	allErrs := field.ErrorList{}
   587  
   588  	if len(spec.Rules) != 1 {
   589  		return append(allErrs, field.TooMany(fieldPath.Child("rules"), len(spec.Rules), 1))
   590  	}
   591  
   592  	// the number of paths of the first rule of the spec must be 0
   593  	if spec.Rules[0].HTTP != nil && len(spec.Rules[0].HTTP.Paths) > 0 {
   594  		pathsField := fieldPath.Child("rules").Index(0).Child("http").Child("paths")
   595  		return append(allErrs, field.TooMany(pathsField, len(spec.Rules[0].HTTP.Paths), 0))
   596  	}
   597  
   598  	return allErrs
   599  }
   600  
   601  func validateMinionSpec(spec *networking.IngressSpec, fieldPath *field.Path) field.ErrorList {
   602  	allErrs := field.ErrorList{}
   603  
   604  	if len(spec.TLS) > 0 {
   605  		allErrs = append(allErrs, field.TooMany(fieldPath.Child("tls"), len(spec.TLS), 0))
   606  	}
   607  
   608  	if len(spec.Rules) != 1 {
   609  		return append(allErrs, field.TooMany(fieldPath.Child("rules"), len(spec.Rules), 1))
   610  	}
   611  
   612  	// the number of paths of the first rule of the spec must be greater than 0
   613  	if spec.Rules[0].HTTP == nil || len(spec.Rules[0].HTTP.Paths) == 0 {
   614  		pathsField := fieldPath.Child("rules").Index(0).Child("http").Child("paths")
   615  		return append(allErrs, field.Required(pathsField, "must include at least one path"))
   616  	}
   617  
   618  	return allErrs
   619  }
   620  
   621  func getSpecServices(ingressSpec networking.IngressSpec) map[string]bool {
   622  	services := make(map[string]bool)
   623  	if ingressSpec.Backend != nil {
   624  		services[ingressSpec.Backend.ServiceName] = true
   625  	}
   626  	for _, rule := range ingressSpec.Rules {
   627  		if rule.HTTP != nil {
   628  			for _, path := range rule.HTTP.Paths {
   629  				services[path.Backend.ServiceName] = true
   630  			}
   631  		}
   632  	}
   633  	return services
   634  }