github.com/verrazzano/verrazzano@v1.7.1/application-operator/apis/oam/v1alpha1/ingresstrait_webhook.go (about)

     1  // Copyright (c) 2020, 2022, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package v1alpha1
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	s "strings"
    10  
    11  	vzlog "github.com/verrazzano/verrazzano/pkg/log"
    12  	"go.uber.org/zap"
    13  	"k8s.io/apimachinery/pkg/runtime"
    14  	k8sValidations "k8s.io/apimachinery/pkg/util/validation"
    15  	ctrl "sigs.k8s.io/controller-runtime"
    16  	c "sigs.k8s.io/controller-runtime/pkg/client"
    17  	"sigs.k8s.io/controller-runtime/pkg/webhook"
    18  )
    19  
    20  var getAllIngressTraits = listIngressTraits
    21  var client c.Client
    22  
    23  // log is for logging in this package.
    24  var log = zap.S().With(vzlog.FieldResourceName, "ingresstrait-resource")
    25  
    26  // SetupWebhookWithManager saves client from manager and sets up webhook
    27  func (r *IngressTrait) SetupWebhookWithManager(mgr ctrl.Manager) error {
    28  	client = mgr.GetClient()
    29  	return ctrl.NewWebhookManagedBy(mgr).
    30  		For(r).
    31  		Complete()
    32  }
    33  
    34  // +kubebuilder:webhook:verbs=create;update,path=/validate-oam-verrazzano-io-v1alpha1-ingresstrait,mutating=false,failurePolicy=fail,groups=oam.verrazzano.io,resources=ingresstraits,versions=v1alpha1,name=vingresstrait.kb.io
    35  
    36  var _ webhook.Validator = &IngressTrait{}
    37  
    38  // ValidateCreate implements webhook.Validator so a webhook will be registered for ingress trait type creation.
    39  func (r *IngressTrait) ValidateCreate() error {
    40  	log.Debugw("Validate create", "name", r.Name)
    41  	allIngressTraits, err := getAllIngressTraits(r.Namespace)
    42  	if err != nil {
    43  		return fmt.Errorf("unable to obtain list of existing IngressTrait's during create validation: %v", err)
    44  	}
    45  	return r.validateIngressTrait(allIngressTraits.Items)
    46  }
    47  
    48  // ValidateUpdate implements webhook.Validator so a webhook will be registered for ingress trait type update.
    49  func (r *IngressTrait) ValidateUpdate(old runtime.Object) error {
    50  	log.Debugw("Validate update", "name", r.Name)
    51  
    52  	existingIngressList, err := getAllIngressTraits(r.Namespace)
    53  	if err != nil {
    54  		return fmt.Errorf("unable to obtain list of existing IngressTrait's during update validation: %v", err)
    55  	}
    56  	// Remove the trait that is being updated from the list
    57  	updatedTrait := old.(*IngressTrait)
    58  	updatedTraitUID := updatedTrait.UID
    59  	allIngressTraits := existingIngressList.Items
    60  	for i, existingTrait := range allIngressTraits {
    61  		if existingTrait.UID == updatedTraitUID {
    62  			allIngressTraits = append(allIngressTraits[:i], allIngressTraits[i+1:]...)
    63  			break
    64  		}
    65  	}
    66  	return r.validateIngressTrait(allIngressTraits)
    67  }
    68  
    69  // ValidateDelete implements webhook.Validator so a webhook will be registered for ingress trait type deletion.
    70  func (r *IngressTrait) ValidateDelete() error {
    71  	log.Debugw("Validate delete", "name", r.Name)
    72  
    73  	// no validation on delete
    74  	return nil
    75  }
    76  
    77  // validateIngressTrait validates a new or updated ingress trait.
    78  func (r *IngressTrait) validateIngressTrait(existingTraits []IngressTrait) error {
    79  	// validation rules
    80  	// For "exact" hosts such as "foo.example.com"
    81  	// - ensure that no other ingressTrait exists with the same host and path
    82  	// - For "prefix" hosts such as "*.example.com"
    83  	// - These don't conflict with ingressTrait's with "exact" hosts as the exact host takes precedence because it is more specific
    84  	// - Only conflict with other "prefix" ingressTraits with matching host string and path
    85  	// For empty or * host
    86  	// - * or empty host means to match all
    87  	// - IngressTrait's with "all" hosts only conflict with other ingressTrait's with "all" hosts and same path
    88  	// - "All" ingressTrait's don't conflict with "prefix" ingressTraits which take precedence because they are more specific
    89  	// - "All" ingressTrait's don't conflict with "exact" ingressTraits which take precedence because they are more specific
    90  
    91  	hostPathMap, e := r.createIngressTraitMap()
    92  	if e != nil {
    93  		return e
    94  	}
    95  
    96  	for _, ingressTrait := range existingTraits {
    97  		for _, rule := range ingressTrait.Spec.Rules {
    98  			hosts := getNormalizedHosts(rule)
    99  
   100  			for _, host := range hosts {
   101  				ingressPaths, exists := hostPathMap[host]
   102  				if exists {
   103  					for _, path := range rule.Paths {
   104  						_, exists := ingressPaths[path.Path]
   105  						if exists {
   106  							return fmt.Errorf(
   107  								"IngressTrait collision. An existing IngressTrait with the name: '%v' exists with host: '%v' and path: '%v'",
   108  								ingressTrait.Name, host, path)
   109  						}
   110  					}
   111  					// This is to support empty paths. We are considering defaulting to '/'.
   112  					// With the '/' default, we can remove this block
   113  					if len(rule.Paths) == 0 && len(ingressPaths) == 0 {
   114  						return fmt.Errorf(
   115  							"IngressTrait collision. An existing IngressTrait with the name: '%v' exists with host: '%v' and no paths",
   116  							ingressTrait.Name, host)
   117  					}
   118  				}
   119  			}
   120  		}
   121  	}
   122  	return nil
   123  }
   124  
   125  // createIngressTraitMap creates a map of ingress traits with hosts mapped to associated paths.
   126  func (r *IngressTrait) createIngressTraitMap() (map[string]map[string]struct{}, error) {
   127  	hostPathMap := make(map[string]map[string]struct{})
   128  	for _, rule := range r.Spec.Rules {
   129  		hosts := getNormalizedHosts(rule)
   130  		for _, host := range hosts {
   131  			err := r.validateHost(host)
   132  			if err != nil {
   133  				return nil, err
   134  			}
   135  			paths, exists := hostPathMap[host]
   136  			if !exists {
   137  				paths = make(map[string]struct{})
   138  				hostPathMap[host] = paths
   139  			}
   140  			for _, path := range rule.Paths {
   141  				paths[path.Path] = struct{}{}
   142  			}
   143  		}
   144  	}
   145  	return hostPathMap, nil
   146  }
   147  
   148  // validateHost does syntactic validation of a host string
   149  func (r *IngressTrait) validateHost(host string) error {
   150  	if len(host) == 0 {
   151  		return nil
   152  	}
   153  
   154  	if host == "*" {
   155  		return nil
   156  	}
   157  
   158  	var errMessages []string
   159  	var errFound bool
   160  
   161  	if s.HasPrefix(host, "*.") {
   162  		for _, msg := range k8sValidations.IsWildcardDNS1123Subdomain(host) {
   163  			errMessages = append(errMessages, msg)
   164  			errFound = true
   165  		}
   166  	} else {
   167  		for _, msg := range k8sValidations.IsDNS1123Subdomain(host) {
   168  			errMessages = append(errMessages, msg)
   169  			errFound = true
   170  		}
   171  	}
   172  
   173  	if !errFound {
   174  		labels := s.Split(host, ".")
   175  		for i := range labels {
   176  			label := labels[i]
   177  			// '*' isn't a valid label but is valid as the prefix in a wildcard host
   178  			if !(i == 0 && label == "*") {
   179  				for _, msg := range k8sValidations.IsDNS1123Label(label) {
   180  					errMessages = append(errMessages, msg)
   181  					errFound = true
   182  				}
   183  			}
   184  		}
   185  	}
   186  
   187  	if errFound {
   188  		return fmt.Errorf("invalid host specified for IngressTrait with name '%v': %v",
   189  			r.Name, s.Join(errMessages, ", "))
   190  	}
   191  	return nil
   192  }
   193  
   194  // getNormalizedHosts gets a normalized host string from a rule
   195  func getNormalizedHosts(rule IngressRule) []string {
   196  	hosts := make([]string, len(rule.Hosts))
   197  	for i, host := range rule.Hosts {
   198  		host := s.TrimSpace(host)
   199  		if host == "*" {
   200  			host = ""
   201  		}
   202  		hosts[i] = host
   203  	}
   204  	return hosts
   205  }
   206  
   207  // listIngressTraits obtains all existing ingress traits in the specified namespace
   208  func listIngressTraits(namespace string) (*IngressTraitList, error) {
   209  	allIngressTraits := &IngressTraitList{}
   210  	//todo: context.TODO or context.Background?
   211  	//todo: currently ignoring namespace
   212  	err := client.List(context.TODO(), allIngressTraits)
   213  	return allIngressTraits, err
   214  }