istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/checkinject/checkinject.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 checkinject
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"io"
    21  	"reflect"
    22  	"sort"
    23  	"strings"
    24  
    25  	"github.com/fatih/color"
    26  	"github.com/spf13/cobra"
    27  	admitv1 "k8s.io/api/admissionregistration/v1"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/labels"
    30  
    31  	"istio.io/api/label"
    32  	"istio.io/istio/istioctl/pkg/cli"
    33  	"istio.io/istio/istioctl/pkg/completion"
    34  	"istio.io/istio/istioctl/pkg/util"
    35  	"istio.io/istio/istioctl/pkg/writer/table"
    36  	analyzer_util "istio.io/istio/pkg/config/analysis/analyzers/util"
    37  )
    38  
    39  var labelPairs string
    40  
    41  func Cmd(ctx cli.Context) *cobra.Command {
    42  	cmd := &cobra.Command{
    43  		Use:   "check-inject [<type>/]<name>[.<namespace>]",
    44  		Short: "Check the injection status or inject-ability of a given resource, explains why it is (or will be) injected or not",
    45  		Long: `
    46  Checks associated resources of the given resource, and running webhooks to examine whether the pod can be or will be injected or not.`,
    47  		Example: `  # Check the injection status of a pod
    48    istioctl experimental check-inject details-v1-fcff6c49c-kqnfk.test
    49  	
    50    # Check the injection status of a pod under a deployment
    51    istioctl x check-inject deployment/details-v1
    52  
    53    # Check the injection status of a pod under a deployment in namespace test
    54    istioctl x check-inject deployment/details-v1 -n test
    55  
    56    # Check the injection status of label pairs in a specific namespace before actual injection 
    57    istioctl x check-inject -n test -l app=helloworld,version=v1
    58  `,
    59  		Args: func(cmd *cobra.Command, args []string) error {
    60  			if len(args) == 0 && labelPairs == "" || len(args) > 1 {
    61  				cmd.Println(cmd.UsageString())
    62  				return fmt.Errorf("check-inject requires only [<resource-type>/]<resource-name>[.<namespace>], or specify labels flag")
    63  			}
    64  			return nil
    65  		},
    66  		RunE: func(cmd *cobra.Command, args []string) error {
    67  			kubeClient, err := ctx.CLIClient()
    68  			if err != nil {
    69  				return err
    70  			}
    71  			var podName, podNs string
    72  			var podLabels, nsLabels map[string]string
    73  			if len(args) == 1 {
    74  				podName, podNs, err = ctx.InferPodInfoFromTypedResource(args[0], ctx.Namespace())
    75  				if err != nil {
    76  					return err
    77  				}
    78  				pod, err := kubeClient.Kube().CoreV1().Pods(podNs).Get(context.TODO(), podName, metav1.GetOptions{})
    79  				if err != nil {
    80  					return err
    81  				}
    82  				ns, err := kubeClient.Kube().CoreV1().Namespaces().Get(context.TODO(), podNs, metav1.GetOptions{})
    83  				if err != nil {
    84  					return err
    85  				}
    86  				podLabels = pod.GetLabels()
    87  				nsLabels = ns.GetLabels()
    88  			} else {
    89  				namespace := ctx.NamespaceOrDefault(ctx.Namespace())
    90  				ns, err := kubeClient.Kube().CoreV1().Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{})
    91  				if err != nil {
    92  					return err
    93  				}
    94  				ls, err := metav1.ParseToLabelSelector(labelPairs)
    95  				if err != nil {
    96  					return err
    97  				}
    98  				podLabels = ls.MatchLabels
    99  				nsLabels = ns.GetLabels()
   100  			}
   101  			whs, err := kubeClient.Kube().AdmissionregistrationV1().MutatingWebhookConfigurations().List(context.TODO(), metav1.ListOptions{})
   102  			if err != nil {
   103  				return err
   104  			}
   105  			checkResults := analyzeRunningWebhooks(whs.Items, podLabels, nsLabels)
   106  			return printCheckInjectorResults(cmd.OutOrStdout(), checkResults)
   107  		},
   108  		ValidArgsFunction: completion.ValidPodsNameArgs(ctx),
   109  	}
   110  	cmd.PersistentFlags().StringVarP(&labelPairs, "labels", "l", "",
   111  		"Check namespace and label pairs injection status, split multiple labels by commas")
   112  	return cmd
   113  }
   114  
   115  func printCheckInjectorResults(writer io.Writer, was []webhookAnalysis) error {
   116  	if len(was) == 0 {
   117  		fmt.Fprintf(writer, "ERROR: no Istio injection hooks present.\n")
   118  		return nil
   119  	}
   120  	w := table.NewStyleWriter(writer)
   121  	w.SetAddRowFunc(func(obj interface{}) table.Row {
   122  		wa := obj.(webhookAnalysis)
   123  		row := table.Row{
   124  			Cells: make([]table.Cell, 0),
   125  		}
   126  		row.Cells = append(row.Cells, table.NewCell(wa.Name), table.NewCell(wa.Revision))
   127  		if wa.Injected {
   128  			row.Cells = append(row.Cells, table.NewCell("✔", color.FgGreen))
   129  		} else {
   130  			row.Cells = append(row.Cells, table.NewCell("✘", color.FgRed))
   131  		}
   132  		row.Cells = append(row.Cells, table.NewCell(wa.Reason))
   133  		return row
   134  	})
   135  	w.AddHeader("WEBHOOK", "REVISION", "INJECTED", "REASON")
   136  	injectedTotal := 0
   137  	for _, ws := range was {
   138  		if ws.Injected {
   139  			injectedTotal++
   140  		}
   141  		w.AddRow(ws)
   142  	}
   143  	w.Flush()
   144  	if injectedTotal > 1 {
   145  		fmt.Fprintf(writer, "ERROR: multiple webhooks will inject, which can lead to errors")
   146  	}
   147  	return nil
   148  }
   149  
   150  type webhookAnalysis struct {
   151  	Name     string
   152  	Revision string
   153  	Injected bool
   154  	Reason   string
   155  }
   156  
   157  func analyzeRunningWebhooks(whs []admitv1.MutatingWebhookConfiguration, podLabels, nsLabels map[string]string) []webhookAnalysis {
   158  	results := make([]webhookAnalysis, 0)
   159  	for _, mwc := range whs {
   160  		if !isIstioWebhook(&mwc) {
   161  			continue
   162  		}
   163  		rev := extractRevision(&mwc)
   164  		reason, injected := analyzeWebhooksMatchStatus(mwc.Webhooks, podLabels, nsLabels)
   165  		results = append(results, webhookAnalysis{
   166  			Name:     mwc.Name,
   167  			Revision: rev,
   168  			Injected: injected,
   169  			Reason:   reason,
   170  		})
   171  	}
   172  	sort.Slice(results, func(i, j int) bool {
   173  		return results[i].Name < results[j].Name
   174  	})
   175  	return results
   176  }
   177  
   178  func analyzeWebhooksMatchStatus(whs []admitv1.MutatingWebhook, podLabels, nsLabels map[string]string) (reason string, injected bool) {
   179  	for _, wh := range whs {
   180  		nsMatched, nsLabel := extractMatchedSelectorInfo(wh.NamespaceSelector, nsLabels)
   181  		podMatched, podLabel := extractMatchedSelectorInfo(wh.ObjectSelector, podLabels)
   182  		if nsMatched && podMatched {
   183  			if nsLabel != "" && podLabel != "" {
   184  				return fmt.Sprintf("Namespace label %s matches, and pod label %s matches", nsLabel, podLabel), true
   185  			} else if nsLabel != "" {
   186  				outMsg := fmt.Sprintf("Namespace label %s matches", nsLabel)
   187  				if strings.Contains(nsLabel, "kubernetes.io/metadata.name") {
   188  					outMsg += " (Automatic injection is enabled in all namespaces)."
   189  				}
   190  				return outMsg, true
   191  			} else if podLabel != "" {
   192  				return fmt.Sprintf("Pod label %s matches", podLabel), true
   193  			}
   194  		} else if nsMatched {
   195  			for _, me := range wh.ObjectSelector.MatchExpressions {
   196  				switch me.Operator {
   197  				case metav1.LabelSelectorOpDoesNotExist:
   198  					v, ok := podLabels[me.Key]
   199  					if ok {
   200  						return fmt.Sprintf("Pod has %s=%s label, preventing injection", me.Key, v), false
   201  					}
   202  				case metav1.LabelSelectorOpNotIn:
   203  					v, ok := podLabels[me.Key]
   204  					if !ok {
   205  						continue
   206  					}
   207  					for _, nv := range me.Values {
   208  						if nv == v {
   209  							return fmt.Sprintf("Pod has %s=%s label, preventing injection", me.Key, v), false
   210  						}
   211  					}
   212  				}
   213  			}
   214  		} else if podMatched {
   215  			if v, ok := nsLabels[analyzer_util.InjectionLabelName]; ok {
   216  				if v != "enabled" {
   217  					return fmt.Sprintf("Namespace has %s=%s label, preventing injection",
   218  						analyzer_util.InjectionLabelName, v), false
   219  				}
   220  			}
   221  		}
   222  	}
   223  	noMatchingReason := func(whs []admitv1.MutatingWebhook) string {
   224  		nsMatchedLabels := make([]string, 0)
   225  		podMatchedLabels := make([]string, 0)
   226  		extractMatchLabels := func(selector *metav1.LabelSelector) []string {
   227  			if selector == nil {
   228  				return nil
   229  			}
   230  			labels := make([]string, 0)
   231  			for _, me := range selector.MatchExpressions {
   232  				if me.Operator != metav1.LabelSelectorOpIn {
   233  					continue
   234  				}
   235  				for _, v := range me.Values {
   236  					labels = append(labels, fmt.Sprintf("%s=%s", me.Key, v))
   237  				}
   238  			}
   239  			return labels
   240  		}
   241  
   242  		var isDeactivated bool
   243  		for _, wh := range whs {
   244  			if reflect.DeepEqual(wh.NamespaceSelector, util.NeverMatch) && reflect.DeepEqual(wh.ObjectSelector, util.NeverMatch) {
   245  				isDeactivated = true
   246  			}
   247  			nsMatchedLabels = append(nsMatchedLabels, extractMatchLabels(wh.NamespaceSelector)...)
   248  			podMatchedLabels = append(podMatchedLabels, extractMatchLabels(wh.ObjectSelector)...)
   249  		}
   250  		if isDeactivated {
   251  			return "The injection webhook is deactivated, and will never match labels."
   252  		}
   253  		return fmt.Sprintf("No matching namespace labels (%s) "+
   254  			"or pod labels (%s)", strings.Join(nsMatchedLabels, ", "), strings.Join(podMatchedLabels, ", "))
   255  	}
   256  	return noMatchingReason(whs), false
   257  }
   258  
   259  func extractMatchedSelectorInfo(ls *metav1.LabelSelector, objLabels map[string]string) (matched bool, injLabel string) {
   260  	if ls == nil {
   261  		return true, ""
   262  	}
   263  	selector, err := metav1.LabelSelectorAsSelector(ls)
   264  	if err != nil {
   265  		return false, ""
   266  	}
   267  	matched = selector.Matches(labels.Set(objLabels))
   268  	if !matched {
   269  		return matched, ""
   270  	}
   271  	for _, me := range ls.MatchExpressions {
   272  		switch me.Operator {
   273  		case metav1.LabelSelectorOpIn, metav1.LabelSelectorOpNotIn:
   274  			if v, exist := objLabels[me.Key]; exist {
   275  				return matched, fmt.Sprintf("%s=%s", me.Key, v)
   276  			}
   277  		}
   278  	}
   279  	return matched, ""
   280  }
   281  
   282  func extractRevision(wh *admitv1.MutatingWebhookConfiguration) string {
   283  	return wh.GetLabels()[label.IoIstioRev.Name]
   284  }
   285  
   286  func isIstioWebhook(wh *admitv1.MutatingWebhookConfiguration) bool {
   287  	for _, w := range wh.Webhooks {
   288  		if strings.HasSuffix(w.Name, "istio.io") {
   289  			return true
   290  		}
   291  	}
   292  	return false
   293  }