istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/injector/injector-list.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 injector
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"io"
    21  	"sort"
    22  	"strconv"
    23  	"strings"
    24  	"text/tabwriter"
    25  
    26  	"github.com/spf13/cobra"
    27  	admitv1 "k8s.io/api/admissionregistration/v1"
    28  	corev1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	api_pkg_labels "k8s.io/apimachinery/pkg/labels"
    31  
    32  	"istio.io/api/annotation"
    33  	"istio.io/api/label"
    34  	"istio.io/istio/istioctl/pkg/cli"
    35  	"istio.io/istio/istioctl/pkg/clioptions"
    36  	"istio.io/istio/istioctl/pkg/describe"
    37  	"istio.io/istio/istioctl/pkg/util/ambient"
    38  	"istio.io/istio/pkg/config/analysis/analyzers/injection"
    39  	analyzer_util "istio.io/istio/pkg/config/analysis/analyzers/util"
    40  	"istio.io/istio/pkg/config/resource"
    41  	"istio.io/istio/pkg/kube"
    42  	"istio.io/istio/pkg/kube/inject"
    43  	"istio.io/istio/pkg/slices"
    44  	"istio.io/istio/pkg/util/sets"
    45  )
    46  
    47  type revisionCount struct {
    48  	// pods in a revision
    49  	pods int
    50  	// pods that are disabled from injection
    51  	disabled int
    52  	// pods that are enabled for injection, but whose revision doesn't match their namespace's revision
    53  	needsRestart int
    54  }
    55  
    56  func Cmd(cliContext cli.Context) *cobra.Command {
    57  	cmd := &cobra.Command{
    58  		Use:     "injector",
    59  		Short:   "List sidecar injector and sidecar versions",
    60  		Long:    `List sidecar injector and sidecar versions`,
    61  		Example: `  istioctl experimental injector list`,
    62  		Args: func(cmd *cobra.Command, args []string) error {
    63  			if len(args) != 0 {
    64  				return fmt.Errorf("unknown subcommand %q", args[0])
    65  			}
    66  			return nil
    67  		},
    68  		RunE: func(cmd *cobra.Command, args []string) error {
    69  			cmd.HelpFunc()(cmd, args)
    70  			return nil
    71  		},
    72  	}
    73  
    74  	cmd.AddCommand(injectorListCommand(cliContext))
    75  	return cmd
    76  }
    77  
    78  func injectorListCommand(ctx cli.Context) *cobra.Command {
    79  	var opts clioptions.ControlPlaneOptions
    80  	cmd := &cobra.Command{
    81  		Use:     "list",
    82  		Short:   "List sidecar injector and sidecar versions",
    83  		Long:    `List sidecar injector and sidecar versions`,
    84  		Example: `  istioctl experimental injector list`,
    85  		RunE: func(cmd *cobra.Command, args []string) error {
    86  			client, err := ctx.CLIClientWithRevision(opts.Revision)
    87  			if err != nil {
    88  				return fmt.Errorf("failed to create k8s client: %v", err)
    89  			}
    90  
    91  			nslist, err := getNamespaces(context.Background(), client, ctx.IstioNamespace())
    92  			if err != nil {
    93  				return err
    94  			}
    95  
    96  			hooksList, err := client.Kube().AdmissionregistrationV1().MutatingWebhookConfigurations().List(context.Background(), metav1.ListOptions{})
    97  			if err != nil {
    98  				return err
    99  			}
   100  			hooks := hooksList.Items
   101  			pods, err := getPods(context.Background(), client)
   102  			if err != nil {
   103  				return err
   104  			}
   105  			err = printNS(cmd.OutOrStdout(), nslist, hooks, pods)
   106  			if err != nil {
   107  				return err
   108  			}
   109  			fmt.Fprintln(cmd.OutOrStdout())
   110  			injectedImages, err := getInjectedImages(context.Background(), client)
   111  			if err != nil {
   112  				return err
   113  			}
   114  
   115  			sort.Slice(hooks, func(i, j int) bool {
   116  				return hooks[i].Name < hooks[j].Name
   117  			})
   118  			return printHooks(cmd.OutOrStdout(), nslist, hooks, injectedImages)
   119  		},
   120  	}
   121  
   122  	return cmd
   123  }
   124  
   125  func filterSystemNamespaces(nss []corev1.Namespace, istioNamespace string) []corev1.Namespace {
   126  	filtered := make([]corev1.Namespace, 0)
   127  	for _, ns := range nss {
   128  		if inject.IgnoredNamespaces.Contains(ns.Name) || ns.Name == istioNamespace {
   129  			continue
   130  		}
   131  		filtered = append(filtered, ns)
   132  	}
   133  	return filtered
   134  }
   135  
   136  func getNamespaces(ctx context.Context, client kube.CLIClient, istioNamespace string) ([]corev1.Namespace, error) {
   137  	nslist, err := client.Kube().CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
   138  	if err != nil {
   139  		return []corev1.Namespace{}, err
   140  	}
   141  	filtered := filterSystemNamespaces(nslist.Items, istioNamespace)
   142  	filtered = slices.Filter(filtered, func(namespace corev1.Namespace) bool {
   143  		return !ambient.InAmbient(&namespace)
   144  	})
   145  	sort.Slice(filtered, func(i, j int) bool {
   146  		return filtered[i].Name < filtered[j].Name
   147  	})
   148  	return filtered, nil
   149  }
   150  
   151  func printNS(writer io.Writer, namespaces []corev1.Namespace, hooks []admitv1.MutatingWebhookConfiguration,
   152  	allPods map[resource.Namespace][]corev1.Pod,
   153  ) error {
   154  	outputCount := 0
   155  
   156  	w := new(tabwriter.Writer).Init(writer, 0, 8, 1, ' ', 0)
   157  	for _, namespace := range namespaces {
   158  		revision := getInjectedRevision(&namespace, hooks)
   159  		podCount := podCountByRevision(allPods[resource.Namespace(namespace.Name)], revision)
   160  		if len(podCount) == 0 {
   161  			// This namespace has no pods, but we wish to display it if new pods will be auto-injected
   162  			if revision != "" {
   163  				podCount[revision] = revisionCount{}
   164  			}
   165  		}
   166  		for injectedRevision, count := range podCount {
   167  			if outputCount == 0 {
   168  				fmt.Fprintln(w, "NAMESPACE\tISTIO-REVISION\tPOD-REVISIONS")
   169  			}
   170  			outputCount++
   171  
   172  			fmt.Fprintf(w, "%s\t%s\t%s\n", namespace.Name, revision, renderCounts(injectedRevision, count))
   173  		}
   174  	}
   175  	if outputCount == 0 {
   176  		fmt.Fprintf(writer, "No Istio injected namespaces present.\n")
   177  	}
   178  
   179  	return w.Flush()
   180  }
   181  
   182  func printHooks(writer io.Writer, namespaces []corev1.Namespace, hooks []admitv1.MutatingWebhookConfiguration, injectedImages map[string]string) error {
   183  	if len(hooks) == 0 {
   184  		fmt.Fprintf(writer, "No Istio injection hooks present.\n")
   185  		return nil
   186  	}
   187  
   188  	w := new(tabwriter.Writer).Init(writer, 0, 8, 1, ' ', 0)
   189  	fmt.Fprintln(w, "NAMESPACES\tINJECTOR-HOOK\tISTIO-REVISION\tSIDECAR-IMAGE")
   190  	for _, hook := range hooks {
   191  		revision := hook.ObjectMeta.GetLabels()[label.IoIstioRev.Name]
   192  		namespaces := getMatchingNamespaces(&hook, namespaces)
   193  		if len(namespaces) == 0 {
   194  			fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", "DOES NOT AUTOINJECT", hook.Name, revision, injectedImages[revision])
   195  			continue
   196  		}
   197  		for _, namespace := range namespaces {
   198  			fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", namespace.Name, hook.Name, revision, injectedImages[revision])
   199  		}
   200  	}
   201  	return w.Flush()
   202  }
   203  
   204  func getInjector(namespace *corev1.Namespace, hooks []admitv1.MutatingWebhookConfiguration) *admitv1.MutatingWebhookConfiguration {
   205  	// find matching hook
   206  	for _, hook := range hooks {
   207  		for _, webhook := range hook.Webhooks {
   208  			nsSelector, err := metav1.LabelSelectorAsSelector(webhook.NamespaceSelector)
   209  			if err != nil {
   210  				continue
   211  			}
   212  			if nsSelector.Matches(api_pkg_labels.Set(namespace.ObjectMeta.Labels)) {
   213  				return &hook
   214  			}
   215  		}
   216  	}
   217  	return nil
   218  }
   219  
   220  func getInjectedRevision(namespace *corev1.Namespace, hooks []admitv1.MutatingWebhookConfiguration) string {
   221  	injector := getInjector(namespace, hooks)
   222  	if injector != nil {
   223  		return injector.ObjectMeta.GetLabels()[label.IoIstioRev.Name]
   224  	}
   225  	newRev := namespace.ObjectMeta.GetLabels()[label.IoIstioRev.Name]
   226  	oldLabel, ok := namespace.ObjectMeta.GetLabels()[analyzer_util.InjectionLabelName]
   227  	// If there is no istio-injection=disabled and no istio.io/rev, the namespace isn't injected
   228  	if newRev == "" && (ok && oldLabel == "disabled" || !ok) {
   229  		return ""
   230  	}
   231  	if newRev != "" {
   232  		return fmt.Sprintf("MISSING/%s", newRev)
   233  	}
   234  	return fmt.Sprintf("MISSING/%s", analyzer_util.InjectionLabelName)
   235  }
   236  
   237  func getMatchingNamespaces(hook *admitv1.MutatingWebhookConfiguration, namespaces []corev1.Namespace) []corev1.Namespace {
   238  	retval := make([]corev1.Namespace, 0, len(namespaces))
   239  	seen := sets.String{}
   240  	for _, webhook := range hook.Webhooks {
   241  		nsSelector, err := metav1.LabelSelectorAsSelector(webhook.NamespaceSelector)
   242  		if err != nil {
   243  			return retval
   244  		}
   245  
   246  		for _, namespace := range namespaces {
   247  			if !seen.Contains(namespace.Name) && nsSelector.Matches(api_pkg_labels.Set(namespace.Labels)) {
   248  				retval = append(retval, namespace)
   249  				seen.Insert(namespace.Name)
   250  			}
   251  		}
   252  	}
   253  	return retval
   254  }
   255  
   256  func getPods(ctx context.Context, client kube.CLIClient) (map[resource.Namespace][]corev1.Pod, error) {
   257  	retval := map[resource.Namespace][]corev1.Pod{}
   258  	// All pods in all namespaces
   259  	pods, err := client.Kube().CoreV1().Pods("").List(ctx, metav1.ListOptions{})
   260  	if err != nil {
   261  		return retval, err
   262  	}
   263  	for _, pod := range pods.Items {
   264  		retval[resource.Namespace(pod.GetNamespace())] = append(retval[resource.Namespace(pod.GetNamespace())], pod)
   265  	}
   266  	return retval, nil
   267  }
   268  
   269  // getInjectedImages() returns a map of revision->dockerimage
   270  func getInjectedImages(ctx context.Context, client kube.CLIClient) (map[string]string, error) {
   271  	retval := map[string]string{}
   272  
   273  	// All configs in all namespaces that are Istio revisioned
   274  	configMaps, err := client.Kube().CoreV1().ConfigMaps("").List(ctx, metav1.ListOptions{LabelSelector: label.IoIstioRev.Name})
   275  	if err != nil {
   276  		return retval, err
   277  	}
   278  
   279  	for _, configMap := range configMaps.Items {
   280  		image := injection.GetIstioProxyImage(&configMap)
   281  		if image != "" {
   282  			retval[configMap.ObjectMeta.GetLabels()[label.IoIstioRev.Name]] = image
   283  		}
   284  	}
   285  
   286  	return retval, nil
   287  }
   288  
   289  // podCountByRevision() returns a map of revision->pods, with "<non-Istio>" as the dummy "revision" for uninjected pods
   290  func podCountByRevision(pods []corev1.Pod, expectedRevision string) map[string]revisionCount {
   291  	retval := map[string]revisionCount{}
   292  	for _, pod := range pods {
   293  		revision := extractRevisionFromPod(&pod)
   294  		revisionLabel := revision
   295  		if revision == "" {
   296  			revisionLabel = "<non-Istio>"
   297  		}
   298  		counts := retval[revisionLabel]
   299  		counts.pods++
   300  		if injectionDisabled(&pod) {
   301  			counts.disabled++
   302  		} else if revision != expectedRevision {
   303  			counts.needsRestart++
   304  		}
   305  		retval[revisionLabel] = counts
   306  	}
   307  	return retval
   308  }
   309  
   310  func extractRevisionFromPod(pod *corev1.Pod) string {
   311  	return describe.GetRevisionFromPodAnnotation(pod.GetAnnotations())
   312  }
   313  
   314  func injectionDisabled(pod *corev1.Pod) bool {
   315  	inject := pod.ObjectMeta.GetAnnotations()[annotation.SidecarInject.Name]
   316  	if lbl, labelPresent := pod.ObjectMeta.GetLabels()[label.SidecarInject.Name]; labelPresent {
   317  		inject = lbl
   318  	}
   319  	return strings.EqualFold(inject, "false")
   320  }
   321  
   322  func renderCounts(injectedRevision string, counts revisionCount) string {
   323  	if counts.pods == 0 {
   324  		return "<no pods>"
   325  	}
   326  
   327  	podText := strconv.Itoa(counts.pods)
   328  	if counts.disabled > 0 {
   329  		podText += fmt.Sprintf(" (injection disabled: %d)", counts.disabled)
   330  	}
   331  	if counts.needsRestart > 0 {
   332  		podText += fmt.Sprintf(" NEEDS RESTART: %d", counts.needsRestart)
   333  	}
   334  	return fmt.Sprintf("%s: %s", injectedRevision, podText)
   335  }