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 }