istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/describe/describe.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 describe
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"io"
    22  	"regexp"
    23  	"sort"
    24  	"strconv"
    25  	"strings"
    26  
    27  	cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
    28  	core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    29  	listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    30  	route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
    31  	rbachttp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3"
    32  	hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
    33  	"github.com/hashicorp/go-multierror"
    34  	"github.com/spf13/cobra"
    35  	"google.golang.org/protobuf/types/known/structpb"
    36  	corev1 "k8s.io/api/core/v1"
    37  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    38  	klabels "k8s.io/apimachinery/pkg/labels"
    39  	"k8s.io/client-go/kubernetes"
    40  
    41  	apiannotation "istio.io/api/annotation"
    42  	"istio.io/api/label"
    43  	meshconfig "istio.io/api/mesh/v1alpha1"
    44  	"istio.io/api/networking/v1alpha3"
    45  	typev1beta1 "istio.io/api/type/v1beta1"
    46  	clientnetworking "istio.io/client-go/pkg/apis/networking/v1alpha3"
    47  	istioclient "istio.io/client-go/pkg/clientset/versioned"
    48  	"istio.io/istio/istioctl/pkg/cli"
    49  	"istio.io/istio/istioctl/pkg/clioptions"
    50  	"istio.io/istio/istioctl/pkg/completion"
    51  	istioctlutil "istio.io/istio/istioctl/pkg/util"
    52  	"istio.io/istio/istioctl/pkg/util/configdump"
    53  	"istio.io/istio/istioctl/pkg/util/handlers"
    54  	istio_envoy_configdump "istio.io/istio/istioctl/pkg/writer/envoy/configdump"
    55  	"istio.io/istio/pilot/pkg/config/kube/crdclient"
    56  	"istio.io/istio/pilot/pkg/model"
    57  	"istio.io/istio/pilot/pkg/networking/util"
    58  	"istio.io/istio/pilot/pkg/security/authn"
    59  	pilotcontroller "istio.io/istio/pilot/pkg/serviceregistry/kube/controller"
    60  	v3 "istio.io/istio/pilot/pkg/xds/v3"
    61  	"istio.io/istio/pkg/config"
    62  	analyzerutil "istio.io/istio/pkg/config/analysis/analyzers/util"
    63  	"istio.io/istio/pkg/config/constants"
    64  	"istio.io/istio/pkg/config/host"
    65  	configKube "istio.io/istio/pkg/config/kube"
    66  	"istio.io/istio/pkg/config/mesh"
    67  	"istio.io/istio/pkg/kube"
    68  	"istio.io/istio/pkg/kube/inject"
    69  	"istio.io/istio/pkg/kube/labels"
    70  	"istio.io/istio/pkg/log"
    71  	"istio.io/istio/pkg/maps"
    72  	"istio.io/istio/pkg/slices"
    73  	"istio.io/istio/pkg/url"
    74  	"istio.io/istio/pkg/util/sets"
    75  	"istio.io/istio/pkg/wellknown"
    76  )
    77  
    78  type myProtoValue struct {
    79  	*structpb.Value
    80  }
    81  
    82  const (
    83  	k8sSuffix = ".svc." + constants.DefaultClusterLocalDomain
    84  
    85  	printLevel0 = 0
    86  	printLevel1 = 3
    87  	printLevel2 = 6
    88  )
    89  
    90  func printSpaces(numSpaces int) string {
    91  	return strings.Repeat(" ", numSpaces)
    92  }
    93  
    94  var (
    95  	// Ignore unmeshed pods.  This makes it easy to suppress warnings about kube-system etc
    96  	ignoreUnmeshed = false
    97  
    98  	describeNamespace string
    99  )
   100  
   101  func podDescribeCmd(ctx cli.Context) *cobra.Command {
   102  	var opts clioptions.ControlPlaneOptions
   103  	cmd := &cobra.Command{
   104  		Use:     "pod <pod>",
   105  		Aliases: []string{"po"},
   106  		Short:   "Describe pods and their Istio configuration [kube-only]",
   107  		Long: `Analyzes pod, its Services, DestinationRules, and VirtualServices and reports
   108  the configuration objects that affect that pod.`,
   109  		Example: `  istioctl experimental describe pod productpage-v1-c7765c886-7zzd4`,
   110  		RunE: func(cmd *cobra.Command, args []string) error {
   111  			describeNamespace = ctx.NamespaceOrDefault(ctx.Namespace())
   112  			if len(args) != 1 {
   113  				return fmt.Errorf("expecting pod name")
   114  			}
   115  
   116  			podName, ns := handlers.InferPodInfo(args[0], ctx.NamespaceOrDefault(""))
   117  
   118  			client, err := ctx.CLIClient()
   119  			if err != nil {
   120  				return err
   121  			}
   122  			pod, err := client.Kube().CoreV1().Pods(ns).Get(context.TODO(), podName, metav1.GetOptions{})
   123  			if err != nil {
   124  				return err
   125  			}
   126  
   127  			writer := cmd.OutOrStdout()
   128  
   129  			podLabels := klabels.Set(pod.ObjectMeta.Labels)
   130  			annotations := klabels.Set(pod.ObjectMeta.Annotations)
   131  			opts.Revision = GetRevisionFromPodAnnotation(annotations)
   132  
   133  			printPod(writer, pod, opts.Revision)
   134  
   135  			svcs, err := client.Kube().CoreV1().Services(ns).List(context.TODO(), metav1.ListOptions{})
   136  			if err != nil {
   137  				return err
   138  			}
   139  
   140  			matchingServices := make([]corev1.Service, 0, len(svcs.Items))
   141  			for _, svc := range svcs.Items {
   142  				if len(svc.Spec.Selector) > 0 {
   143  					svcSelector := klabels.SelectorFromSet(svc.Spec.Selector)
   144  					if svcSelector.Matches(podLabels) {
   145  						matchingServices = append(matchingServices, svc)
   146  					}
   147  				}
   148  			}
   149  			// Validate Istio's "Service association" requirement
   150  			if len(matchingServices) == 0 && !ignoreUnmeshed {
   151  				fmt.Fprintf(cmd.OutOrStdout(),
   152  					"Warning: No Kubernetes Services select pod %s (see https://istio.io/docs/setup/kubernetes/additional-setup/requirements/ )\n", // nolint: lll
   153  					kname(pod.ObjectMeta))
   154  			}
   155  			// TODO look for port collisions between services targeting this pod
   156  
   157  			kubeClient, err := ctx.CLIClientWithRevision(opts.Revision)
   158  			if err != nil {
   159  				return err
   160  			}
   161  
   162  			configClient := client.Istio()
   163  
   164  			podsLabels := []klabels.Set{klabels.Set(pod.ObjectMeta.Labels)}
   165  			fmt.Fprintf(writer, "--------------------\n")
   166  			err = describePodServices(writer, kubeClient, configClient, pod, matchingServices, podsLabels)
   167  			if err != nil {
   168  				return err
   169  			}
   170  
   171  			// render PeerAuthentication info
   172  			fmt.Fprintf(writer, "--------------------\n")
   173  			err = describePeerAuthentication(writer, kubeClient, configClient, ns, klabels.Set(pod.ObjectMeta.Labels), ctx.IstioNamespace())
   174  			if err != nil {
   175  				return err
   176  			}
   177  
   178  			// TODO find sidecar configs that select this workload and render them
   179  
   180  			// Now look for ingress gateways
   181  			return printIngressInfo(writer, matchingServices, podsLabels, client.Kube(), configClient, kubeClient)
   182  		},
   183  		ValidArgsFunction: completion.ValidPodsNameArgs(ctx),
   184  	}
   185  
   186  	cmd.PersistentFlags().BoolVar(&ignoreUnmeshed, "ignoreUnmeshed", false,
   187  		"Suppress warnings for unmeshed pods")
   188  	cmd.Long += "\n\n" + istioctlutil.ExperimentalMsg
   189  	return cmd
   190  }
   191  
   192  func GetRevisionFromPodAnnotation(anno klabels.Set) string {
   193  	if v, ok := anno[label.IoIstioRev.Name]; ok {
   194  		return v
   195  	}
   196  	statusString := anno.Get(apiannotation.SidecarStatus.Name)
   197  	var injectionStatus inject.SidecarInjectionStatus
   198  	if err := json.Unmarshal([]byte(statusString), &injectionStatus); err != nil {
   199  		return ""
   200  	}
   201  
   202  	return injectionStatus.Revision
   203  }
   204  
   205  func Cmd(ctx cli.Context) *cobra.Command {
   206  	describeCmd := &cobra.Command{
   207  		Use:     "describe",
   208  		Aliases: []string{"des"},
   209  		Short:   "Describe resource and related Istio configuration",
   210  		Args: func(cmd *cobra.Command, args []string) error {
   211  			if len(args) != 0 {
   212  				return fmt.Errorf("unknown resource type %q", args[0])
   213  			}
   214  			return nil
   215  		},
   216  		RunE: func(cmd *cobra.Command, args []string) error {
   217  			describeNamespace = ctx.NamespaceOrDefault(ctx.Namespace())
   218  			cmd.HelpFunc()(cmd, args)
   219  			return nil
   220  		},
   221  	}
   222  
   223  	describeCmd.AddCommand(podDescribeCmd(ctx))
   224  	describeCmd.AddCommand(svcDescribeCmd(ctx))
   225  	return describeCmd
   226  }
   227  
   228  // Append ".svc.cluster.local" if it isn't already present
   229  func extendFQDN(host string) string {
   230  	if host[0] == '*' {
   231  		return host
   232  	}
   233  	if strings.HasSuffix(host, k8sSuffix) {
   234  		return host
   235  	}
   236  	return host + k8sSuffix
   237  }
   238  
   239  // getDestRuleSubsets gets names of subsets that match any pod labels (also, ones that don't match).
   240  func getDestRuleSubsets(subsets []*v1alpha3.Subset, podsLabels []klabels.Set) ([]string, []string) {
   241  	matchingSubsets := make([]string, 0, len(subsets))
   242  	nonmatchingSubsets := make([]string, 0, len(subsets))
   243  	for _, subset := range subsets {
   244  		subsetSelector := klabels.SelectorFromSet(subset.Labels)
   245  		if matchesAnyPod(subsetSelector, podsLabels) {
   246  			matchingSubsets = append(matchingSubsets, subset.Name)
   247  		} else {
   248  			nonmatchingSubsets = append(nonmatchingSubsets, subset.Name)
   249  		}
   250  	}
   251  
   252  	return matchingSubsets, nonmatchingSubsets
   253  }
   254  
   255  func matchesAnyPod(subsetSelector klabels.Selector, podsLabels []klabels.Set) bool {
   256  	for _, podLabels := range podsLabels {
   257  		if subsetSelector.Matches(podLabels) {
   258  			return true
   259  		}
   260  	}
   261  	return false
   262  }
   263  
   264  func printDestinationRule(writer io.Writer, initPrintNum int,
   265  	dr *clientnetworking.DestinationRule, podsLabels []klabels.Set,
   266  ) {
   267  	fmt.Fprintf(writer, "%sDestinationRule: %s for %q\n",
   268  		printSpaces(initPrintNum+printLevel0), kname(dr.ObjectMeta), dr.Spec.Host)
   269  
   270  	matchingSubsets, nonmatchingSubsets := getDestRuleSubsets(dr.Spec.Subsets, podsLabels)
   271  	if len(matchingSubsets) != 0 || len(nonmatchingSubsets) != 0 {
   272  		if len(matchingSubsets) == 0 {
   273  			fmt.Fprintf(writer, "%sWARNING POD DOES NOT MATCH ANY SUBSETS.  (Non matching subsets %s)\n",
   274  				printSpaces(initPrintNum+printLevel1), strings.Join(nonmatchingSubsets, ","))
   275  		}
   276  		fmt.Fprintf(writer, "%sMatching subsets: %s\n",
   277  			printSpaces(initPrintNum+printLevel1), strings.Join(matchingSubsets, ","))
   278  		if len(nonmatchingSubsets) > 0 {
   279  			fmt.Fprintf(writer, "%s(Non-matching subsets %s)\n",
   280  				printSpaces(initPrintNum+printLevel2), strings.Join(nonmatchingSubsets, ","))
   281  		}
   282  	}
   283  
   284  	// Ignore LoadBalancer, ConnectionPool, OutlierDetection
   285  	trafficPolicy := dr.Spec.TrafficPolicy
   286  	if trafficPolicy == nil {
   287  		fmt.Fprintf(writer, "%sNo Traffic Policy\n", printSpaces(initPrintNum+printLevel1))
   288  	} else {
   289  		if trafficPolicy.Tls != nil {
   290  			fmt.Fprintf(writer, "%sTraffic Policy TLS Mode: %s\n",
   291  				printSpaces(initPrintNum+printLevel1), dr.Spec.TrafficPolicy.Tls.Mode.String())
   292  		}
   293  		shortPolicies := recordShortPolicies(
   294  			trafficPolicy.LoadBalancer,
   295  			trafficPolicy.ConnectionPool,
   296  			trafficPolicy.OutlierDetection)
   297  		if shortPolicies != "" {
   298  			fmt.Fprintf(writer, "%s%s", printSpaces(initPrintNum+printLevel1), shortPolicies)
   299  		}
   300  
   301  		if trafficPolicy.PortLevelSettings != nil {
   302  			fmt.Fprintf(writer, "%sPort Level Settings:\n", printSpaces(initPrintNum+printLevel1))
   303  			for _, ps := range trafficPolicy.PortLevelSettings {
   304  				fmt.Fprintf(writer, "%s%d:\n", printSpaces(4), ps.GetPort().GetNumber())
   305  				if ps.Tls != nil {
   306  					fmt.Fprintf(writer, "%sTLS Mode: %s\n", printSpaces(initPrintNum+printLevel2), ps.Tls.Mode.String())
   307  				}
   308  				if sp := recordShortPolicies(
   309  					ps.LoadBalancer,
   310  					ps.ConnectionPool,
   311  					ps.OutlierDetection); sp != "" {
   312  					fmt.Fprintf(writer, "%s%s", printSpaces(initPrintNum+printLevel2), sp)
   313  				}
   314  			}
   315  		}
   316  	}
   317  }
   318  
   319  func recordShortPolicies(lb *v1alpha3.LoadBalancerSettings,
   320  	connectionPool *v1alpha3.ConnectionPoolSettings,
   321  	outlierDetection *v1alpha3.OutlierDetection,
   322  ) string {
   323  	extra := make([]string, 0)
   324  	if lb != nil {
   325  		extra = append(extra, "load balancer")
   326  	}
   327  	if connectionPool != nil {
   328  		extra = append(extra, "connection pool")
   329  	}
   330  	if outlierDetection != nil {
   331  		extra = append(extra, "outlier detection")
   332  	}
   333  	if len(extra) > 0 {
   334  		return fmt.Sprintf("Policies: %s\n", strings.Join(extra, "/"))
   335  	}
   336  	return ""
   337  }
   338  
   339  // httpRouteMatchSvc returns true if it matches and a slice of facts about the match
   340  func httpRouteMatchSvc(vs *clientnetworking.VirtualService, route *v1alpha3.HTTPRoute, svc corev1.Service, matchingSubsets []string, nonmatchingSubsets []string, dr *clientnetworking.DestinationRule) (bool, []string) { // nolint: lll
   341  	svcHost := extendFQDN(fmt.Sprintf("%s.%s", svc.ObjectMeta.Name, svc.ObjectMeta.Namespace))
   342  	facts := []string{}
   343  	mismatchNotes := []string{}
   344  	match := false
   345  	for _, dest := range route.Route {
   346  		fqdn := string(model.ResolveShortnameToFQDN(dest.Destination.Host, config.Meta{Namespace: vs.Namespace}))
   347  		if extendFQDN(fqdn) == svcHost {
   348  			if dest.Destination.Subset != "" {
   349  				if slices.Contains(nonmatchingSubsets, dest.Destination.Subset) {
   350  					mismatchNotes = append(mismatchNotes, fmt.Sprintf("Route to non-matching subset %s for (%s)",
   351  						dest.Destination.Subset,
   352  						renderMatches(route.Match)))
   353  					continue
   354  				}
   355  				if !slices.Contains(matchingSubsets, dest.Destination.Subset) {
   356  					if dr == nil {
   357  						// Don't bother giving the match conditions, the problem is that there are unknowns in the VirtualService
   358  						mismatchNotes = append(mismatchNotes, fmt.Sprintf("Warning: Route to subset %s but NO DESTINATION RULE defining subsets!", dest.Destination.Subset))
   359  					} else {
   360  						// Don't bother giving the match conditions, the problem is that there are unknowns in the VirtualService
   361  						mismatchNotes = append(mismatchNotes,
   362  							fmt.Sprintf("Warning: Route to UNKNOWN subset %s; check DestinationRule %s", dest.Destination.Subset, kname(dr.ObjectMeta)))
   363  					}
   364  					continue
   365  				}
   366  			}
   367  
   368  			match = true
   369  			if dest.Weight > 0 {
   370  				fact := fmt.Sprintf("Route to host \"%s\"", dest.Destination.Host)
   371  				if dest.Destination.Subset != "" {
   372  					fact = fmt.Sprintf("%s subset \"%s\"", fact, dest.Destination.Subset)
   373  				}
   374  				fact = fmt.Sprintf("%s with weight %d%%", fact, dest.Weight)
   375  				facts = append(facts, fact)
   376  			}
   377  			// Consider adding RemoveResponseHeaders, AppendResponseHeaders, RemoveRequestHeaders, AppendRequestHeaders
   378  		} else {
   379  			if dest.Destination.Subset == "" {
   380  				differentHostFact := fmt.Sprintf("Route to host \"%s\" with weight %d%%", dest.Destination.Host, dest.Weight)
   381  				facts = append(facts, differentHostFact)
   382  			} else {
   383  				facts = append(facts, fmt.Sprintf("Route to %s with invalid config", dest.Destination.Host))
   384  			}
   385  		}
   386  	}
   387  
   388  	if match {
   389  		reqMatchFacts := []string{}
   390  
   391  		if route.Fault != nil {
   392  			reqMatchFacts = append(reqMatchFacts, fmt.Sprintf("Fault injection %s", route.Fault.String()))
   393  		}
   394  
   395  		// TODO Consider adding Headers, SourceLabels
   396  
   397  		for _, trafficMatch := range route.Match {
   398  			reqMatchFacts = append(reqMatchFacts, renderMatch(trafficMatch))
   399  		}
   400  
   401  		if len(reqMatchFacts) > 0 {
   402  			facts = append(facts, strings.Join(reqMatchFacts, ", "))
   403  		}
   404  	}
   405  
   406  	if !match && len(mismatchNotes) > 0 {
   407  		facts = append(facts, mismatchNotes...)
   408  	}
   409  	return match, facts
   410  }
   411  
   412  func tcpRouteMatchSvc(vs *clientnetworking.VirtualService, route *v1alpha3.TCPRoute, svc corev1.Service) (bool, []string) {
   413  	match := false
   414  	facts := []string{}
   415  	svcHost := extendFQDN(fmt.Sprintf("%s.%s", svc.ObjectMeta.Name, svc.ObjectMeta.Namespace))
   416  	for _, dest := range route.Route {
   417  		fqdn := string(model.ResolveShortnameToFQDN(dest.Destination.Host, config.Meta{Namespace: vs.Namespace}))
   418  		if extendFQDN(fqdn) == svcHost {
   419  			match = true
   420  		}
   421  	}
   422  
   423  	if match {
   424  		for _, trafficMatch := range route.Match {
   425  			facts = append(facts, trafficMatch.String())
   426  		}
   427  	}
   428  
   429  	return match, facts
   430  }
   431  
   432  func renderStringMatch(sm *v1alpha3.StringMatch) string {
   433  	if sm == nil {
   434  		return ""
   435  	}
   436  
   437  	switch x := sm.MatchType.(type) {
   438  	case *v1alpha3.StringMatch_Exact:
   439  		return x.Exact
   440  	case *v1alpha3.StringMatch_Prefix:
   441  		return x.Prefix + "*"
   442  	}
   443  
   444  	return sm.String()
   445  }
   446  
   447  func renderMatches(trafficMatches []*v1alpha3.HTTPMatchRequest) string {
   448  	if len(trafficMatches) == 0 {
   449  		return "everything"
   450  	}
   451  
   452  	matches := []string{}
   453  	for _, trafficMatch := range trafficMatches {
   454  		matches = append(matches, renderMatch(trafficMatch))
   455  	}
   456  	return strings.Join(matches, ", ")
   457  }
   458  
   459  func renderMatch(match *v1alpha3.HTTPMatchRequest) string {
   460  	retval := "Match: "
   461  	// TODO Are users interested in seeing Scheme, Method, Authority?
   462  	if match.Uri != nil {
   463  		retval += renderStringMatch(match.Uri)
   464  
   465  		if match.IgnoreUriCase {
   466  			retval += " uncased"
   467  		}
   468  	}
   469  
   470  	if len(match.Headers) > 0 {
   471  		headerConds := []string{}
   472  		for key, val := range match.Headers {
   473  			headerConds = append(headerConds, fmt.Sprintf("%s=%s", key, renderStringMatch(val)))
   474  		}
   475  		retval += " when headers are " + strings.Join(headerConds, "; ")
   476  	}
   477  
   478  	// TODO QueryParams, maybe Gateways
   479  	return strings.TrimSpace(retval)
   480  }
   481  
   482  func printPod(writer io.Writer, pod *corev1.Pod, revision string) {
   483  	ports := []string{}
   484  	UserID := int64(1337)
   485  	for _, container := range pod.Spec.Containers {
   486  		for _, port := range container.Ports {
   487  			var protocol string
   488  			// Suppress /<protocol> for TCP, print it for everything else
   489  			if port.Protocol != "TCP" {
   490  				protocol = fmt.Sprintf("/%s", port.Protocol)
   491  			}
   492  			ports = append(ports, fmt.Sprintf("%d%s (%s)", port.ContainerPort, protocol, container.Name))
   493  		}
   494  		// Ref: https://istio.io/latest/docs/ops/deployment/requirements/#pod-requirements
   495  		if container.Name != "istio-proxy" && container.Name != "istio-operator" {
   496  			if container.SecurityContext != nil && container.SecurityContext.RunAsUser != nil {
   497  				if *container.SecurityContext.RunAsUser == UserID {
   498  					fmt.Fprintf(writer, "WARNING: User ID (UID) 1337 is reserved for the sidecar proxy.\n")
   499  				}
   500  			}
   501  		}
   502  	}
   503  
   504  	fmt.Fprintf(writer, "Pod: %s\n", kname(pod.ObjectMeta))
   505  	fmt.Fprintf(writer, "   Pod Revision: %s\n", revision)
   506  	if len(ports) > 0 {
   507  		fmt.Fprintf(writer, "   Pod Ports: %s\n", strings.Join(ports, ", "))
   508  	} else {
   509  		fmt.Fprintf(writer, "   Pod does not expose ports\n")
   510  	}
   511  
   512  	if pod.Status.Phase != corev1.PodRunning {
   513  		fmt.Printf("   Pod is not %s (%s)\n", corev1.PodRunning, pod.Status.Phase)
   514  		return
   515  	}
   516  
   517  	for _, containerStatus := range pod.Status.ContainerStatuses {
   518  		if !containerStatus.Ready {
   519  			fmt.Fprintf(writer, "WARNING: Pod %s Container %s NOT READY\n", kname(pod.ObjectMeta), containerStatus.Name)
   520  		}
   521  	}
   522  	for _, containerStatus := range pod.Status.InitContainerStatuses {
   523  		if !containerStatus.Ready {
   524  			fmt.Fprintf(writer, "WARNING: Pod %s Init Container %s NOT READY\n", kname(pod.ObjectMeta), containerStatus.Name)
   525  		}
   526  	}
   527  
   528  	if ignoreUnmeshed {
   529  		return
   530  	}
   531  
   532  	if !isMeshed(pod) {
   533  		fmt.Fprintf(writer, "WARNING: %s is not part of mesh; no Istio sidecar\n", kname(pod.ObjectMeta))
   534  		return
   535  	}
   536  
   537  	// Ref: https://istio.io/latest/docs/ops/deployment/requirements/#pod-requirements
   538  	if pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.RunAsUser != nil {
   539  		if *pod.Spec.SecurityContext.RunAsUser == UserID {
   540  			fmt.Fprintf(writer, "   WARNING: User ID (UID) 1337 is reserved for the sidecar proxy.\n")
   541  		}
   542  	}
   543  
   544  	// https://istio.io/docs/setup/kubernetes/additional-setup/requirements/
   545  	// says "We recommend adding an explicit app label and version label to deployments."
   546  	if !labels.HasCanonicalServiceName(pod.Labels) {
   547  		fmt.Fprintf(writer, "Suggestion: add required service name label for Istio telemetry. "+
   548  			"See %s.\n", url.DeploymentRequirements)
   549  	}
   550  	if !labels.HasCanonicalServiceRevision(pod.Labels) {
   551  		fmt.Fprintf(writer, "Suggestion: add required service revision label for Istio telemetry. "+
   552  			"See %s.\n", url.DeploymentRequirements)
   553  	}
   554  }
   555  
   556  func kname(meta metav1.ObjectMeta) string {
   557  	if meta.Namespace == describeNamespace {
   558  		return meta.Name
   559  	}
   560  
   561  	// Use the Istio convention pod-name[.namespace]
   562  	return fmt.Sprintf("%s.%s", meta.Name, meta.Namespace)
   563  }
   564  
   565  func printService(writer io.Writer, svc corev1.Service, pod *corev1.Pod) {
   566  	fmt.Fprintf(writer, "Service: %s\n", kname(svc.ObjectMeta))
   567  	for _, port := range svc.Spec.Ports {
   568  		if port.Protocol != "TCP" {
   569  			// Ignore UDP ports, which are not supported by Istio
   570  			continue
   571  		}
   572  		// Get port number
   573  		nport, err := pilotcontroller.FindPort(pod, &port)
   574  		if err == nil {
   575  			protocol := findProtocolForPort(&port)
   576  			fmt.Fprintf(writer, "   Port: %s %d/%s targets pod port %d\n", port.Name, port.Port, protocol, nport)
   577  		} else {
   578  			fmt.Fprintf(writer, "   %s\n", err.Error())
   579  		}
   580  	}
   581  }
   582  
   583  func findProtocolForPort(port *corev1.ServicePort) string {
   584  	var protocol string
   585  	if port.Name == "" && port.AppProtocol == nil && port.Protocol != corev1.ProtocolUDP {
   586  		protocol = "auto-detect"
   587  	} else {
   588  		protocol = string(configKube.ConvertProtocol(port.Port, port.Name, port.Protocol, port.AppProtocol))
   589  	}
   590  	return protocol
   591  }
   592  
   593  func isMeshed(pod *corev1.Pod) bool {
   594  	return inject.FindSidecar(pod) != nil
   595  }
   596  
   597  // Extract value of key out of Struct, but always return a Struct, even if the value isn't one
   598  func (v *myProtoValue) keyAsStruct(key string) *myProtoValue {
   599  	if v == nil || v.GetStructValue() == nil {
   600  		return asMyProtoValue(&structpb.Struct{Fields: make(map[string]*structpb.Value)})
   601  	}
   602  
   603  	return &myProtoValue{v.GetStructValue().Fields[key]}
   604  }
   605  
   606  // asMyProtoValue wraps a protobuf Struct so we may use it with keyAsStruct and keyAsString
   607  func asMyProtoValue(s *structpb.Struct) *myProtoValue {
   608  	return &myProtoValue{
   609  		&structpb.Value{
   610  			Kind: &structpb.Value_StructValue{
   611  				StructValue: s,
   612  			},
   613  		},
   614  	}
   615  }
   616  
   617  func (v *myProtoValue) keyAsString(key string) string {
   618  	s := v.keyAsStruct(key)
   619  	return s.GetStringValue()
   620  }
   621  
   622  func getIstioRBACPolicies(cd *configdump.Wrapper, port int32) ([]string, error) {
   623  	hcm, err := getInboundHTTPConnectionManager(cd, port)
   624  	if err != nil || hcm == nil {
   625  		return []string{}, err
   626  	}
   627  
   628  	// Identify RBAC policies. Currently there are no "breadcrumbs" so we only return the policy names.
   629  	for _, httpFilter := range hcm.HttpFilters {
   630  		if httpFilter.Name == wellknown.HTTPRoleBasedAccessControl {
   631  			rbac := &rbachttp.RBAC{}
   632  			if err := httpFilter.GetTypedConfig().UnmarshalTo(rbac); err == nil {
   633  				policies := []string{}
   634  				for polName := range rbac.Rules.Policies {
   635  					policies = append(policies, polName)
   636  				}
   637  				return policies, nil
   638  			}
   639  		}
   640  	}
   641  
   642  	return []string{}, nil
   643  }
   644  
   645  // Return the first HTTP Connection Manager config for the inbound port
   646  func getInboundHTTPConnectionManager(cd *configdump.Wrapper, port int32) (*hcm.HttpConnectionManager, error) {
   647  	filter := istio_envoy_configdump.ListenerFilter{
   648  		Port: uint32(port),
   649  	}
   650  	listeners, err := cd.GetListenerConfigDump()
   651  	if err != nil {
   652  		return nil, err
   653  	}
   654  
   655  	for _, l := range listeners.DynamicListeners {
   656  		if l.ActiveState == nil {
   657  			continue
   658  		}
   659  		// Support v2 or v3 in config dump. See ads.go:RequestedTypes for more info.
   660  		l.ActiveState.Listener.TypeUrl = v3.ListenerType
   661  		listenerTyped := &listener.Listener{}
   662  		err = l.ActiveState.Listener.UnmarshalTo(listenerTyped)
   663  		if err != nil {
   664  			return nil, err
   665  		}
   666  		if listenerTyped.Name == model.VirtualInboundListenerName {
   667  			for _, filterChain := range listenerTyped.FilterChains {
   668  				for _, filter := range filterChain.Filters {
   669  					hcm := &hcm.HttpConnectionManager{}
   670  					if err := filter.GetTypedConfig().UnmarshalTo(hcm); err == nil {
   671  						return hcm, nil
   672  					}
   673  				}
   674  			}
   675  		}
   676  		// This next check is deprecated in 1.6 and can be removed when we remove
   677  		// the old config_dumps in support of https://github.com/istio/istio/issues/23042
   678  		if filter.Verify(listenerTyped) {
   679  			sockAddr := listenerTyped.Address.GetSocketAddress()
   680  			if sockAddr != nil {
   681  				// Skip outbound listeners
   682  				if sockAddr.Address == "0.0.0.0" {
   683  					continue
   684  				}
   685  			}
   686  
   687  			for _, filterChain := range listenerTyped.FilterChains {
   688  				for _, filter := range filterChain.Filters {
   689  					hcm := &hcm.HttpConnectionManager{}
   690  					if err := filter.GetTypedConfig().UnmarshalTo(hcm); err == nil {
   691  						return hcm, nil
   692  					}
   693  				}
   694  			}
   695  		}
   696  	}
   697  
   698  	return nil, nil
   699  }
   700  
   701  // getIstioVirtualServiceNameForSvc returns name, namespace
   702  func getIstioVirtualServiceNameForSvc(cd *configdump.Wrapper, svc corev1.Service, port int32) (string, string, error) {
   703  	path, err := getIstioVirtualServicePathForSvcFromRoute(cd, svc, port)
   704  	if err != nil {
   705  		return "", "", err
   706  	}
   707  
   708  	// Starting with recent 1.5.0 builds, the path will include .istio.io.  Handle both.
   709  	// nolint: gosimple
   710  	re := regexp.MustCompile("/apis/networking(\\.istio\\.io)?/v1alpha3/namespaces/(?P<namespace>[^/]+)/virtual-service/(?P<name>[^/]+)")
   711  	ss := re.FindStringSubmatch(path)
   712  	if ss == nil {
   713  		return "", "", fmt.Errorf("not a VS path: %s", path)
   714  	}
   715  	return ss[3], ss[2], nil
   716  }
   717  
   718  // getIstioVirtualServicePathForSvcFromRoute returns something like "/apis/networking/v1alpha3/namespaces/default/virtual-service/reviews"
   719  func getIstioVirtualServicePathForSvcFromRoute(cd *configdump.Wrapper, svc corev1.Service, port int32) (string, error) {
   720  	sPort := strconv.Itoa(int(port))
   721  
   722  	// Routes know their destination Service name, namespace, and port, and the DR that configures them
   723  	rcd, err := cd.GetDynamicRouteDump(false)
   724  	if err != nil {
   725  		return "", err
   726  	}
   727  	for _, rcd := range rcd.DynamicRouteConfigs {
   728  		routeTyped := &route.RouteConfiguration{}
   729  		err = rcd.RouteConfig.UnmarshalTo(routeTyped)
   730  		if err != nil {
   731  			return "", err
   732  		}
   733  		if routeTyped.Name != sPort && !strings.HasPrefix(routeTyped.Name, "http.") &&
   734  			!strings.HasPrefix(routeTyped.Name, "https.") {
   735  			continue
   736  		}
   737  
   738  		for _, vh := range routeTyped.VirtualHosts {
   739  			for _, route := range vh.Routes {
   740  				if routeDestinationMatchesSvc(route, svc, vh, port) {
   741  					return getIstioConfig(route.Metadata)
   742  				}
   743  			}
   744  		}
   745  	}
   746  	return "", nil
   747  }
   748  
   749  // routeDestinationMatchesSvc determines whether or not to use this service as a destination
   750  func routeDestinationMatchesSvc(vhRoute *route.Route, svc corev1.Service, vh *route.VirtualHost, port int32) bool {
   751  	if vhRoute == nil {
   752  		return false
   753  	}
   754  
   755  	// Infer from VirtualHost domains matching <service>.<namespace>.svc.cluster.local
   756  	re := regexp.MustCompile(`(?P<service>[^\.]+)\.(?P<namespace>[^\.]+)\.svc\.cluster\.local$`)
   757  	for _, domain := range vh.Domains {
   758  		ss := re.FindStringSubmatch(domain)
   759  		if ss != nil {
   760  			if ss[1] == svc.ObjectMeta.Name && ss[2] == svc.ObjectMeta.Namespace {
   761  				return true
   762  			}
   763  		}
   764  	}
   765  
   766  	clusterName := ""
   767  	switch cs := vhRoute.GetRoute().GetClusterSpecifier().(type) {
   768  	case *route.RouteAction_Cluster:
   769  		clusterName = cs.Cluster
   770  	case *route.RouteAction_WeightedClusters:
   771  		clusterName = cs.WeightedClusters.Clusters[0].GetName()
   772  	}
   773  
   774  	// If this is an ingress gateway, the Domains will be something like *:80, so check routes
   775  	// which will look like "outbound|9080||productpage.default.svc.cluster.local"
   776  	res := fmt.Sprintf(`outbound\|%d\|[^\|]*\|(?P<service>[^\.]+)\.(?P<namespace>[^\.]+)\.svc\.cluster\.local$`, port)
   777  	re = regexp.MustCompile(res)
   778  
   779  	ss := re.FindStringSubmatch(clusterName)
   780  	if ss != nil {
   781  		if ss[1] == svc.ObjectMeta.Name && ss[2] == svc.ObjectMeta.Namespace {
   782  			return true
   783  		}
   784  	}
   785  
   786  	return false
   787  }
   788  
   789  // getIstioConfig returns .metadata.filter_metadata.istio.config, err
   790  func getIstioConfig(metadata *core.Metadata) (string, error) {
   791  	if metadata != nil {
   792  		istioConfig := asMyProtoValue(metadata.FilterMetadata[util.IstioMetadataKey]).
   793  			keyAsString("config")
   794  		return istioConfig, nil
   795  	}
   796  	return "", fmt.Errorf("no istio config")
   797  }
   798  
   799  // getIstioDestinationRuleNameForSvc returns name, namespace
   800  func getIstioDestinationRuleNameForSvc(cd *configdump.Wrapper, svc corev1.Service, port int32) (string, string, error) {
   801  	path, err := getIstioDestinationRulePathForSvc(cd, svc, port)
   802  	if err != nil || path == "" {
   803  		return "", "", err
   804  	}
   805  
   806  	// Starting with recent 1.5.0 builds, the path will include .istio.io.  Handle both.
   807  	// nolint: gosimple
   808  	re := regexp.MustCompile("/apis/networking(\\.istio\\.io)?/v1alpha3/namespaces/(?P<namespace>[^/]+)/destination-rule/(?P<name>[^/]+)")
   809  	ss := re.FindStringSubmatch(path)
   810  	if ss == nil {
   811  		return "", "", fmt.Errorf("not a DR path: %s", path)
   812  	}
   813  	return ss[3], ss[2], nil
   814  }
   815  
   816  // getIstioDestinationRulePathForSvc returns something like "/apis/networking/v1alpha3/namespaces/default/destination-rule/reviews"
   817  func getIstioDestinationRulePathForSvc(cd *configdump.Wrapper, svc corev1.Service, port int32) (string, error) {
   818  	svcHost := extendFQDN(fmt.Sprintf("%s.%s", svc.ObjectMeta.Name, svc.ObjectMeta.Namespace))
   819  	filter := istio_envoy_configdump.ClusterFilter{
   820  		FQDN: host.Name(svcHost),
   821  		Port: int(port),
   822  		// Although we want inbound traffic, ask for outbound traffic, as the DR is
   823  		// not associated with the inbound traffic.
   824  		Direction: model.TrafficDirectionOutbound,
   825  	}
   826  
   827  	dump, err := cd.GetClusterConfigDump()
   828  	if err != nil {
   829  		return "", err
   830  	}
   831  
   832  	for _, dac := range dump.DynamicActiveClusters {
   833  		clusterTyped := &cluster.Cluster{}
   834  		// Support v2 or v3 in config dump. See ads.go:RequestedTypes for more info.
   835  		dac.Cluster.TypeUrl = v3.ClusterType
   836  		err = dac.Cluster.UnmarshalTo(clusterTyped)
   837  		if err != nil {
   838  			return "", err
   839  		}
   840  		if filter.Verify(clusterTyped) {
   841  			metadata := clusterTyped.Metadata
   842  			if metadata != nil {
   843  				istioConfig := asMyProtoValue(metadata.FilterMetadata[util.IstioMetadataKey]).
   844  					keyAsString("config")
   845  				return istioConfig, nil
   846  			}
   847  		}
   848  	}
   849  
   850  	return "", nil
   851  }
   852  
   853  // TODO simplify this by showing for each matching Destination the negation of the previous HttpMatchRequest
   854  // and showing the non-matching Destinations.  (The current code is ad-hoc, and usually shows most of that information.)
   855  func printVirtualService(writer io.Writer, initPrintNum int,
   856  	vs *clientnetworking.VirtualService, svc corev1.Service, matchingSubsets []string, nonmatchingSubsets []string, dr *clientnetworking.DestinationRule,
   857  ) { // nolint: lll
   858  	fmt.Fprintf(writer, "%sVirtualService: %s\n", printSpaces(initPrintNum+printLevel0), kname(vs.ObjectMeta))
   859  
   860  	// There is no point in checking that 'port' uses HTTP (for HTTP route matches)
   861  	// or uses TCP (for TCP route matches) because if the port has the wrong name
   862  	// the VirtualService metadata will not appear.
   863  
   864  	matches := 0
   865  	facts := 0
   866  	mismatchNotes := []string{}
   867  	for _, httpRoute := range vs.Spec.Http {
   868  		routeMatch, newfacts := httpRouteMatchSvc(vs, httpRoute, svc, matchingSubsets, nonmatchingSubsets, dr)
   869  		if routeMatch {
   870  			matches++
   871  			for _, newfact := range newfacts {
   872  				fmt.Fprintf(writer, "%s%s\n", printSpaces(initPrintNum+printLevel1), newfact)
   873  				facts++
   874  			}
   875  		} else {
   876  			mismatchNotes = append(mismatchNotes, newfacts...)
   877  		}
   878  	}
   879  
   880  	// TODO vsSpec.Tls if I can find examples in the wild
   881  
   882  	for _, tcpRoute := range vs.Spec.Tcp {
   883  		routeMatch, newfacts := tcpRouteMatchSvc(vs, tcpRoute, svc)
   884  		if routeMatch {
   885  			matches++
   886  			for _, newfact := range newfacts {
   887  				fmt.Fprintf(writer, "%s%s\n", printSpaces(initPrintNum+printLevel1), newfact)
   888  				facts++
   889  			}
   890  		} else {
   891  			mismatchNotes = append(mismatchNotes, newfacts...)
   892  		}
   893  	}
   894  
   895  	if matches == 0 {
   896  		if len(vs.Spec.Http) > 0 {
   897  			fmt.Fprintf(writer, "%sWARNING: No destinations match pod subsets (checked %d HTTP routes)\n",
   898  				printSpaces(initPrintNum+printLevel1), len(vs.Spec.Http))
   899  		}
   900  		if len(vs.Spec.Tcp) > 0 {
   901  			fmt.Fprintf(writer, "%sWARNING: No destinations match pod subsets (checked %d TCP routes)\n",
   902  				printSpaces(initPrintNum+printLevel1), len(vs.Spec.Tcp))
   903  		}
   904  		for _, mismatch := range mismatchNotes {
   905  			fmt.Fprintf(writer, "%s%s\n",
   906  				printSpaces(initPrintNum+printLevel2), mismatch)
   907  		}
   908  		return
   909  	}
   910  
   911  	possibleDests := len(vs.Spec.Http) + len(vs.Spec.Tls) + len(vs.Spec.Tcp)
   912  	if matches < possibleDests {
   913  		// We've printed the match conditions.  We can't say for sure that matching
   914  		// traffic will reach this pod, because an earlier match condition could have captured it.
   915  		fmt.Fprintf(writer, "%s%d additional destination(s) that will not reach this pod\n",
   916  			printSpaces(initPrintNum+printLevel1), possibleDests-matches)
   917  		// If we matched, but printed nothing, treat this as the catch-all
   918  		if facts == 0 {
   919  			for _, mismatch := range mismatchNotes {
   920  				fmt.Fprintf(writer, "%s%s\n",
   921  					printSpaces(initPrintNum+printLevel2), mismatch)
   922  			}
   923  		}
   924  
   925  		return
   926  	}
   927  
   928  	if facts == 0 {
   929  		// We printed nothing other than the name.  Print something.
   930  		if len(vs.Spec.Http) > 0 {
   931  			fmt.Fprintf(writer, "%s%d HTTP route(s)\n", printSpaces(initPrintNum+printLevel1), len(vs.Spec.Http))
   932  		}
   933  		if len(vs.Spec.Tcp) > 0 {
   934  			fmt.Fprintf(writer, "%s%d TCP route(s)\n", printSpaces(initPrintNum+printLevel1), len(vs.Spec.Tcp))
   935  		}
   936  	}
   937  }
   938  
   939  type ingressInfo struct {
   940  	service *corev1.Service
   941  	pods    []*corev1.Pod
   942  }
   943  
   944  func (ingress *ingressInfo) match(gw *clientnetworking.Gateway) bool {
   945  	if ingress == nil || gw == nil {
   946  		return false
   947  	}
   948  	if gw.Spec.Selector == nil {
   949  		return true
   950  	}
   951  	for _, p := range ingress.pods {
   952  		if maps.Contains(p.GetLabels(), gw.Spec.Selector) {
   953  			return true
   954  		}
   955  	}
   956  	return false
   957  }
   958  
   959  func (ingress *ingressInfo) getIngressIP() string {
   960  	if ingress == nil || ingress.service == nil || len(ingress.pods) == 0 {
   961  		return "unknown"
   962  	}
   963  
   964  	if len(ingress.service.Status.LoadBalancer.Ingress) > 0 {
   965  		return ingress.service.Status.LoadBalancer.Ingress[0].IP
   966  	}
   967  
   968  	if hIP := ingress.pods[0].Status.HostIP; hIP != "" {
   969  		return hIP
   970  	}
   971  
   972  	// The scope of this function is to get the IP from Kubernetes, we do not
   973  	// ask Docker or minikube for an IP.
   974  	// See https://istio.io/docs/tasks/traffic-management/ingress/ingress-control/#determining-the-ingress-ip-and-ports
   975  	return "unknown"
   976  }
   977  
   978  func printIngressInfo(
   979  	writer io.Writer,
   980  	matchingServices []corev1.Service,
   981  	podsLabels []klabels.Set,
   982  	kubeClient kubernetes.Interface,
   983  	configClient istioclient.Interface,
   984  	client kube.CLIClient,
   985  ) error {
   986  	pods, err := kubeClient.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{
   987  		LabelSelector: "istio=ingressgateway",
   988  		FieldSelector: "status.phase=Running",
   989  	})
   990  	if err != nil {
   991  		return multierror.Prefix(err, "Could not find ingress gateway pods")
   992  	}
   993  	if len(pods.Items) == 0 {
   994  		fmt.Fprintf(writer, "Skipping Gateway information (no ingress gateway pods)\n")
   995  		return nil
   996  	}
   997  	// key: namespace
   998  	ingressPods := map[string][]*corev1.Pod{}
   999  	ingressNss := sets.New[string]()
  1000  	for i, pod := range pods.Items {
  1001  		ns := pod.GetNamespace()
  1002  		ingressNss.Insert(ns)
  1003  		ingressPods[ns] = append(ingressPods[ns], pods.Items[i].DeepCopy())
  1004  	}
  1005  
  1006  	foundIngresses := []*ingressInfo{}
  1007  	for _, ns := range ingressNss.UnsortedList() {
  1008  		// Currently no support for non-standard gateways selecting non ingressgateway pods
  1009  		serviceList, err := kubeClient.CoreV1().Services(ns).List(context.TODO(), metav1.ListOptions{})
  1010  		if err == nil {
  1011  			for i, s := range serviceList.Items {
  1012  				iInfo := &ingressInfo{
  1013  					service: serviceList.Items[i].DeepCopy(),
  1014  				}
  1015  				for j, p := range ingressPods[ns] {
  1016  					if p.GetLabels() == nil {
  1017  						continue
  1018  					}
  1019  					if maps.Contains(p.GetLabels(), s.Spec.Selector) {
  1020  						iInfo.pods = append(iInfo.pods, ingressPods[ns][j])
  1021  					}
  1022  				}
  1023  				if len(iInfo.pods) > 0 {
  1024  					foundIngresses = append(foundIngresses, iInfo)
  1025  				}
  1026  			}
  1027  		}
  1028  	}
  1029  
  1030  	if len(foundIngresses) == 0 {
  1031  		fmt.Fprintf(writer, "Skipping Gateway information (no ingress gateway service)\n")
  1032  	}
  1033  
  1034  	newResourceID := func(ns, name string) string { return fmt.Sprintf("%s/%s", ns, name) }
  1035  	recordVirtualServices := map[string]*clientnetworking.VirtualService{}
  1036  	recordDestinationRules := map[string]*clientnetworking.DestinationRule{}
  1037  	// recordGateways, key: ns/gwName
  1038  	recordGateways := map[string]bool{}
  1039  
  1040  	for _, pod := range pods.Items {
  1041  		byConfigDump, err := client.EnvoyDo(context.TODO(), pod.Name, pod.Namespace, "GET", "config_dump")
  1042  		if err != nil {
  1043  			return fmt.Errorf("failed to execute command on ingress gateway sidecar: %v", err)
  1044  		}
  1045  		cd := configdump.Wrapper{}
  1046  		err = cd.UnmarshalJSON(byConfigDump)
  1047  		if err != nil {
  1048  			return fmt.Errorf("can't parse ingress gateway sidecar config_dump: %v", err)
  1049  		}
  1050  
  1051  		for _, svc := range matchingServices {
  1052  			for _, port := range svc.Spec.Ports {
  1053  				// found destination rule and matching subsets
  1054  				matchingSubsets := []string{}
  1055  				nonMatchingSubsets := []string{}
  1056  				drName, drNamespace, err := getIstioDestinationRuleNameForSvc(&cd, svc, port.Port)
  1057  				var dr *clientnetworking.DestinationRule
  1058  				if err == nil && drName != "" && drNamespace != "" {
  1059  					exist := false
  1060  					dr, exist = recordDestinationRules[newResourceID(drNamespace, drName)]
  1061  					if !exist {
  1062  						dr, _ = configClient.NetworkingV1alpha3().DestinationRules(drNamespace).Get(context.Background(), drName, metav1.GetOptions{})
  1063  						if dr == nil {
  1064  							fmt.Fprintf(writer,
  1065  								"WARNING: Proxy is stale; it references to non-existent destination rule %s.%s\n",
  1066  								drName, drNamespace)
  1067  						}
  1068  						recordDestinationRules[newResourceID(drNamespace, drName)] = dr.DeepCopy()
  1069  					}
  1070  				}
  1071  				if dr != nil {
  1072  					matchingSubsets, nonMatchingSubsets = getDestRuleSubsets(dr.Spec.Subsets, podsLabels)
  1073  				}
  1074  
  1075  				// found virtual service
  1076  				vsName, vsNamespace, err := getIstioVirtualServiceNameForSvc(&cd, svc, port.Port)
  1077  				var vs *clientnetworking.VirtualService
  1078  				if err == nil && vsName != "" && vsNamespace != "" {
  1079  					exist := false
  1080  					vs, exist = recordVirtualServices[newResourceID(vsNamespace, vsName)]
  1081  					if !exist {
  1082  						vs, _ = configClient.NetworkingV1alpha3().VirtualServices(vsNamespace).Get(context.Background(), vsName, metav1.GetOptions{})
  1083  						if vs == nil {
  1084  							fmt.Fprintf(writer,
  1085  								"WARNING: Proxy is stale; it references to non-existent virtual service %s.%s\n",
  1086  								vsName, vsNamespace)
  1087  						}
  1088  						recordVirtualServices[newResourceID(vsNamespace, vsName)] = vs.DeepCopy()
  1089  					}
  1090  					if vs != nil {
  1091  						// Matching gateways from vs.spec.gateways
  1092  						for _, gatewayName := range vs.Spec.Gateways {
  1093  							if gatewayName == "" || gatewayName == analyzerutil.MeshGateway {
  1094  								continue
  1095  							}
  1096  							// parse gateway
  1097  							gns := vsNamespace
  1098  							parts := strings.SplitN(gatewayName, "/", 2)
  1099  							if len(parts) == 2 {
  1100  								gatewayName = parts[1]
  1101  								gns = parts[0]
  1102  							}
  1103  							// todo: check istiod env `PILOT_SCOPE_GATEWAY_TO_NAMESPACE`, if true, need to match gateway namespace
  1104  
  1105  							gwID := newResourceID(gns, gatewayName)
  1106  							if gok := recordGateways[gwID]; !gok {
  1107  								gw, _ := configClient.NetworkingV1alpha3().Gateways(gns).Get(context.Background(), gatewayName, metav1.GetOptions{})
  1108  								if gw != nil {
  1109  									recordGateways[gwID] = true
  1110  									if gw.Spec.Selector == nil {
  1111  										fmt.Fprintf(writer,
  1112  											"Ingress Gateway %s/%s be applyed all workloads",
  1113  											gns, gatewayName)
  1114  										continue
  1115  									}
  1116  
  1117  									var matchIngressInfos []*ingressInfo
  1118  									for i, ingress := range foundIngresses {
  1119  										if ingress.match(gw) {
  1120  											matchIngressInfos = append(matchIngressInfos, foundIngresses[i])
  1121  										}
  1122  									}
  1123  									if len(matchIngressInfos) > 0 {
  1124  										sort.Slice(matchIngressInfos, func(i, j int) bool {
  1125  											return matchIngressInfos[i].getIngressIP() < matchIngressInfos[j].getIngressIP()
  1126  										})
  1127  										fmt.Fprintf(writer, "--------------------\n")
  1128  										for _, ingress := range matchIngressInfos {
  1129  											printIngressService(writer, printLevel0, ingress)
  1130  										}
  1131  										printVirtualService(writer, printLevel0, vs, svc, matchingSubsets, nonMatchingSubsets, dr)
  1132  									}
  1133  								} else {
  1134  									fmt.Fprintf(writer,
  1135  										"WARNING: Proxy is stale; it references to non-existent gateway %s/%s\n",
  1136  										gns, gatewayName)
  1137  								}
  1138  							}
  1139  						}
  1140  					}
  1141  				}
  1142  			}
  1143  		}
  1144  	}
  1145  
  1146  	return nil
  1147  }
  1148  
  1149  func printIngressService(writer io.Writer, initPrintNum int,
  1150  	ingress *ingressInfo,
  1151  ) {
  1152  	if ingress == nil || ingress.service == nil || len(ingress.pods) == 0 {
  1153  		return
  1154  	}
  1155  	// The ingressgateway service offers a lot of ports but the pod doesn't listen to all
  1156  	// of them.  For example, it doesn't listen on 443 without additional setup.  This prints
  1157  	// the most basic output.
  1158  	portsToShow := map[string]bool{
  1159  		"http2": true,
  1160  		"http":  true,
  1161  	}
  1162  	protocolToScheme := map[string]string{
  1163  		"HTTP2": "http",
  1164  		"HTTP":  "http",
  1165  	}
  1166  	schemePortDefault := map[string]int{
  1167  		"http": 80,
  1168  	}
  1169  
  1170  	for _, port := range ingress.service.Spec.Ports {
  1171  		if port.Protocol != "TCP" || !portsToShow[port.Name] {
  1172  			continue
  1173  		}
  1174  
  1175  		// Get port number
  1176  		_, err := pilotcontroller.FindPort(ingress.pods[0], &port)
  1177  		if err == nil {
  1178  			nport := int(port.Port)
  1179  			protocol := string(configKube.ConvertProtocol(port.Port, port.Name, port.Protocol, port.AppProtocol))
  1180  
  1181  			scheme := protocolToScheme[protocol]
  1182  			portSuffix := ""
  1183  			if schemePortDefault[scheme] != nport {
  1184  				portSuffix = fmt.Sprintf(":%d", nport)
  1185  			}
  1186  			ip := ingress.getIngressIP()
  1187  			fmt.Fprintf(writer, "%sExposed on Ingress Gateway %s://%s%s\n", printSpaces(initPrintNum), scheme, ip, portSuffix)
  1188  		}
  1189  	}
  1190  }
  1191  
  1192  func svcDescribeCmd(ctx cli.Context) *cobra.Command {
  1193  	var opts clioptions.ControlPlaneOptions
  1194  	cmd := &cobra.Command{
  1195  		Use:     "service <svc>",
  1196  		Aliases: []string{"svc"},
  1197  		Short:   "Describe services and their Istio configuration [kube-only]",
  1198  		Long: `Analyzes service, pods, DestinationRules, and VirtualServices and reports
  1199  the configuration objects that affect that service.`,
  1200  		Example: `  istioctl experimental describe service productpage`,
  1201  		Args: func(cmd *cobra.Command, args []string) error {
  1202  			if len(args) != 1 {
  1203  				cmd.Println(cmd.UsageString())
  1204  				return fmt.Errorf("expecting service name")
  1205  			}
  1206  			return nil
  1207  		},
  1208  		RunE: func(cmd *cobra.Command, args []string) error {
  1209  			describeNamespace = ctx.NamespaceOrDefault(ctx.Namespace())
  1210  			svcName, ns := handlers.InferPodInfo(args[0], ctx.NamespaceOrDefault(ctx.Namespace()))
  1211  
  1212  			client, err := ctx.CLIClient()
  1213  			if err != nil {
  1214  				return err
  1215  			}
  1216  			svc, err := client.Kube().CoreV1().Services(ns).Get(context.TODO(), svcName, metav1.GetOptions{})
  1217  			if err != nil {
  1218  				return err
  1219  			}
  1220  
  1221  			writer := cmd.OutOrStdout()
  1222  
  1223  			labels := make([]string, 0)
  1224  			for k, v := range svc.Spec.Selector {
  1225  				labels = append(labels, fmt.Sprintf("%s=%s", k, v))
  1226  			}
  1227  
  1228  			matchingPods := make([]corev1.Pod, 0)
  1229  			var selectedPodCount int
  1230  			if len(labels) > 0 {
  1231  				pods, err := client.Kube().CoreV1().Pods(ns).List(context.TODO(), metav1.ListOptions{
  1232  					LabelSelector: strings.Join(labels, ","),
  1233  				})
  1234  				if err != nil {
  1235  					return err
  1236  				}
  1237  				selectedPodCount = len(pods.Items)
  1238  				for _, pod := range pods.Items {
  1239  					if pod.Status.Phase != corev1.PodRunning {
  1240  						fmt.Printf("   Pod is not %s (%s)\n", corev1.PodRunning, pod.Status.Phase)
  1241  						continue
  1242  					}
  1243  
  1244  					ready, err := containerReady(&pod, inject.ProxyContainerName)
  1245  					if err != nil {
  1246  						fmt.Fprintf(writer, "Pod %s: %s\n", kname(pod.ObjectMeta), err)
  1247  						continue
  1248  					}
  1249  					if !ready {
  1250  						fmt.Fprintf(writer, "WARNING: Pod %s Container %s NOT READY\n", kname(pod.ObjectMeta), inject.ProxyContainerName)
  1251  						continue
  1252  					}
  1253  					matchingPods = append(matchingPods, pod)
  1254  				}
  1255  			}
  1256  
  1257  			if len(matchingPods) == 0 {
  1258  				if selectedPodCount == 0 {
  1259  					fmt.Fprintf(writer, "Service %q has no pods.\n", kname(svc.ObjectMeta))
  1260  					return nil
  1261  				}
  1262  				fmt.Fprintf(writer, "Service %q has no Istio pods.  (%d pods in service).\n", kname(svc.ObjectMeta), selectedPodCount)
  1263  				fmt.Fprintf(writer, "Use `istioctl kube-inject` or redeploy with Istio automatic sidecar injection.\n")
  1264  				return nil
  1265  			}
  1266  
  1267  			kubeClient, err := ctx.CLIClientWithRevision(opts.Revision)
  1268  			if err != nil {
  1269  				return err
  1270  			}
  1271  
  1272  			configClient := client.Istio()
  1273  
  1274  			// Get all the labels for all the matching pods.  We will used this to complain
  1275  			// if NONE of the pods match a VirtualService
  1276  			podsLabels := make([]klabels.Set, len(matchingPods))
  1277  			for i, pod := range matchingPods {
  1278  				podsLabels[i] = klabels.Set(pod.ObjectMeta.Labels)
  1279  			}
  1280  
  1281  			// Describe based on the Envoy config for this first pod only
  1282  			pod := matchingPods[0]
  1283  
  1284  			// Only consider the service invoked with this command, not other services that might select the pod
  1285  			svcs := []corev1.Service{*svc}
  1286  
  1287  			err = describePodServices(writer, kubeClient, configClient, &pod, svcs, podsLabels)
  1288  			if err != nil {
  1289  				return err
  1290  			}
  1291  
  1292  			// Now look for ingress gateways
  1293  			return printIngressInfo(writer, svcs, podsLabels, client.Kube(), configClient, kubeClient)
  1294  		},
  1295  		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
  1296  			return completion.ValidServiceArgs(cmd, ctx, args, toComplete)
  1297  		},
  1298  	}
  1299  
  1300  	cmd.PersistentFlags().BoolVar(&ignoreUnmeshed, "ignoreUnmeshed", false,
  1301  		"Suppress warnings for unmeshed pods")
  1302  	cmd.Long += "\n\n" + istioctlutil.ExperimentalMsg
  1303  	return cmd
  1304  }
  1305  
  1306  func describePodServices(writer io.Writer, kubeClient kube.CLIClient, configClient istioclient.Interface, pod *corev1.Pod, matchingServices []corev1.Service, podsLabels []klabels.Set) error { // nolint: lll
  1307  	byConfigDump, err := kubeClient.EnvoyDo(context.TODO(), pod.ObjectMeta.Name, pod.ObjectMeta.Namespace, "GET", "config_dump")
  1308  	if err != nil {
  1309  		if ignoreUnmeshed {
  1310  			return nil
  1311  		}
  1312  
  1313  		return fmt.Errorf("failed to execute command on sidecar: %v", err)
  1314  	}
  1315  
  1316  	cd := configdump.Wrapper{}
  1317  	err = cd.UnmarshalJSON(byConfigDump)
  1318  	if err != nil {
  1319  		return fmt.Errorf("can't parse sidecar config_dump for %v: %v", err, pod.ObjectMeta.Name)
  1320  	}
  1321  
  1322  	for row, svc := range matchingServices {
  1323  		if row != 0 {
  1324  			fmt.Fprintf(writer, "--------------------\n")
  1325  		}
  1326  		printService(writer, svc, pod)
  1327  
  1328  		needPrintPort := false
  1329  		initPolicyLevel := printLevel0
  1330  		if len(svc.Spec.Ports) > 1 {
  1331  			needPrintPort = true
  1332  			initPolicyLevel = printLevel1
  1333  		}
  1334  		for _, port := range svc.Spec.Ports {
  1335  			if needPrintPort {
  1336  				// If there is more than one port, prefix each DR by the port it applies to
  1337  				fmt.Fprintf(writer, "%d:\n", port.Port)
  1338  			}
  1339  			matchingSubsets := []string{}
  1340  			nonmatchingSubsets := []string{}
  1341  			drName, drNamespace, err := getIstioDestinationRuleNameForSvc(&cd, svc, port.Port)
  1342  			if err != nil {
  1343  				log.Errorf("fetch destination rule for %v: %v", svc.Name, err)
  1344  			}
  1345  			var dr *clientnetworking.DestinationRule
  1346  			if err == nil && drName != "" && drNamespace != "" {
  1347  				dr, _ = configClient.NetworkingV1alpha3().DestinationRules(drNamespace).Get(context.Background(), drName, metav1.GetOptions{})
  1348  				if dr != nil {
  1349  					printDestinationRule(writer, initPolicyLevel, dr, podsLabels)
  1350  					matchingSubsets, nonmatchingSubsets = getDestRuleSubsets(dr.Spec.Subsets, podsLabels)
  1351  				} else {
  1352  					fmt.Fprintf(writer,
  1353  						"WARNING: Proxy is stale; it references to non-existent destination rule %s.%s\n",
  1354  						drName, drNamespace)
  1355  				}
  1356  			}
  1357  
  1358  			vsName, vsNamespace, err := getIstioVirtualServiceNameForSvc(&cd, svc, port.Port)
  1359  			if err == nil && vsName != "" && vsNamespace != "" {
  1360  				vs, _ := configClient.NetworkingV1alpha3().VirtualServices(vsNamespace).Get(context.Background(), vsName, metav1.GetOptions{})
  1361  				if vs != nil {
  1362  					printVirtualService(writer, initPolicyLevel, vs, svc, matchingSubsets, nonmatchingSubsets, dr)
  1363  				} else {
  1364  					fmt.Fprintf(writer,
  1365  						"WARNING: Proxy is stale; it references to non-existent virtual service %s.%s\n",
  1366  						vsName, vsNamespace)
  1367  				}
  1368  			}
  1369  
  1370  			policies, err := getIstioRBACPolicies(&cd, port.Port)
  1371  			if err != nil {
  1372  				log.Errorf("error getting rbac policies: %v", err)
  1373  			}
  1374  			if len(policies) > 0 {
  1375  				if len(svc.Spec.Ports) > 1 {
  1376  					// If there is more than one port, prefix each DR by the port it applies to
  1377  					fmt.Fprintf(writer, "%d ", port.Port)
  1378  				}
  1379  
  1380  				fmt.Fprintf(writer, "RBAC policies: %s\n", strings.Join(policies, ", "))
  1381  			}
  1382  		}
  1383  	}
  1384  
  1385  	return nil
  1386  }
  1387  
  1388  func containerReady(pod *corev1.Pod, containerName string) (bool, error) {
  1389  	for _, containerStatus := range pod.Status.ContainerStatuses {
  1390  		if containerStatus.Name == containerName {
  1391  			return containerStatus.Ready, nil
  1392  		}
  1393  	}
  1394  	for _, containerStatus := range pod.Status.InitContainerStatuses {
  1395  		if containerStatus.Name == containerName {
  1396  			return containerStatus.Ready, nil
  1397  		}
  1398  	}
  1399  	return false, fmt.Errorf("no container %q in pod", containerName)
  1400  }
  1401  
  1402  // describePeerAuthentication fetches all PeerAuthentication in workload and root namespace.
  1403  // It lists the ones applied to the pod, and the current active mTLS mode.
  1404  // When the client doesn't have access to root namespace, it will only show workload namespace Peerauthentications.
  1405  func describePeerAuthentication(
  1406  	writer io.Writer,
  1407  	kubeClient kube.CLIClient,
  1408  	configClient istioclient.Interface,
  1409  	workloadNamespace string,
  1410  	podsLabels klabels.Set,
  1411  	istioNamespace string,
  1412  ) error {
  1413  	meshCfg, err := getMeshConfig(kubeClient, istioNamespace)
  1414  	if err != nil {
  1415  		return fmt.Errorf("failed to fetch mesh config: %v", err)
  1416  	}
  1417  
  1418  	workloadPAList, err := configClient.SecurityV1beta1().PeerAuthentications(workloadNamespace).List(context.Background(), metav1.ListOptions{})
  1419  	if err != nil {
  1420  		return fmt.Errorf("failed to fetch workload namespace PeerAuthentication: %v", err)
  1421  	}
  1422  
  1423  	rootPAList, err := configClient.SecurityV1beta1().PeerAuthentications(meshCfg.RootNamespace).List(context.Background(), metav1.ListOptions{})
  1424  	if err != nil {
  1425  		return fmt.Errorf("failed to fetch root namespace PeerAuthentication: %v", err)
  1426  	}
  1427  
  1428  	allPAs := append(rootPAList.Items, workloadPAList.Items...)
  1429  
  1430  	var cfgs []*config.Config
  1431  	for _, pa := range allPAs {
  1432  		pa := pa
  1433  		cfg := crdclient.TranslateObject(pa, config.GroupVersionKind(pa.GroupVersionKind()), "")
  1434  		cfgs = append(cfgs, &cfg)
  1435  	}
  1436  
  1437  	matchedPA := findMatchedConfigs(podsLabels, cfgs)
  1438  	effectivePA := authn.ComposePeerAuthentication(meshCfg.RootNamespace, matchedPA)
  1439  	printPeerAuthentication(writer, effectivePA)
  1440  	if len(matchedPA) != 0 {
  1441  		printConfigs(writer, matchedPA)
  1442  	}
  1443  
  1444  	return nil
  1445  }
  1446  
  1447  // Workloader is used for matching all configs
  1448  type Workloader interface {
  1449  	GetSelector() *typev1beta1.WorkloadSelector
  1450  }
  1451  
  1452  // findMatchedConfigs should filter out unrelated configs that are not matched given podsLabels.
  1453  // When the config has no selector labels, this method will treat it as qualified namespace level
  1454  // config. So configs passed into this method should only contains workload's namespaces configs
  1455  // and rootNamespaces configs, caller should be responsible for controlling configs passed
  1456  // in.
  1457  func findMatchedConfigs(podsLabels klabels.Set, configs []*config.Config) []*config.Config {
  1458  	var cfgs []*config.Config
  1459  
  1460  	for _, cfg := range configs {
  1461  		cfg := cfg
  1462  		labels := cfg.Spec.(Workloader).GetSelector().GetMatchLabels()
  1463  		selector := klabels.SelectorFromSet(labels)
  1464  		if selector.Matches(podsLabels) {
  1465  			cfgs = append(cfgs, cfg)
  1466  		}
  1467  	}
  1468  
  1469  	return cfgs
  1470  }
  1471  
  1472  // printConfigs prints the applied configs based on the member's type.
  1473  // When there is the array is empty, caller should make sure the intended
  1474  // log is handled in their methods.
  1475  func printConfigs(writer io.Writer, configs []*config.Config) {
  1476  	if len(configs) == 0 {
  1477  		return
  1478  	}
  1479  	fmt.Fprintf(writer, "Applied %s:\n", configs[0].Meta.GroupVersionKind.Kind)
  1480  	var cfgNames string
  1481  	for i, cfg := range configs {
  1482  		cfgNames += cfg.Meta.Name + "." + cfg.Meta.Namespace
  1483  		if i < len(configs)-1 {
  1484  			cfgNames += ", "
  1485  		}
  1486  	}
  1487  	fmt.Fprintf(writer, "   %s\n", cfgNames)
  1488  }
  1489  
  1490  func printPeerAuthentication(writer io.Writer, pa authn.MergedPeerAuthentication) {
  1491  	fmt.Fprintf(writer, "Effective PeerAuthentication:\n")
  1492  	fmt.Fprintf(writer, "   Workload mTLS mode: %s\n", pa.Mode.String())
  1493  	if len(pa.PerPort) != 0 {
  1494  		fmt.Fprintf(writer, "   Port Level mTLS mode:\n")
  1495  		for port, mode := range pa.PerPort {
  1496  			fmt.Fprintf(writer, "      %d: %s\n", port, mode.String())
  1497  		}
  1498  	}
  1499  }
  1500  
  1501  func getMeshConfig(kubeClient kube.CLIClient, istioNamespace string) (*meshconfig.MeshConfig, error) {
  1502  	rev := kubeClient.Revision()
  1503  	meshConfigMapName := istioctlutil.DefaultMeshConfigMapName
  1504  
  1505  	// if the revision is not "default", render mesh config map name with revision
  1506  	if rev != "default" && rev != "" {
  1507  		meshConfigMapName = fmt.Sprintf("%s-%s", istioctlutil.DefaultMeshConfigMapName, rev)
  1508  	}
  1509  
  1510  	meshConfigMap, err := kubeClient.Kube().CoreV1().ConfigMaps(istioNamespace).Get(context.TODO(), meshConfigMapName, metav1.GetOptions{})
  1511  	if err != nil {
  1512  		return nil, fmt.Errorf("could not read configmap %q from namespace %q: %v", meshConfigMapName, istioNamespace, err)
  1513  	}
  1514  
  1515  	configYaml, ok := meshConfigMap.Data[istioctlutil.ConfigMapKey]
  1516  	if !ok {
  1517  		return nil, fmt.Errorf("missing config map key %q", istioctlutil.ConfigMapKey)
  1518  	}
  1519  
  1520  	cfg, err := mesh.ApplyMeshConfigDefaults(configYaml)
  1521  	if err != nil {
  1522  		return nil, fmt.Errorf("error parsing mesh config: %v", err)
  1523  	}
  1524  
  1525  	return cfg, nil
  1526  }