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  }