blob: 89022de72ce903f13258e38f803102cd0755e012 [file] [log] [blame]
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"context"
"encoding/json"
"fmt"
"io"
"regexp"
"strconv"
"strings"
)
import (
cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
envoy_api_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
rbac_http_filter "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3"
http_conn "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
"github.com/envoyproxy/go-control-plane/pkg/wellknown"
"github.com/hashicorp/go-multierror"
"github.com/spf13/cobra"
"google.golang.org/protobuf/types/known/structpb"
apiannotation "istio.io/api/annotation"
meshconfig "istio.io/api/mesh/v1alpha1"
"istio.io/api/networking/v1alpha3"
"istio.io/api/security/v1beta1"
typev1beta1 "istio.io/api/type/v1beta1"
clientnetworking "istio.io/client-go/pkg/apis/networking/v1alpha3"
istioclient "istio.io/client-go/pkg/clientset/versioned"
"istio.io/pkg/log"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8s_labels "k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/kubernetes"
)
import (
"github.com/apache/dubbo-go-pixiu/istioctl/pkg/clioptions"
"github.com/apache/dubbo-go-pixiu/istioctl/pkg/tag"
"github.com/apache/dubbo-go-pixiu/istioctl/pkg/util/configdump"
"github.com/apache/dubbo-go-pixiu/istioctl/pkg/util/handlers"
istio_envoy_configdump "github.com/apache/dubbo-go-pixiu/istioctl/pkg/writer/envoy/configdump"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/config/kube/crdclient"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/model"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/networking/util"
authnv1beta1 "github.com/apache/dubbo-go-pixiu/pilot/pkg/security/authn/v1beta1"
pilotcontroller "github.com/apache/dubbo-go-pixiu/pilot/pkg/serviceregistry/kube/controller"
v3 "github.com/apache/dubbo-go-pixiu/pilot/pkg/xds/v3"
"github.com/apache/dubbo-go-pixiu/pkg/config"
"github.com/apache/dubbo-go-pixiu/pkg/config/constants"
"github.com/apache/dubbo-go-pixiu/pkg/config/host"
configKube "github.com/apache/dubbo-go-pixiu/pkg/config/kube"
"github.com/apache/dubbo-go-pixiu/pkg/config/mesh"
"github.com/apache/dubbo-go-pixiu/pkg/kube"
"github.com/apache/dubbo-go-pixiu/pkg/kube/inject"
)
type myProtoValue struct {
*structpb.Value
}
const (
k8sSuffix = ".svc." + constants.DefaultKubernetesDomain
)
var (
// Function that creates Kubernetes client-go; making it a variable lets us mock client-go
interfaceFactory = createInterface
// Ignore unmeshed pods. This makes it easy to suppress warnings about kube-system etc
ignoreUnmeshed = false
)
func podDescribeCmd() *cobra.Command {
var opts clioptions.ControlPlaneOptions
cmd := &cobra.Command{
Use: "pod <pod>",
Aliases: []string{"po"},
Short: "Describe pods and their Istio configuration [kube-only]",
Long: `Analyzes pod, its Services, DestinationRules, and VirtualServices and reports
the configuration objects that affect that pod.`,
Example: ` istioctl experimental describe pod productpage-v1-c7765c886-7zzd4`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf("expecting pod name")
}
podName, ns := handlers.InferPodInfo(args[0], handlers.HandleNamespace(namespace, defaultNamespace))
client, err := interfaceFactory(kubeconfig)
if err != nil {
return err
}
pod, err := client.CoreV1().Pods(ns).Get(context.TODO(), podName, metav1.GetOptions{})
if err != nil {
return err
}
writer := cmd.OutOrStdout()
podLabels := k8s_labels.Set(pod.ObjectMeta.Labels)
annotations := k8s_labels.Set(pod.ObjectMeta.Annotations)
opts.Revision = getRevisionFromPodAnnotation(annotations)
printPod(writer, pod, opts.Revision)
svcs, err := client.CoreV1().Services(ns).List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err
}
matchingServices := make([]v1.Service, 0, len(svcs.Items))
for _, svc := range svcs.Items {
if len(svc.Spec.Selector) > 0 {
svcSelector := k8s_labels.SelectorFromSet(svc.Spec.Selector)
if svcSelector.Matches(podLabels) {
matchingServices = append(matchingServices, svc)
}
}
}
// Validate Istio's "Service association" requirement
if len(matchingServices) == 0 && !ignoreUnmeshed {
fmt.Fprintf(cmd.OutOrStdout(),
"Warning: No Kubernetes Services select pod %s (see https://istio.io/docs/setup/kubernetes/additional-setup/requirements/ )\n", // nolint: lll
kname(pod.ObjectMeta))
}
// TODO look for port collisions between services targeting this pod
kubeClient, err := kubeClientWithRevision(kubeconfig, configContext, opts.Revision)
if err != nil {
return err
}
var configClient istioclient.Interface
if configClient, err = configStoreFactory(); err != nil {
return err
}
podsLabels := []k8s_labels.Set{k8s_labels.Set(pod.ObjectMeta.Labels)}
fmt.Fprintf(writer, "--------------------\n")
err = describePodServices(writer, kubeClient, configClient, pod, matchingServices, podsLabels)
if err != nil {
return err
}
// render PeerAuthentication info
fmt.Fprintf(writer, "--------------------\n")
err = describePeerAuthentication(writer, kubeClient, configClient, ns, k8s_labels.Set(pod.ObjectMeta.Labels))
if err != nil {
return err
}
// TODO find sidecar configs that select this workload and render them
// Now look for ingress gateways
return printIngressInfo(writer, matchingServices, podsLabels, client, configClient, kubeClient)
},
ValidArgsFunction: validPodsNameArgs,
}
cmd.PersistentFlags().BoolVar(&ignoreUnmeshed, "ignoreUnmeshed", false,
"Suppress warnings for unmeshed pods")
cmd.Long += "\n\n" + ExperimentalMsg
return cmd
}
func getRevisionFromPodAnnotation(anno k8s_labels.Set) string {
statusString := anno.Get(apiannotation.SidecarStatus.Name)
var injectionStatus inject.SidecarInjectionStatus
if err := json.Unmarshal([]byte(statusString), &injectionStatus); err != nil {
return ""
}
return injectionStatus.Revision
}
func describe() *cobra.Command {
describeCmd := &cobra.Command{
Use: "describe",
Aliases: []string{"des"},
Short: "Describe resource and related Istio configuration",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 0 {
return fmt.Errorf("unknown resource type %q", args[0])
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
cmd.HelpFunc()(cmd, args)
return nil
},
}
describeCmd.AddCommand(podDescribeCmd())
describeCmd.AddCommand(svcDescribeCmd())
return describeCmd
}
// Append ".svc.cluster.local" if it isn't already present
func extendFQDN(host string) string {
if host[0] == '*' {
return host
}
if strings.HasSuffix(host, k8sSuffix) {
return host
}
return host + k8sSuffix
}
// getDestRuleSubsets gets names of subsets that match any pod labels (also, ones that don't match).
func getDestRuleSubsets(subsets []*v1alpha3.Subset, podsLabels []k8s_labels.Set) ([]string, []string) {
matchingSubsets := make([]string, 0, len(subsets))
nonmatchingSubsets := make([]string, 0, len(subsets))
for _, subset := range subsets {
subsetSelector := k8s_labels.SelectorFromSet(subset.Labels)
if matchesAnyPod(subsetSelector, podsLabels) {
matchingSubsets = append(matchingSubsets, subset.Name)
} else {
nonmatchingSubsets = append(nonmatchingSubsets, subset.Name)
}
}
return matchingSubsets, nonmatchingSubsets
}
func matchesAnyPod(subsetSelector k8s_labels.Selector, podsLabels []k8s_labels.Set) bool {
for _, podLabels := range podsLabels {
if subsetSelector.Matches(podLabels) {
return true
}
}
return false
}
func printDestinationRule(writer io.Writer, dr *clientnetworking.DestinationRule, podsLabels []k8s_labels.Set) {
fmt.Fprintf(writer, "DestinationRule: %s for %q\n", kname(dr.ObjectMeta), dr.Spec.Host)
matchingSubsets, nonmatchingSubsets := getDestRuleSubsets(dr.Spec.Subsets, podsLabels)
if len(matchingSubsets) != 0 || len(nonmatchingSubsets) != 0 {
if len(matchingSubsets) == 0 {
fmt.Fprintf(writer, " WARNING POD DOES NOT MATCH ANY SUBSETS. (Non matching subsets %s)\n",
strings.Join(nonmatchingSubsets, ","))
}
fmt.Fprintf(writer, " Matching subsets: %s\n", strings.Join(matchingSubsets, ","))
if len(nonmatchingSubsets) > 0 {
fmt.Fprintf(writer, " (Non-matching subsets %s)\n", strings.Join(nonmatchingSubsets, ","))
}
}
// Ignore LoadBalancer, ConnectionPool, OutlierDetection, and PortLevelSettings
trafficPolicy := dr.Spec.TrafficPolicy
if trafficPolicy == nil {
fmt.Fprintf(writer, " No Traffic Policy\n")
} else {
if trafficPolicy.Tls != nil {
fmt.Fprintf(writer, " Traffic Policy TLS Mode: %s\n", dr.Spec.TrafficPolicy.Tls.Mode.String())
}
extra := []string{}
if trafficPolicy.LoadBalancer != nil {
extra = append(extra, "load balancer")
}
if trafficPolicy.ConnectionPool != nil {
extra = append(extra, "connection pool")
}
if trafficPolicy.OutlierDetection != nil {
extra = append(extra, "outlier detection")
}
if trafficPolicy.PortLevelSettings != nil {
extra = append(extra, "port level settings")
}
if len(extra) > 0 {
fmt.Fprintf(writer, " %s\n", strings.Join(extra, "/"))
}
}
}
// httpRouteMatchSvc returns true if it matches and a slice of facts about the match
func httpRouteMatchSvc(vs *clientnetworking.VirtualService, route *v1alpha3.HTTPRoute, svc v1.Service, matchingSubsets []string, nonmatchingSubsets []string, dr *clientnetworking.DestinationRule) (bool, []string) { // nolint: lll
svcHost := extendFQDN(fmt.Sprintf("%s.%s", svc.ObjectMeta.Name, svc.ObjectMeta.Namespace))
facts := []string{}
mismatchNotes := []string{}
match := false
for _, dest := range route.Route {
fqdn := string(model.ResolveShortnameToFQDN(dest.Destination.Host, config.Meta{Namespace: vs.Namespace}))
if extendFQDN(fqdn) == svcHost {
if dest.Destination.Subset != "" {
if contains(nonmatchingSubsets, dest.Destination.Subset) {
mismatchNotes = append(mismatchNotes, fmt.Sprintf("Route to non-matching subset %s for (%s)",
dest.Destination.Subset,
renderMatches(route.Match)))
continue
}
if !contains(matchingSubsets, dest.Destination.Subset) {
if dr == nil {
// Don't bother giving the match conditions, the problem is that there are unknowns in the VirtualService
mismatchNotes = append(mismatchNotes, fmt.Sprintf("Warning: Route to subset %s but NO DESTINATION RULE defining subsets!", dest.Destination.Subset))
} else {
// Don't bother giving the match conditions, the problem is that there are unknowns in the VirtualService
mismatchNotes = append(mismatchNotes,
fmt.Sprintf("Warning: Route to UNKNOWN subset %s; check DestinationRule %s", dest.Destination.Subset, kname(dr.ObjectMeta)))
}
continue
}
}
match = true
if dest.Weight > 0 {
facts = append(facts, fmt.Sprintf("Weight %d%%", dest.Weight))
}
// Consider adding RemoveResponseHeaders, AppendResponseHeaders, RemoveRequestHeaders, AppendRequestHeaders
} else {
mismatchNotes = append(mismatchNotes, fmt.Sprintf("Route to %s", dest.Destination.Host))
}
}
if match {
reqMatchFacts := []string{}
if route.Fault != nil {
reqMatchFacts = append(reqMatchFacts, fmt.Sprintf("Fault injection %s", route.Fault.String()))
}
// TODO Consider adding Headers, SourceLabels
for _, trafficMatch := range route.Match {
reqMatchFacts = append(reqMatchFacts, renderMatch(trafficMatch))
}
if len(reqMatchFacts) > 0 {
facts = append(facts, strings.Join(reqMatchFacts, ", "))
}
}
if !match && len(mismatchNotes) > 0 {
facts = append(facts, mismatchNotes...)
}
return match, facts
}
func tcpRouteMatchSvc(vs *clientnetworking.VirtualService, route *v1alpha3.TCPRoute, svc v1.Service) (bool, []string) {
match := false
facts := []string{}
svcHost := extendFQDN(fmt.Sprintf("%s.%s", svc.ObjectMeta.Name, svc.ObjectMeta.Namespace))
for _, dest := range route.Route {
fqdn := string(model.ResolveShortnameToFQDN(dest.Destination.Host, config.Meta{Namespace: vs.Namespace}))
if extendFQDN(fqdn) == svcHost {
match = true
}
}
if match {
for _, trafficMatch := range route.Match {
facts = append(facts, trafficMatch.String())
}
}
return match, facts
}
func renderStringMatch(sm *v1alpha3.StringMatch) string {
if sm == nil {
return ""
}
switch x := sm.MatchType.(type) {
case *v1alpha3.StringMatch_Exact:
return x.Exact
case *v1alpha3.StringMatch_Prefix:
return x.Prefix + "*"
}
return sm.String()
}
func renderMatches(trafficMatches []*v1alpha3.HTTPMatchRequest) string {
if len(trafficMatches) == 0 {
return "everything"
}
matches := []string{}
for _, trafficMatch := range trafficMatches {
matches = append(matches, renderMatch(trafficMatch))
}
return strings.Join(matches, ", ")
}
func renderMatch(match *v1alpha3.HTTPMatchRequest) string {
retval := ""
// TODO Are users interested in seeing Scheme, Method, Authority?
if match.Uri != nil {
retval += renderStringMatch(match.Uri)
if match.IgnoreUriCase {
retval += " uncased"
}
}
if len(match.Headers) > 0 {
headerConds := []string{}
for key, val := range match.Headers {
headerConds = append(headerConds, fmt.Sprintf("%s=%s", key, renderStringMatch(val)))
}
retval += " when headers are " + strings.Join(headerConds, "; ")
}
// TODO QueryParams, maybe Gateways
return strings.TrimSpace(retval)
}
func printPod(writer io.Writer, pod *v1.Pod, revision string) {
ports := []string{}
UserID := int64(1337)
for _, container := range pod.Spec.Containers {
for _, port := range container.Ports {
var protocol string
// Suppress /<protocol> for TCP, print it for everything else
if port.Protocol != "TCP" {
protocol = fmt.Sprintf("/%s", port.Protocol)
}
ports = append(ports, fmt.Sprintf("%d%s (%s)", port.ContainerPort, protocol, container.Name))
}
// Ref: https://istio.io/latest/docs/ops/deployment/requirements/#pod-requirements
if container.Name != "istio-proxy" && container.Name != "istio-operator" {
if container.SecurityContext != nil && container.SecurityContext.RunAsUser != nil {
if *container.SecurityContext.RunAsUser == UserID {
fmt.Fprintf(writer, "WARNING: User ID (UID) 1337 is reserved for the sidecar proxy.\n")
}
}
}
}
fmt.Fprintf(writer, "Pod: %s\n", kname(pod.ObjectMeta))
fmt.Fprintf(writer, " Pod Revision: %s\n", revision)
if len(ports) > 0 {
fmt.Fprintf(writer, " Pod Ports: %s\n", strings.Join(ports, ", "))
} else {
fmt.Fprintf(writer, " Pod does not expose ports\n")
}
if pod.Status.Phase != v1.PodRunning {
fmt.Printf(" Pod is not %s (%s)\n", v1.PodRunning, pod.Status.Phase)
return
}
for _, containerStatus := range pod.Status.ContainerStatuses {
if !containerStatus.Ready {
fmt.Fprintf(writer, "WARNING: Pod %s Container %s NOT READY\n", kname(pod.ObjectMeta), containerStatus.Name)
}
}
if ignoreUnmeshed {
return
}
if !isMeshed(pod) {
fmt.Fprintf(writer, "WARNING: %s is not part of mesh; no Istio sidecar\n", kname(pod.ObjectMeta))
return
}
// Ref: https://istio.io/latest/docs/ops/deployment/requirements/#pod-requirements
if pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.RunAsUser != nil {
if *pod.Spec.SecurityContext.RunAsUser == UserID {
fmt.Fprintf(writer, " WARNING: User ID (UID) 1337 is reserved for the sidecar proxy.\n")
}
}
// https://istio.io/docs/setup/kubernetes/additional-setup/requirements/
// says "We recommend adding an explicit app label and version label to deployments."
app, ok := pod.ObjectMeta.Labels["app"]
if !ok || app == "" {
fmt.Fprintf(writer, "Suggestion: add 'app' label to pod for Istio telemetry.\n")
}
version, ok := pod.ObjectMeta.Labels["version"]
if !ok || version == "" {
fmt.Fprintf(writer, "Suggestion: add 'version' label to pod for Istio telemetry.\n")
}
}
func kname(meta metav1.ObjectMeta) string {
ns := handlers.HandleNamespace(namespace, defaultNamespace)
if meta.Namespace == ns {
return meta.Name
}
// Use the Istio convention pod-name[.namespace]
return fmt.Sprintf("%s.%s", meta.Name, meta.Namespace)
}
func printService(writer io.Writer, svc v1.Service, pod *v1.Pod) {
fmt.Fprintf(writer, "Service: %s\n", kname(svc.ObjectMeta))
for _, port := range svc.Spec.Ports {
if port.Protocol != "TCP" {
// Ignore UDP ports, which are not supported by Istio
continue
}
// Get port number
nport, err := pilotcontroller.FindPort(pod, &port)
if err == nil {
protocol := findProtocolForPort(&port)
fmt.Fprintf(writer, " Port: %s %d/%s targets pod port %d\n", port.Name, port.Port, protocol, nport)
} else {
fmt.Fprintf(writer, " %s\n", err.Error())
}
}
}
func findProtocolForPort(port *v1.ServicePort) string {
var protocol string
if port.Name == "" && port.AppProtocol == nil && port.Protocol != v1.ProtocolUDP {
protocol = "auto-detect"
} else {
protocol = string(configKube.ConvertProtocol(port.Port, port.Name, port.Protocol, port.AppProtocol))
}
return protocol
}
func contains(slice []string, s string) bool {
for _, candidate := range slice {
if candidate == s {
return true
}
}
return false
}
func isMeshed(pod *v1.Pod) bool {
var sidecar bool
for _, container := range pod.Spec.Containers {
sidecar = sidecar || (container.Name == inject.ProxyContainerName)
}
return sidecar
}
// Extract value of key out of Struct, but always return a Struct, even if the value isn't one
func (v *myProtoValue) keyAsStruct(key string) *myProtoValue {
if v == nil || v.GetStructValue() == nil {
return asMyProtoValue(&structpb.Struct{Fields: make(map[string]*structpb.Value)})
}
return &myProtoValue{v.GetStructValue().Fields[key]}
}
// asMyProtoValue wraps a protobuf Struct so we may use it with keyAsStruct and keyAsString
func asMyProtoValue(s *structpb.Struct) *myProtoValue {
return &myProtoValue{
&structpb.Value{
Kind: &structpb.Value_StructValue{
StructValue: s,
},
},
}
}
func (v *myProtoValue) keyAsString(key string) string {
s := v.keyAsStruct(key)
return s.GetStringValue()
}
func getIstioRBACPolicies(cd *configdump.Wrapper, port int32) ([]string, error) {
hcm, err := getInboundHTTPConnectionManager(cd, port)
if err != nil || hcm == nil {
return []string{}, err
}
// Identify RBAC policies. Currently there are no "breadcrumbs" so we only return the policy names.
for _, httpFilter := range hcm.HttpFilters {
if httpFilter.Name == wellknown.HTTPRoleBasedAccessControl {
rbac := &rbac_http_filter.RBAC{}
if err := httpFilter.GetTypedConfig().UnmarshalTo(rbac); err == nil {
policies := []string{}
for polName := range rbac.Rules.Policies {
policies = append(policies, polName)
}
return policies, nil
}
}
}
return []string{}, nil
}
// Return the first HTTP Connection Manager config for the inbound port
func getInboundHTTPConnectionManager(cd *configdump.Wrapper, port int32) (*http_conn.HttpConnectionManager, error) {
filter := istio_envoy_configdump.ListenerFilter{
Port: uint32(port),
}
listeners, err := cd.GetListenerConfigDump()
if err != nil {
return nil, err
}
for _, l := range listeners.DynamicListeners {
if l.ActiveState == nil {
continue
}
// Support v2 or v3 in config dump. See ads.go:RequestedTypes for more info.
l.ActiveState.Listener.TypeUrl = v3.ListenerType
listenerTyped := &listener.Listener{}
err = l.ActiveState.Listener.UnmarshalTo(listenerTyped)
if err != nil {
return nil, err
}
if listenerTyped.Name == model.VirtualInboundListenerName {
for _, filterChain := range listenerTyped.FilterChains {
for _, filter := range filterChain.Filters {
hcm := &http_conn.HttpConnectionManager{}
if err := filter.GetTypedConfig().UnmarshalTo(hcm); err == nil {
return hcm, nil
}
}
}
}
// This next check is deprecated in 1.6 and can be removed when we remove
// the old config_dumps in support of https://github.com/istio/istio/issues/23042
if filter.Verify(listenerTyped) {
sockAddr := listenerTyped.Address.GetSocketAddress()
if sockAddr != nil {
// Skip outbound listeners
if sockAddr.Address == "0.0.0.0" {
continue
}
}
for _, filterChain := range listenerTyped.FilterChains {
for _, filter := range filterChain.Filters {
hcm := &http_conn.HttpConnectionManager{}
if err := filter.GetTypedConfig().UnmarshalTo(hcm); err == nil {
return hcm, nil
}
}
}
}
}
return nil, nil
}
// getIstioConfigNameForSvc returns name, namespace
func getIstioVirtualServiceNameForSvc(cd *configdump.Wrapper, svc v1.Service, port int32) (string, string, error) {
path, err := getIstioVirtualServicePathForSvcFromRoute(cd, svc, port)
if err != nil {
return "", "", err
}
// Starting with recent 1.5.0 builds, the path will include .istio.io. Handle both.
// nolint: gosimple
re := regexp.MustCompile("/apis/networking(\\.istio\\.io)?/v1alpha3/namespaces/(?P<namespace>[^/]+)/virtual-service/(?P<name>[^/]+)")
ss := re.FindStringSubmatch(path)
if ss == nil {
return "", "", fmt.Errorf("not a VS path: %s", path)
}
return ss[3], ss[2], nil
}
// getIstioVirtualServicePathForSvcFromRoute returns something like "/apis/networking/v1alpha3/namespaces/default/virtual-service/reviews"
func getIstioVirtualServicePathForSvcFromRoute(cd *configdump.Wrapper, svc v1.Service, port int32) (string, error) {
sPort := strconv.Itoa(int(port))
// Routes know their destination Service name, namespace, and port, and the DR that configures them
rcd, err := cd.GetDynamicRouteDump(false)
if err != nil {
return "", err
}
for _, rcd := range rcd.DynamicRouteConfigs {
routeTyped := &route.RouteConfiguration{}
err = rcd.RouteConfig.UnmarshalTo(routeTyped)
if err != nil {
return "", err
}
if routeTyped.Name != sPort && !strings.HasPrefix(routeTyped.Name, "http.") &&
!strings.HasPrefix(routeTyped.Name, "https.") {
continue
}
for _, vh := range routeTyped.VirtualHosts {
for _, route := range vh.Routes {
if routeDestinationMatchesSvc(route, svc, vh, port) {
return getIstioConfig(route.Metadata)
}
}
}
}
return "", nil
}
// routeDestinationMatchesSvc determines whether or not to use this service as a destination
func routeDestinationMatchesSvc(vhRoute *route.Route, svc v1.Service, vh *route.VirtualHost, port int32) bool {
if vhRoute == nil {
return false
}
// Infer from VirtualHost domains matching <service>.<namespace>.svc.cluster.local
re := regexp.MustCompile(`(?P<service>[^\.]+)\.(?P<namespace>[^\.]+)\.svc\.cluster\.local$`)
for _, domain := range vh.Domains {
ss := re.FindStringSubmatch(domain)
if ss != nil {
if ss[1] == svc.ObjectMeta.Name && ss[2] == svc.ObjectMeta.Namespace {
return true
}
}
}
clusterName := ""
switch cs := vhRoute.GetRoute().GetClusterSpecifier().(type) {
case *route.RouteAction_Cluster:
clusterName = cs.Cluster
case *route.RouteAction_WeightedClusters:
clusterName = cs.WeightedClusters.Clusters[0].GetName()
}
// If this is an ingress gateway, the Domains will be something like *:80, so check routes
// which will look like "outbound|9080||productpage.default.svc.cluster.local"
res := fmt.Sprintf(`outbound\|%d\|[^\|]*\|(?P<service>[^\.]+)\.(?P<namespace>[^\.]+)\.svc\.cluster\.local$`, port)
re = regexp.MustCompile(res)
ss := re.FindStringSubmatch(clusterName)
if ss != nil {
if ss[1] == svc.ObjectMeta.Name && ss[2] == svc.ObjectMeta.Namespace {
return true
}
}
return false
}
// getIstioConfig returns .metadata.filter_metadata.istio.config, err
func getIstioConfig(metadata *envoy_api_core.Metadata) (string, error) {
if metadata != nil {
istioConfig := asMyProtoValue(metadata.FilterMetadata[util.IstioMetadataKey]).
keyAsString("config")
return istioConfig, nil
}
return "", fmt.Errorf("no istio config")
}
// getIstioConfigNameForSvc returns name, namespace
func getIstioDestinationRuleNameForSvc(cd *configdump.Wrapper, svc v1.Service, port int32) (string, string, error) {
path, err := getIstioDestinationRulePathForSvc(cd, svc, port)
if err != nil || path == "" {
return "", "", err
}
// Starting with recent 1.5.0 builds, the path will include .istio.io. Handle both.
// nolint: gosimple
re := regexp.MustCompile("/apis/networking(\\.istio\\.io)?/v1alpha3/namespaces/(?P<namespace>[^/]+)/destination-rule/(?P<name>[^/]+)")
ss := re.FindStringSubmatch(path)
if ss == nil {
return "", "", fmt.Errorf("not a DR path: %s", path)
}
return ss[3], ss[2], nil
}
// getIstioDestinationRulePathForSvc returns something like "/apis/networking/v1alpha3/namespaces/default/destination-rule/reviews"
func getIstioDestinationRulePathForSvc(cd *configdump.Wrapper, svc v1.Service, port int32) (string, error) {
svcHost := extendFQDN(fmt.Sprintf("%s.%s", svc.ObjectMeta.Name, svc.ObjectMeta.Namespace))
filter := istio_envoy_configdump.ClusterFilter{
FQDN: host.Name(svcHost),
Port: int(port),
// Although we want inbound traffic, ask for outbound traffic, as the DR is
// not associated with the inbound traffic.
Direction: model.TrafficDirectionOutbound,
}
dump, err := cd.GetClusterConfigDump()
if err != nil {
return "", err
}
for _, dac := range dump.DynamicActiveClusters {
clusterTyped := &cluster.Cluster{}
// Support v2 or v3 in config dump. See ads.go:RequestedTypes for more info.
dac.Cluster.TypeUrl = v3.ClusterType
err = dac.Cluster.UnmarshalTo(clusterTyped)
if err != nil {
return "", err
}
if filter.Verify(clusterTyped) {
metadata := clusterTyped.Metadata
if metadata != nil {
istioConfig := asMyProtoValue(metadata.FilterMetadata[util.IstioMetadataKey]).
keyAsString("config")
return istioConfig, nil
}
}
}
return "", nil
}
// TODO simplify this by showing for each matching Destination the negation of the previous HttpMatchRequest
// and showing the non-matching Destinations. (The current code is ad-hoc, and usually shows most of that information.)
func printVirtualService(writer io.Writer, vs *clientnetworking.VirtualService, svc v1.Service, matchingSubsets []string, nonmatchingSubsets []string, dr *clientnetworking.DestinationRule) { // nolint: lll
fmt.Fprintf(writer, "VirtualService: %s\n", kname(vs.ObjectMeta))
// There is no point in checking that 'port' uses HTTP (for HTTP route matches)
// or uses TCP (for TCP route matches) because if the port has the wrong name
// the VirtualService metadata will not appear.
matches := 0
facts := 0
mismatchNotes := []string{}
for _, httpRoute := range vs.Spec.Http {
routeMatch, newfacts := httpRouteMatchSvc(vs, httpRoute, svc, matchingSubsets, nonmatchingSubsets, dr)
if routeMatch {
matches++
for _, newfact := range newfacts {
fmt.Fprintf(writer, " %s\n", newfact)
facts++
}
} else {
mismatchNotes = append(mismatchNotes, newfacts...)
}
}
// TODO vsSpec.Tls if I can find examples in the wild
for _, tcpRoute := range vs.Spec.Tcp {
routeMatch, newfacts := tcpRouteMatchSvc(vs, tcpRoute, svc)
if routeMatch {
matches++
for _, newfact := range newfacts {
fmt.Fprintf(writer, " %s\n", newfact)
facts++
}
} else {
mismatchNotes = append(mismatchNotes, newfacts...)
}
}
if matches == 0 {
if len(vs.Spec.Http) > 0 {
fmt.Fprintf(writer, " WARNING: No destinations match pod subsets (checked %d HTTP routes)\n", len(vs.Spec.Http))
}
if len(vs.Spec.Tcp) > 0 {
fmt.Fprintf(writer, " WARNING: No destinations match pod subsets (checked %d TCP routes)\n", len(vs.Spec.Tcp))
}
for _, mismatch := range mismatchNotes {
fmt.Fprintf(writer, " %s\n", mismatch)
}
return
}
possibleDests := len(vs.Spec.Http) + len(vs.Spec.Tls) + len(vs.Spec.Tcp)
if matches < possibleDests {
// We've printed the match conditions. We can't say for sure that matching
// traffic will reach this pod, because an earlier match condition could have captured it.
fmt.Fprintf(writer, " %d additional destination(s) that will not reach this pod\n", possibleDests-matches)
// If we matched, but printed nothing, treat this as the catch-all
if facts == 0 {
for _, mismatch := range mismatchNotes {
fmt.Fprintf(writer, " %s\n", mismatch)
}
}
return
}
if facts == 0 {
// We printed nothing other than the name. Print something.
if len(vs.Spec.Http) > 0 {
fmt.Fprintf(writer, " %d HTTP route(s)\n", len(vs.Spec.Http))
}
if len(vs.Spec.Tcp) > 0 {
fmt.Fprintf(writer, " %d TCP route(s)\n", len(vs.Spec.Tcp))
}
}
}
func printIngressInfo(writer io.Writer, matchingServices []v1.Service, podsLabels []k8s_labels.Set, kubeClient kubernetes.Interface, configClient istioclient.Interface, client kube.ExtendedClient) error { // nolint: lll
pods, err := kubeClient.CoreV1().Pods(istioNamespace).List(context.TODO(), metav1.ListOptions{
LabelSelector: "istio=ingressgateway",
FieldSelector: "status.phase=Running",
})
if err != nil {
return multierror.Prefix(err, "Could not find ingress gateway pods")
}
if len(pods.Items) == 0 {
fmt.Fprintf(writer, "Skipping Gateway information (no ingress gateway pods)\n")
return nil
}
pod := pods.Items[0]
// Currently no support for non-standard gateways selecting non ingressgateway pods
ingressSvcs, err := kubeClient.CoreV1().Services(istioNamespace).List(context.TODO(), metav1.ListOptions{
LabelSelector: "istio=ingressgateway",
})
if err != nil {
return multierror.Prefix(err, "Could not find ingress gateway service")
}
if len(ingressSvcs.Items) == 0 {
return fmt.Errorf("no ingress gateway service")
}
byConfigDump, err := client.EnvoyDo(context.TODO(), pod.Name, pod.Namespace, "GET", "config_dump")
if err != nil {
return fmt.Errorf("failed to execute command on ingress gateway sidecar: %v", err)
}
cd := configdump.Wrapper{}
err = cd.UnmarshalJSON(byConfigDump)
if err != nil {
return fmt.Errorf("can't parse ingress gateway sidecar config_dump: %v", err)
}
ipIngress := getIngressIP(ingressSvcs.Items[0], pod)
for row, svc := range matchingServices {
for _, port := range svc.Spec.Ports {
matchingSubsets := []string{}
nonmatchingSubsets := []string{}
drName, drNamespace, err := getIstioDestinationRuleNameForSvc(&cd, svc, port.Port)
var dr *clientnetworking.DestinationRule
if err == nil && drName != "" && drNamespace != "" {
dr, _ = configClient.NetworkingV1alpha3().DestinationRules(drNamespace).Get(context.Background(), drName, metav1.GetOptions{})
if dr != nil {
matchingSubsets, nonmatchingSubsets = getDestRuleSubsets(dr.Spec.Subsets, podsLabels)
} else {
fmt.Fprintf(writer,
"WARNING: Proxy is stale; it references to non-existent destination rule %s.%s\n",
drName, drNamespace)
}
}
vsName, vsNamespace, err := getIstioVirtualServiceNameForSvc(&cd, svc, port.Port)
if err == nil && vsName != "" && vsNamespace != "" {
vs, _ := configClient.NetworkingV1alpha3().VirtualServices(vsNamespace).Get(context.Background(), vsName, metav1.GetOptions{})
if vs != nil {
if row == 0 {
fmt.Fprintf(writer, "\n")
} else {
fmt.Fprintf(writer, "--------------------\n")
}
printIngressService(writer, &ingressSvcs.Items[0], &pod, ipIngress)
printVirtualService(writer, vs, svc, matchingSubsets, nonmatchingSubsets, dr)
} else {
fmt.Fprintf(writer,
"WARNING: Proxy is stale; it references to non-existent virtual service %s.%s\n",
vsName, vsNamespace)
}
}
}
}
return nil
}
func printIngressService(writer io.Writer, ingressSvc *v1.Service, ingressPod *v1.Pod, ip string) {
// The ingressgateway service offers a lot of ports but the pod doesn't listen to all
// of them. For example, it doesn't listen on 443 without additional setup. This prints
// the most basic output.
portsToShow := map[string]bool{
"http2": true,
}
protocolToScheme := map[string]string{
"HTTP2": "http",
}
schemePortDefault := map[string]int{
"http": 80,
}
for _, port := range ingressSvc.Spec.Ports {
if port.Protocol != "TCP" || !portsToShow[port.Name] {
continue
}
// Get port number
_, err := pilotcontroller.FindPort(ingressPod, &port)
if err == nil {
nport := int(port.Port)
protocol := string(configKube.ConvertProtocol(port.Port, port.Name, port.Protocol, port.AppProtocol))
scheme := protocolToScheme[protocol]
portSuffix := ""
if schemePortDefault[scheme] != nport {
portSuffix = fmt.Sprintf(":%d", nport)
}
fmt.Fprintf(writer, "\nExposed on Ingress Gateway %s://%s%s\n", scheme, ip, portSuffix)
}
}
}
func getIngressIP(service v1.Service, pod v1.Pod) string {
if len(service.Status.LoadBalancer.Ingress) > 0 {
return service.Status.LoadBalancer.Ingress[0].IP
}
if pod.Status.HostIP != "" {
return pod.Status.HostIP
}
// The scope of this function is to get the IP from Kubernetes, we do not
// ask Docker or minikube for an IP.
// See https://istio.io/docs/tasks/traffic-management/ingress/ingress-control/#determining-the-ingress-ip-and-ports
return "unknown"
}
func svcDescribeCmd() *cobra.Command {
var opts clioptions.ControlPlaneOptions
cmd := &cobra.Command{
Use: "service <svc>",
Aliases: []string{"svc"},
Short: "Describe services and their Istio configuration [kube-only]",
Long: `Analyzes service, pods, DestinationRules, and VirtualServices and reports
the configuration objects that affect that service.`,
Example: ` istioctl experimental describe service productpage`,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
cmd.Println(cmd.UsageString())
return fmt.Errorf("expecting service name")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
svcName, ns := handlers.InferPodInfo(args[0], handlers.HandleNamespace(namespace, defaultNamespace))
client, err := interfaceFactory(kubeconfig)
if err != nil {
return err
}
svc, err := client.CoreV1().Services(ns).Get(context.TODO(), svcName, metav1.GetOptions{})
if err != nil {
return err
}
writer := cmd.OutOrStdout()
pods, err := client.CoreV1().Pods(ns).List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err
}
matchingPods := []v1.Pod{}
selectedPodCount := 0
if len(svc.Spec.Selector) > 0 {
svcSelector := k8s_labels.SelectorFromSet(svc.Spec.Selector)
for _, pod := range pods.Items {
if svcSelector.Matches(k8s_labels.Set(pod.ObjectMeta.Labels)) {
selectedPodCount++
if pod.Status.Phase != v1.PodRunning {
fmt.Printf(" Pod is not %s (%s)\n", v1.PodRunning, pod.Status.Phase)
continue
}
ready, err := containerReady(&pod, proxyContainerName)
if err != nil {
fmt.Fprintf(writer, "Pod %s: %s\n", kname(pod.ObjectMeta), err)
continue
}
if !ready {
fmt.Fprintf(writer, "WARNING: Pod %s Container %s NOT READY\n", kname(pod.ObjectMeta), proxyContainerName)
continue
}
matchingPods = append(matchingPods, pod)
}
}
}
if len(matchingPods) == 0 {
if selectedPodCount == 0 {
fmt.Fprintf(writer, "Service %q has no pods.\n", kname(svc.ObjectMeta))
return nil
}
fmt.Fprintf(writer, "Service %q has no Istio pods. (%d pods in service).\n", kname(svc.ObjectMeta), selectedPodCount)
fmt.Fprintf(writer, "Use `istioctl experimental add-to-mesh`, `istioctl kube-inject`, or redeploy with Istio automatic sidecar injection.\n")
return nil
}
kubeClient, err := kubeClientWithRevision(kubeconfig, configContext, opts.Revision)
if err != nil {
return err
}
var configClient istioclient.Interface
if configClient, err = configStoreFactory(); err != nil {
return err
}
// Get all the labels for all the matching pods. We will used this to complain
// if NONE of the pods match a VirtualService
podsLabels := make([]k8s_labels.Set, len(matchingPods))
for i, pod := range matchingPods {
podsLabels[i] = k8s_labels.Set(pod.ObjectMeta.Labels)
}
// Describe based on the Envoy config for this first pod only
pod := matchingPods[0]
// Only consider the service invoked with this command, not other services that might select the pod
svcs := []v1.Service{*svc}
err = describePodServices(writer, kubeClient, configClient, &pod, svcs, podsLabels)
if err != nil {
return err
}
// Now look for ingress gateways
return printIngressInfo(writer, svcs, podsLabels, client, configClient, kubeClient)
},
ValidArgsFunction: validServiceArgs,
}
cmd.PersistentFlags().BoolVar(&ignoreUnmeshed, "ignoreUnmeshed", false,
"Suppress warnings for unmeshed pods")
cmd.Long += "\n\n" + ExperimentalMsg
return cmd
}
func describePodServices(writer io.Writer, kubeClient kube.ExtendedClient, configClient istioclient.Interface, pod *v1.Pod, matchingServices []v1.Service, podsLabels []k8s_labels.Set) error { // nolint: lll
byConfigDump, err := kubeClient.EnvoyDo(context.TODO(), pod.ObjectMeta.Name, pod.ObjectMeta.Namespace, "GET", "config_dump")
if err != nil {
if ignoreUnmeshed {
return nil
}
return fmt.Errorf("failed to execute command on sidecar: %v", err)
}
cd := configdump.Wrapper{}
err = cd.UnmarshalJSON(byConfigDump)
if err != nil {
return fmt.Errorf("can't parse sidecar config_dump for %v: %v", err, pod.ObjectMeta.Name)
}
for row, svc := range matchingServices {
if row != 0 {
fmt.Fprintf(writer, "--------------------\n")
}
printService(writer, svc, pod)
for _, port := range svc.Spec.Ports {
matchingSubsets := []string{}
nonmatchingSubsets := []string{}
drName, drNamespace, err := getIstioDestinationRuleNameForSvc(&cd, svc, port.Port)
if err != nil {
log.Errorf("fetch destination rule for %v: %v", svc.Name, err)
}
var dr *clientnetworking.DestinationRule
if err == nil && drName != "" && drNamespace != "" {
dr, _ = configClient.NetworkingV1alpha3().DestinationRules(drNamespace).Get(context.Background(), drName, metav1.GetOptions{})
if dr != nil {
if len(svc.Spec.Ports) > 1 {
// If there is more than one port, prefix each DR by the port it applies to
fmt.Fprintf(writer, "%d ", port.Port)
}
printDestinationRule(writer, dr, podsLabels)
matchingSubsets, nonmatchingSubsets = getDestRuleSubsets(dr.Spec.Subsets, podsLabels)
} else {
fmt.Fprintf(writer,
"WARNING: Proxy is stale; it references to non-existent destination rule %s.%s\n",
drName, drNamespace)
}
}
vsName, vsNamespace, err := getIstioVirtualServiceNameForSvc(&cd, svc, port.Port)
if err == nil && vsName != "" && vsNamespace != "" {
vs, _ := configClient.NetworkingV1alpha3().VirtualServices(vsNamespace).Get(context.Background(), vsName, metav1.GetOptions{})
if vs != nil {
if len(svc.Spec.Ports) > 1 {
// If there is more than one port, prefix each DR by the port it applies to
fmt.Fprintf(writer, "%d ", port.Port)
}
printVirtualService(writer, vs, svc, matchingSubsets, nonmatchingSubsets, dr)
} else {
fmt.Fprintf(writer,
"WARNING: Proxy is stale; it references to non-existent virtual service %s.%s\n",
vsName, vsNamespace)
}
}
policies, err := getIstioRBACPolicies(&cd, port.Port)
if err != nil {
log.Errorf("error getting rbac policies: %v", err)
}
if len(policies) > 0 {
if len(svc.Spec.Ports) > 1 {
// If there is more than one port, prefix each DR by the port it applies to
fmt.Fprintf(writer, "%d ", port.Port)
}
fmt.Fprintf(writer, "RBAC policies: %s\n", strings.Join(policies, ", "))
}
}
}
return nil
}
func containerReady(pod *v1.Pod, containerName string) (bool, error) {
for _, containerStatus := range pod.Status.ContainerStatuses {
if containerStatus.Name == containerName {
return containerStatus.Ready, nil
}
}
return false, fmt.Errorf("no container %q in pod", containerName)
}
// describePeerAuthentication fetches all PeerAuthentication in workload and root namespace.
// It lists the ones applied to the pod, and the current active mTLS mode.
// When the client doesn't have access to root namespace, it will only show workload namespace Peerauthentications.
func describePeerAuthentication(writer io.Writer, kubeClient kube.ExtendedClient, configClient istioclient.Interface, workloadNamespace string, podsLabels k8s_labels.Set) error { // nolint: lll
meshCfg, err := getMeshConfig(kubeClient)
if err != nil {
return fmt.Errorf("failed to fetch mesh config: %v", err)
}
workloadPAList, err := configClient.SecurityV1beta1().PeerAuthentications(workloadNamespace).List(context.Background(), metav1.ListOptions{})
if err != nil {
return fmt.Errorf("failed to fetch workload namespace PeerAuthentication: %v", err)
}
rootPAList, err := configClient.SecurityV1beta1().PeerAuthentications(meshCfg.RootNamespace).List(context.Background(), metav1.ListOptions{})
if err != nil {
return fmt.Errorf("failed to fetch root namespace PeerAuthentication: %v", err)
}
allPAs := append(rootPAList.Items, workloadPAList.Items...)
var cfgs []*config.Config
for _, pa := range allPAs {
pa := pa
cfg := crdclient.TranslateObject(pa, config.GroupVersionKind(pa.GroupVersionKind()), "")
cfgs = append(cfgs, &cfg)
}
matchedPA := findMatchedConfigs(podsLabels, cfgs)
effectivePA := authnv1beta1.ComposePeerAuthentication(meshCfg.RootNamespace, matchedPA)
printPeerAuthentication(writer, effectivePA)
if len(matchedPA) != 0 {
printConfigs(writer, matchedPA)
}
return nil
}
// Workloader is used for matching all configs
type Workloader interface {
GetSelector() *typev1beta1.WorkloadSelector
}
// findMatchedConfigs should filter out unrelated configs that are not matched given podsLabels.
// When the config has no selector labels, this method will treat it as qualified namespace level
// config. So configs passed into this method should only contains workload's namespaces configs
// and rootNamespaces configs, caller should be responsible for controlling configs passed
// in.
func findMatchedConfigs(podsLabels k8s_labels.Set, configs []*config.Config) []*config.Config {
var cfgs []*config.Config
for _, cfg := range configs {
cfg := cfg
labels := cfg.Spec.(Workloader).GetSelector().GetMatchLabels()
selector := k8s_labels.SelectorFromSet(labels)
if selector.Matches(podsLabels) {
cfgs = append(cfgs, cfg)
}
}
return cfgs
}
// printConfig prints the applied configs based on the member's type.
// When there is the array is empty, caller should make sure the intended
// log is handled in their methods.
func printConfigs(writer io.Writer, configs []*config.Config) {
if len(configs) == 0 {
return
}
fmt.Fprintf(writer, "Applied %s:\n", configs[0].Meta.GroupVersionKind.Kind)
var cfgNames string
for i, cfg := range configs {
cfgNames += cfg.Meta.Name + "." + cfg.Meta.Namespace
if i < len(configs)-1 {
cfgNames += ", "
}
}
fmt.Fprintf(writer, " %s\n", cfgNames)
}
func printPeerAuthentication(writer io.Writer, pa *v1beta1.PeerAuthentication) {
fmt.Fprintf(writer, "Effective PeerAuthentication:\n")
fmt.Fprintf(writer, " Workload mTLS mode: %s\n", pa.Mtls.Mode.String())
if len(pa.PortLevelMtls) != 0 {
fmt.Fprintf(writer, " Port Level mTLS mode:\n")
for port, mode := range pa.PortLevelMtls {
fmt.Fprintf(writer, " %d: %s\n", port, mode.Mode.String())
}
}
}
func getMeshConfig(kubeClient kube.ExtendedClient) (*meshconfig.MeshConfig, error) {
rev := kubeClient.Revision()
meshConfigMapName := defaultMeshConfigMapName
// if the revision is not "default", render mesh config map name with revision
if rev != tag.DefaultRevisionName && rev != "" {
meshConfigMapName = fmt.Sprintf("%s-%s", defaultMeshConfigMapName, rev)
}
meshConfigMap, err := kubeClient.CoreV1().ConfigMaps(istioNamespace).Get(context.TODO(), meshConfigMapName, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("could not read configmap %q from namespace %q: %v", meshConfigMapName, istioNamespace, err)
}
configYaml, ok := meshConfigMap.Data[defaultMeshConfigMapKey]
if !ok {
return nil, fmt.Errorf("missing config map key %q", defaultMeshConfigMapKey)
}
cfg, err := mesh.ApplyMeshConfigDefaults(configYaml)
if err != nil {
return nil, fmt.Errorf("error parsing mesh config: %v", err)
}
return cfg, nil
}