istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/config/analysis/analyzers/webhook/webhook.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package webhook 16 17 import ( 18 "fmt" 19 "strings" 20 21 v1 "k8s.io/api/admissionregistration/v1" 22 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 klabels "k8s.io/apimachinery/pkg/labels" 24 25 "istio.io/api/label" 26 "istio.io/istio/pkg/config" 27 "istio.io/istio/pkg/config/analysis" 28 "istio.io/istio/pkg/config/analysis/msg" 29 "istio.io/istio/pkg/config/resource" 30 "istio.io/istio/pkg/config/schema/gvk" 31 "istio.io/istio/pkg/util/sets" 32 ) 33 34 type Analyzer struct { 35 SkipServiceCheck bool 36 SkipDefaultRevisionedWebhook bool 37 } 38 39 var _ analysis.Analyzer = &Analyzer{} 40 41 func (a *Analyzer) Metadata() analysis.Metadata { 42 meta := analysis.Metadata{ 43 Name: "webhook.Analyzer", 44 Description: "Checks the validity of Istio webhooks", 45 Inputs: []config.GroupVersionKind{ 46 gvk.MutatingWebhookConfiguration, 47 }, 48 } 49 if !a.SkipServiceCheck { 50 meta.Inputs = append(meta.Inputs, gvk.Service) 51 } 52 return meta 53 } 54 55 func getNamespaceLabels() []klabels.Set { 56 return []klabels.Set{ 57 {}, 58 {"istio-injection": "enabled"}, 59 {"istio-injection": "disabled"}, 60 } 61 } 62 63 func getObjectLabels() []klabels.Set { 64 return []klabels.Set{ 65 {}, 66 {"sidecar.istio.io/inject": "true"}, 67 {"sidecar.istio.io/inject": "false"}, 68 } 69 } 70 71 func (a *Analyzer) Analyze(context analysis.Context) { 72 // First, extract and index all webhooks we found 73 webhooks := map[string][]v1.MutatingWebhook{} 74 resources := map[string]*resource.Instance{} 75 revisions := sets.New[string]() 76 context.ForEach(gvk.MutatingWebhookConfiguration, func(resource *resource.Instance) bool { 77 if a.SkipDefaultRevisionedWebhook && isDefaultRevisionedWebhook(resource.Message.(*v1.MutatingWebhookConfiguration)) { 78 return true 79 } 80 wh := resource.Message.(*v1.MutatingWebhookConfiguration) 81 revs := extractRevisions(wh) 82 if len(revs) == 0 && !isIstioWebhook(wh) { 83 return true 84 } 85 webhooks[resource.Metadata.FullName.String()] = wh.Webhooks 86 for _, h := range wh.Webhooks { 87 resources[fmt.Sprintf("%v/%v", resource.Metadata.FullName.String(), h.Name)] = resource 88 } 89 revisions.InsertAll(revs...) 90 return true 91 }) 92 93 // Set up all relevant namespace and object selector permutations 94 namespaceLabels := getNamespaceLabels() 95 for rev := range revisions { 96 for _, base := range getNamespaceLabels() { 97 base[label.IoIstioRev.Name] = rev 98 namespaceLabels = append(namespaceLabels, base) 99 } 100 } 101 objectLabels := getObjectLabels() 102 for rev := range revisions { 103 for _, base := range getObjectLabels() { 104 base[label.IoIstioRev.Name] = rev 105 objectLabels = append(objectLabels, base) 106 } 107 } 108 109 // For each permutation, we check which webhooks it matches. It must match exactly 0 or 1! 110 for _, nl := range namespaceLabels { 111 for _, ol := range objectLabels { 112 matches := sets.New[string]() 113 for name, whs := range webhooks { 114 for _, wh := range whs { 115 if selectorMatches(wh.NamespaceSelector, nl) && selectorMatches(wh.ObjectSelector, ol) { 116 matches.Insert(fmt.Sprintf("%v/%v", name, wh.Name)) 117 } 118 } 119 } 120 if len(matches) > 1 { 121 for match := range matches { 122 others := matches.Difference(sets.New(match)) 123 context.Report(gvk.MutatingWebhookConfiguration, msg.NewInvalidWebhook(resources[match], 124 fmt.Sprintf("Webhook overlaps with others: %v. This may cause injection to occur twice.", sets.SortedList(others)))) 125 } 126 } 127 } 128 } 129 130 // Next, check service references 131 if a.SkipServiceCheck { 132 return 133 } 134 for name, whs := range webhooks { 135 for _, wh := range whs { 136 if wh.ClientConfig.Service == nil { 137 // it is an url, skip it 138 continue 139 } 140 fname := resource.NewFullName( 141 resource.Namespace(wh.ClientConfig.Service.Namespace), 142 resource.LocalName(wh.ClientConfig.Service.Name)) 143 if !context.Exists(gvk.Service, fname) { 144 context.Report(gvk.MutatingWebhookConfiguration, msg.NewInvalidWebhook(resources[fmt.Sprintf("%v/%v", name, wh.Name)], 145 fmt.Sprintf("Injector refers to a control plane service that does not exist: %v.", fname))) 146 } 147 } 148 } 149 } 150 151 func isIstioWebhook(wh *v1.MutatingWebhookConfiguration) bool { 152 for _, w := range wh.Webhooks { 153 if strings.HasSuffix(w.Name, "istio.io") { 154 return true 155 } 156 } 157 return false 158 } 159 160 func extractRevisions(wh *v1.MutatingWebhookConfiguration) []string { 161 revs := sets.New[string]() 162 if r, f := wh.Labels[label.IoIstioRev.Name]; f { 163 revs.Insert(r) 164 } 165 for _, webhook := range wh.Webhooks { 166 if webhook.NamespaceSelector != nil { 167 if r, f := webhook.NamespaceSelector.MatchLabels[label.IoIstioRev.Name]; f { 168 revs.Insert(r) 169 } 170 171 for _, ls := range webhook.NamespaceSelector.MatchExpressions { 172 if ls.Key == label.IoIstioRev.Name { 173 revs.InsertAll(ls.Values...) 174 } 175 } 176 } 177 if webhook.ObjectSelector != nil { 178 if r, f := webhook.ObjectSelector.MatchLabels[label.IoIstioRev.Name]; f { 179 revs.Insert(r) 180 } 181 182 for _, ls := range webhook.ObjectSelector.MatchExpressions { 183 if ls.Key == label.IoIstioRev.Name { 184 revs.InsertAll(ls.Values...) 185 } 186 } 187 } 188 } 189 return revs.UnsortedList() 190 } 191 192 func isDefaultRevisionedWebhook(wh *v1.MutatingWebhookConfiguration) bool { 193 _, ok := wh.GetLabels()["istio.io/tag"] 194 if !ok && wh.GetLabels()[label.IoIstioRev.Name] == "default" { 195 return true 196 } 197 return false 198 } 199 200 func selectorMatches(selector *metav1.LabelSelector, labels klabels.Set) bool { 201 // From webhook spec: "Default to the empty LabelSelector, which matches everything." 202 if selector == nil { 203 return true 204 } 205 s, err := metav1.LabelSelectorAsSelector(selector) 206 if err != nil { 207 return false 208 } 209 return s.Matches(labels) 210 }