kubernetes/pkg/apis/networking/validation/validation.go
Christopher M. Luciano 912f05bafb
ingress: add alternate resource backend
Signed-off-by: Christopher M. Luciano <cmluciano@us.ibm.com>
2020-03-05 16:08:44 -05:00

523 lines
21 KiB
Go

/*
Copyright 2017 The Kubernetes 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 validation
import (
"fmt"
"net"
"regexp"
"strings"
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
pathvalidation "k8s.io/apimachinery/pkg/api/validation/path"
unversionedvalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
"k8s.io/kubernetes/pkg/apis/networking"
)
const (
annotationIngressClass = "kubernetes.io/ingress.class"
maxLenIngressClassController = 250
)
var (
supportedPathTypes = sets.NewString(
string(networking.PathTypeExact),
string(networking.PathTypePrefix),
string(networking.PathTypeImplementationSpecific),
)
invalidPathSequences = []string{"//", "/./", "/../", "%2f", "%2F"}
invalidPathSuffixes = []string{"/..", "/."}
)
// ValidateNetworkPolicyName can be used to check whether the given networkpolicy
// name is valid.
func ValidateNetworkPolicyName(name string, prefix bool) []string {
return apimachineryvalidation.NameIsDNSSubdomain(name, prefix)
}
// ValidateNetworkPolicyPort validates a NetworkPolicyPort
func ValidateNetworkPolicyPort(port *networking.NetworkPolicyPort, portPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if port.Protocol != nil && *port.Protocol != api.ProtocolTCP && *port.Protocol != api.ProtocolUDP && *port.Protocol != api.ProtocolSCTP {
allErrs = append(allErrs, field.NotSupported(portPath.Child("protocol"), *port.Protocol, []string{string(api.ProtocolTCP), string(api.ProtocolUDP), string(api.ProtocolSCTP)}))
}
if port.Port != nil {
if port.Port.Type == intstr.Int {
for _, msg := range validation.IsValidPortNum(int(port.Port.IntVal)) {
allErrs = append(allErrs, field.Invalid(portPath.Child("port"), port.Port.IntVal, msg))
}
} else {
for _, msg := range validation.IsValidPortName(port.Port.StrVal) {
allErrs = append(allErrs, field.Invalid(portPath.Child("port"), port.Port.StrVal, msg))
}
}
}
return allErrs
}
// ValidateNetworkPolicyPeer validates a NetworkPolicyPeer
func ValidateNetworkPolicyPeer(peer *networking.NetworkPolicyPeer, peerPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
numPeers := 0
if peer.PodSelector != nil {
numPeers++
allErrs = append(allErrs, unversionedvalidation.ValidateLabelSelector(peer.PodSelector, peerPath.Child("podSelector"))...)
}
if peer.NamespaceSelector != nil {
numPeers++
allErrs = append(allErrs, unversionedvalidation.ValidateLabelSelector(peer.NamespaceSelector, peerPath.Child("namespaceSelector"))...)
}
if peer.IPBlock != nil {
numPeers++
allErrs = append(allErrs, ValidateIPBlock(peer.IPBlock, peerPath.Child("ipBlock"))...)
}
if numPeers == 0 {
allErrs = append(allErrs, field.Required(peerPath, "must specify a peer"))
} else if numPeers > 1 && peer.IPBlock != nil {
allErrs = append(allErrs, field.Forbidden(peerPath, "may not specify both ipBlock and another peer"))
}
return allErrs
}
// ValidateNetworkPolicySpec tests if required fields in the networkpolicy spec are set.
func ValidateNetworkPolicySpec(spec *networking.NetworkPolicySpec, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
allErrs = append(allErrs, unversionedvalidation.ValidateLabelSelector(&spec.PodSelector, fldPath.Child("podSelector"))...)
// Validate ingress rules.
for i, ingress := range spec.Ingress {
ingressPath := fldPath.Child("ingress").Index(i)
for i, port := range ingress.Ports {
portPath := ingressPath.Child("ports").Index(i)
allErrs = append(allErrs, ValidateNetworkPolicyPort(&port, portPath)...)
}
for i, from := range ingress.From {
fromPath := ingressPath.Child("from").Index(i)
allErrs = append(allErrs, ValidateNetworkPolicyPeer(&from, fromPath)...)
}
}
// Validate egress rules
for i, egress := range spec.Egress {
egressPath := fldPath.Child("egress").Index(i)
for i, port := range egress.Ports {
portPath := egressPath.Child("ports").Index(i)
allErrs = append(allErrs, ValidateNetworkPolicyPort(&port, portPath)...)
}
for i, to := range egress.To {
toPath := egressPath.Child("to").Index(i)
allErrs = append(allErrs, ValidateNetworkPolicyPeer(&to, toPath)...)
}
}
// Validate PolicyTypes
allowed := sets.NewString(string(networking.PolicyTypeIngress), string(networking.PolicyTypeEgress))
if len(spec.PolicyTypes) > len(allowed) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("policyTypes"), &spec.PolicyTypes, "may not specify more than two policyTypes"))
return allErrs
}
for i, pType := range spec.PolicyTypes {
policyPath := fldPath.Child("policyTypes").Index(i)
if !allowed.Has(string(pType)) {
allErrs = append(allErrs, field.NotSupported(policyPath, pType, []string{string(networking.PolicyTypeIngress), string(networking.PolicyTypeEgress)}))
}
}
return allErrs
}
// ValidateNetworkPolicy validates a networkpolicy.
func ValidateNetworkPolicy(np *networking.NetworkPolicy) field.ErrorList {
allErrs := apivalidation.ValidateObjectMeta(&np.ObjectMeta, true, ValidateNetworkPolicyName, field.NewPath("metadata"))
allErrs = append(allErrs, ValidateNetworkPolicySpec(&np.Spec, field.NewPath("spec"))...)
return allErrs
}
// ValidateNetworkPolicyUpdate tests if an update to a NetworkPolicy is valid.
func ValidateNetworkPolicyUpdate(update, old *networking.NetworkPolicy) field.ErrorList {
allErrs := field.ErrorList{}
allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&update.ObjectMeta, &old.ObjectMeta, field.NewPath("metadata"))...)
allErrs = append(allErrs, ValidateNetworkPolicySpec(&update.Spec, field.NewPath("spec"))...)
return allErrs
}
// ValidateIPBlock validates a cidr and the except fields of an IpBlock NetworkPolicyPeer
func ValidateIPBlock(ipb *networking.IPBlock, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if len(ipb.CIDR) == 0 || ipb.CIDR == "" {
allErrs = append(allErrs, field.Required(fldPath.Child("cidr"), ""))
return allErrs
}
cidrIPNet, err := apivalidation.ValidateCIDR(ipb.CIDR)
if err != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("cidr"), ipb.CIDR, "not a valid CIDR"))
return allErrs
}
exceptCIDR := ipb.Except
for i, exceptIP := range exceptCIDR {
exceptPath := fldPath.Child("except").Index(i)
exceptCIDR, err := apivalidation.ValidateCIDR(exceptIP)
if err != nil {
allErrs = append(allErrs, field.Invalid(exceptPath, exceptIP, "not a valid CIDR"))
return allErrs
}
cidrMaskLen, _ := cidrIPNet.Mask.Size()
exceptMaskLen, _ := exceptCIDR.Mask.Size()
if !cidrIPNet.Contains(exceptCIDR.IP) || cidrMaskLen >= exceptMaskLen {
allErrs = append(allErrs, field.Invalid(exceptPath, exceptIP, "must be a strict subset of `cidr`"))
}
}
return allErrs
}
// ValidateIngressName validates that the given name can be used as an Ingress
// name.
var ValidateIngressName = apimachineryvalidation.NameIsDNSSubdomain
// IngressValidationOptions cover beta to GA transitions for HTTP PathType
type IngressValidationOptions struct {
requireRegexPath bool
allowResourceBackend bool
}
// ValidateIngress validates Ingresses on create and update.
func validateIngress(ingress *networking.Ingress, opts IngressValidationOptions, requestGV schema.GroupVersion) field.ErrorList {
allErrs := apivalidation.ValidateObjectMeta(&ingress.ObjectMeta, true, ValidateIngressName, field.NewPath("metadata"))
allErrs = append(allErrs, ValidateIngressSpec(&ingress.Spec, field.NewPath("spec"), opts, requestGV)...)
return allErrs
}
// ValidateIngressCreate validates Ingresses on create.
func ValidateIngressCreate(ingress *networking.Ingress, requestGV schema.GroupVersion) field.ErrorList {
allErrs := field.ErrorList{}
var opts IngressValidationOptions
opts = IngressValidationOptions{
// TODO(robscott): Remove regex validation for 1.19.
requireRegexPath: true,
// TODO(cmluciano): Allow resource backend for 1.19.
allowResourceBackend: false,
}
allErrs = append(allErrs, validateIngress(ingress, opts, requestGV)...)
annotationVal, annotationIsSet := ingress.Annotations[annotationIngressClass]
if annotationIsSet && ingress.Spec.IngressClassName != nil {
annotationPath := field.NewPath("annotations").Child(annotationIngressClass)
allErrs = append(allErrs, field.Invalid(annotationPath, annotationVal, "can not be set when the class field is also set"))
}
return allErrs
}
// ValidateIngressUpdate validates ingresses on update.
func ValidateIngressUpdate(ingress, oldIngress *networking.Ingress, requestGV schema.GroupVersion) field.ErrorList {
allErrs := apivalidation.ValidateObjectMetaUpdate(&ingress.ObjectMeta, &oldIngress.ObjectMeta, field.NewPath("metadata"))
var opts IngressValidationOptions
opts = IngressValidationOptions{
// TODO(robscott): Remove regex validation for 1.19.
// Only require regex path validation for this Ingress if the previous
// version of the Ingress also passed that validation.
requireRegexPath: allPathsPassRegexValidation(oldIngress),
allowResourceBackend: resourceBackendPresent(oldIngress),
}
allErrs = append(allErrs, validateIngress(ingress, opts, requestGV)...)
return allErrs
}
func validateIngressTLS(spec *networking.IngressSpec, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
// TODO: Perform a more thorough validation of spec.TLS.Hosts that takes
// the wildcard spec from RFC 6125 into account.
for _, itls := range spec.TLS {
for i, host := range itls.Hosts {
if strings.Contains(host, "*") {
for _, msg := range validation.IsWildcardDNS1123Subdomain(host) {
allErrs = append(allErrs, field.Invalid(fldPath.Index(i).Child("hosts"), host, msg))
}
continue
}
for _, msg := range validation.IsDNS1123Subdomain(host) {
allErrs = append(allErrs, field.Invalid(fldPath.Index(i).Child("hosts"), host, msg))
}
}
}
return allErrs
}
// ValidateIngressSpec tests if required fields in the IngressSpec are set.
func ValidateIngressSpec(spec *networking.IngressSpec, fldPath *field.Path, opts IngressValidationOptions, requestGV schema.GroupVersion) field.ErrorList {
allErrs := field.ErrorList{}
if len(spec.Rules) == 0 && spec.Backend == nil {
errMsg := "either `backend` or `rules` must be specified"
allErrs = append(allErrs, field.Invalid(fldPath, spec.Rules, errMsg))
}
if spec.Backend != nil {
allErrs = append(allErrs, validateIngressBackend(spec.Backend, fldPath.Child("backend"), opts)...)
}
if len(spec.Rules) > 0 {
allErrs = append(allErrs, validateIngressRules(spec.Rules, fldPath.Child("rules"), opts)...)
}
if len(spec.TLS) > 0 {
allErrs = append(allErrs, validateIngressTLS(spec, fldPath.Child("tls"))...)
}
if spec.IngressClassName != nil {
for _, msg := range ValidateIngressClassName(*spec.IngressClassName, false) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("ingressClassName"), *spec.IngressClassName, msg))
}
}
return allErrs
}
// ValidateIngressStatusUpdate tests if required fields in the Ingress are set when updating status.
func ValidateIngressStatusUpdate(ingress, oldIngress *networking.Ingress) field.ErrorList {
allErrs := apivalidation.ValidateObjectMetaUpdate(&ingress.ObjectMeta, &oldIngress.ObjectMeta, field.NewPath("metadata"))
allErrs = append(allErrs, apivalidation.ValidateLoadBalancerStatus(&ingress.Status.LoadBalancer, field.NewPath("status", "loadBalancer"))...)
return allErrs
}
func validateIngressRules(ingressRules []networking.IngressRule, fldPath *field.Path, opts IngressValidationOptions) field.ErrorList {
allErrs := field.ErrorList{}
if len(ingressRules) == 0 {
return append(allErrs, field.Required(fldPath, ""))
}
for i, ih := range ingressRules {
if len(ih.Host) > 0 {
if isIP := (net.ParseIP(ih.Host) != nil); isIP {
allErrs = append(allErrs, field.Invalid(fldPath.Index(i).Child("host"), ih.Host, "must be a DNS name, not an IP address"))
}
// TODO: Ports and ips are allowed in the host part of a url
// according to RFC 3986, consider allowing them.
if strings.Contains(ih.Host, "*") {
for _, msg := range validation.IsWildcardDNS1123Subdomain(ih.Host) {
allErrs = append(allErrs, field.Invalid(fldPath.Index(i).Child("host"), ih.Host, msg))
}
continue
}
for _, msg := range validation.IsDNS1123Subdomain(ih.Host) {
allErrs = append(allErrs, field.Invalid(fldPath.Index(i).Child("host"), ih.Host, msg))
}
}
allErrs = append(allErrs, validateIngressRuleValue(&ih.IngressRuleValue, fldPath.Index(0), opts)...)
}
return allErrs
}
func validateIngressRuleValue(ingressRule *networking.IngressRuleValue, fldPath *field.Path, opts IngressValidationOptions) field.ErrorList {
allErrs := field.ErrorList{}
if ingressRule.HTTP != nil {
allErrs = append(allErrs, validateHTTPIngressRuleValue(ingressRule.HTTP, fldPath.Child("http"), opts)...)
}
return allErrs
}
func validateHTTPIngressRuleValue(httpIngressRuleValue *networking.HTTPIngressRuleValue, fldPath *field.Path, opts IngressValidationOptions) field.ErrorList {
allErrs := field.ErrorList{}
if len(httpIngressRuleValue.Paths) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("paths"), ""))
}
for i, path := range httpIngressRuleValue.Paths {
allErrs = append(allErrs, validateHTTPIngressPath(&path, fldPath.Child("paths").Index(i), opts)...)
}
return allErrs
}
func validateHTTPIngressPath(path *networking.HTTPIngressPath, fldPath *field.Path, opts IngressValidationOptions) field.ErrorList {
allErrs := field.ErrorList{}
if path.PathType == nil {
return append(allErrs, field.Required(fldPath.Child("pathType"), "pathType must be specified"))
}
switch *path.PathType {
case networking.PathTypeExact, networking.PathTypePrefix:
if !strings.HasPrefix(path.Path, "/") {
allErrs = append(allErrs, field.Invalid(fldPath.Child("path"), path.Path, "must be an absolute path"))
}
if len(path.Path) > 0 {
for _, invalidSeq := range invalidPathSequences {
if strings.Contains(path.Path, invalidSeq) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("path"), path.Path, fmt.Sprintf("must not contain '%s'", invalidSeq)))
}
}
for _, invalidSuff := range invalidPathSuffixes {
if strings.HasSuffix(path.Path, invalidSuff) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("path"), path.Path, fmt.Sprintf("cannot end with '%s'", invalidSuff)))
}
}
}
case networking.PathTypeImplementationSpecific:
if len(path.Path) > 0 {
if !strings.HasPrefix(path.Path, "/") {
allErrs = append(allErrs, field.Invalid(fldPath.Child("path"), path.Path, "must be an absolute path"))
}
}
default:
allErrs = append(allErrs, field.NotSupported(fldPath.Child("pathType"), *path.PathType, supportedPathTypes.List()))
}
// TODO(robscott): Remove regex validation for 1.19.
if opts.requireRegexPath {
_, err := regexp.CompilePOSIX(path.Path)
if err != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("path"), path.Path, "must be a valid regex"))
}
}
allErrs = append(allErrs, validateIngressBackend(&path.Backend, fldPath.Child("backend"), opts)...)
return allErrs
}
// validateIngressBackend tests if a given backend is valid.
func validateIngressBackend(backend *networking.IngressBackend, fldPath *field.Path, opts IngressValidationOptions) field.ErrorList {
allErrs := field.ErrorList{}
hasResourceBackend := backend.Resource != nil
hasServiceBackend := len(backend.ServiceName) > 0 || backend.ServicePort.IntVal != 0 || len(backend.ServicePort.StrVal) > 0
switch {
case hasResourceBackend && hasServiceBackend:
return append(allErrs, field.Invalid(fldPath, "", "cannot set both resource and service backends"))
case hasResourceBackend && !opts.allowResourceBackend:
return append(allErrs, field.Forbidden(fldPath.Child("resource"), "not supported; only service backends are supported in this version"))
case hasResourceBackend:
allErrs = append(allErrs, validateIngressTypedLocalObjectReference(backend.Resource, fldPath.Child("resource"))...)
default:
if len(backend.ServiceName) == 0 {
return append(allErrs, field.Required(fldPath.Child("serviceName"), ""))
}
for _, msg := range apivalidation.ValidateServiceName(backend.ServiceName, false) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("serviceName"), backend.ServiceName, msg))
}
allErrs = append(allErrs, apivalidation.ValidatePortNumOrName(backend.ServicePort, fldPath.Child("servicePort"))...)
}
return allErrs
}
// ValidateIngressClassName validates that the given name can be used as an
// IngressClass name.
var ValidateIngressClassName = apimachineryvalidation.NameIsDNSSubdomain
// ValidateIngressClass ensures that IngressClass resources are valid.
func ValidateIngressClass(ingressClass *networking.IngressClass) field.ErrorList {
allErrs := apivalidation.ValidateObjectMeta(&ingressClass.ObjectMeta, false, ValidateIngressClassName, field.NewPath("metadata"))
allErrs = append(allErrs, validateIngressClassSpec(&ingressClass.Spec, field.NewPath("spec"))...)
return allErrs
}
// ValidateIngressClassUpdate ensures that IngressClass updates are valid.
func ValidateIngressClassUpdate(newIngressClass, oldIngressClass *networking.IngressClass) field.ErrorList {
allErrs := apivalidation.ValidateObjectMetaUpdate(&newIngressClass.ObjectMeta, &oldIngressClass.ObjectMeta, field.NewPath("metadata"))
allErrs = append(allErrs, validateIngressClassSpecUpdate(&newIngressClass.Spec, &oldIngressClass.Spec, field.NewPath("spec"))...)
allErrs = append(allErrs, ValidateIngressClass(newIngressClass)...)
return allErrs
}
// validateIngressClassSpec ensures that IngressClassSpec fields are valid.
func validateIngressClassSpec(spec *networking.IngressClassSpec, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if len(spec.Controller) > maxLenIngressClassController {
allErrs = append(allErrs, field.TooLong(fldPath.Child("controller"), spec.Controller, maxLenIngressClassController))
}
allErrs = append(allErrs, validation.IsDomainPrefixedPath(fldPath.Child("controller"), spec.Controller)...)
allErrs = append(allErrs, validateIngressTypedLocalObjectReference(spec.Parameters, fldPath.Child("parameters"))...)
return allErrs
}
// validateIngressClassSpecUpdate ensures that IngressClassSpec updates are
// valid.
func validateIngressClassSpecUpdate(newSpec, oldSpec *networking.IngressClassSpec, fldPath *field.Path) field.ErrorList {
return apivalidation.ValidateImmutableField(newSpec.Controller, oldSpec.Controller, fldPath.Child("controller"))
}
// validateIngressTypedLocalObjectReference ensures that Parameters fields are valid.
func validateIngressTypedLocalObjectReference(params *api.TypedLocalObjectReference, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if params == nil {
return allErrs
}
if params.APIGroup != nil {
for _, msg := range validation.IsDNS1123Subdomain(*params.APIGroup) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("apiGroup"), *params.APIGroup, msg))
}
}
if params.Kind == "" {
allErrs = append(allErrs, field.Required(fldPath.Child("kind"), "kind is required"))
} else {
for _, msg := range pathvalidation.IsValidPathSegmentName(params.Kind) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("kind"), params.Kind, msg))
}
}
if params.Name == "" {
allErrs = append(allErrs, field.Required(fldPath.Child("name"), "name is required"))
} else {
for _, msg := range pathvalidation.IsValidPathSegmentName(params.Name) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), params.Name, msg))
}
}
return allErrs
}
// allPathsPassRegexValidation returns true if the Ingress has paths that all
// match the Ingress path validation with requireRegexPath enabled.
func allPathsPassRegexValidation(ingress *networking.Ingress) bool {
for _, rule := range ingress.Spec.Rules {
if rule.HTTP == nil {
continue
}
for _, path := range rule.HTTP.Paths {
if len(path.Path) == 0 {
continue
}
if _, err := regexp.CompilePOSIX(path.Path); err != nil {
return false
}
}
}
return true
}
func resourceBackendPresent(ingress *networking.Ingress) bool {
if ingress.Spec.Backend != nil && ingress.Spec.Backend.Resource != nil {
return true
}
for _, rule := range ingress.Spec.Rules {
if rule.HTTP == nil {
continue
}
for _, path := range rule.HTTP.Paths {
if path.Backend.Resource != nil {
return true
}
}
}
return false
}