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 }