istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/multixds/gather.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 multixds
    16  
    17  // multixds knows how to target either central Istiod or all the Istiod pods on a cluster.
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"net"
    26  	"net/url"
    27  	"os"
    28  	"strings"
    29  
    30  	discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    31  	xdsstatus "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
    32  	"google.golang.org/grpc"
    33  	v1 "k8s.io/api/core/v1"
    34  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    35  
    36  	"istio.io/api/label"
    37  	"istio.io/istio/istioctl/pkg/clioptions"
    38  	"istio.io/istio/istioctl/pkg/xds"
    39  	pilotxds "istio.io/istio/pilot/pkg/xds"
    40  	"istio.io/istio/pkg/kube"
    41  	istioversion "istio.io/istio/pkg/version"
    42  )
    43  
    44  const (
    45  	// Service account to create tokens in
    46  	tokenServiceAccount = "default"
    47  	// Get the pods with limit = 500.
    48  	kubeClientGetPodLimit = 500
    49  )
    50  
    51  type ControlPlaneNotFoundError struct {
    52  	Namespace string
    53  }
    54  
    55  func (c ControlPlaneNotFoundError) Error() string {
    56  	return fmt.Sprintf("no running Istio pods in %q", c.Namespace)
    57  }
    58  
    59  var _ error = ControlPlaneNotFoundError{}
    60  
    61  type Options struct {
    62  	// MessageWriter is a writer for displaying messages to users.
    63  	MessageWriter io.Writer
    64  
    65  	// XdsViaAgents accesses Istiod via the tap service of each agent.
    66  	// This is only used in `proxy-status` command.
    67  	XdsViaAgents bool
    68  
    69  	// XdsViaAgentsLimit is the maximum number of pods being visited by istioctl,
    70  	// when `XdsViaAgents` is true. This is only used in `proxy-status` command.
    71  	// 0 means that there is no limit.
    72  	XdsViaAgentsLimit int
    73  }
    74  
    75  var DefaultOptions = Options{
    76  	MessageWriter:     os.Stdout,
    77  	XdsViaAgents:      false,
    78  	XdsViaAgentsLimit: 0,
    79  }
    80  
    81  // RequestAndProcessXds merges XDS responses from 1 central or 1..N K8s cluster-based XDS servers
    82  // Deprecated This method makes multiple responses appear to come from a single control plane;
    83  // consider using AllRequestAndProcessXds or FirstRequestAndProcessXds
    84  // nolint: lll
    85  func RequestAndProcessXds(dr *discovery.DiscoveryRequest, centralOpts clioptions.CentralControlPlaneOptions, istioNamespace string, kubeClient kube.CLIClient) (*discovery.DiscoveryResponse, error) {
    86  	responses, err := MultiRequestAndProcessXds(true, dr, centralOpts, istioNamespace,
    87  		istioNamespace, tokenServiceAccount, kubeClient, DefaultOptions)
    88  	if err != nil {
    89  		return nil, err
    90  	}
    91  	return mergeShards(responses)
    92  }
    93  
    94  var GetXdsResponse = xds.GetXdsResponse
    95  
    96  // nolint: lll
    97  func queryEachShard(all bool, dr *discovery.DiscoveryRequest, istioNamespace string, kubeClient kube.CLIClient, centralOpts clioptions.CentralControlPlaneOptions) ([]*discovery.DiscoveryResponse, error) {
    98  	labelSelector := centralOpts.XdsPodLabel
    99  	if labelSelector == "" {
   100  		labelSelector = "app=istiod"
   101  	}
   102  	pods, err := kubeClient.GetIstioPods(context.TODO(), istioNamespace, metav1.ListOptions{
   103  		LabelSelector: labelSelector,
   104  		FieldSelector: kube.RunningStatus,
   105  	})
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  	if len(pods) == 0 {
   110  		return nil, ControlPlaneNotFoundError{istioNamespace}
   111  	}
   112  
   113  	responses := []*discovery.DiscoveryResponse{}
   114  	xdsOpts := clioptions.CentralControlPlaneOptions{
   115  		XDSSAN:  makeSan(istioNamespace, kubeClient.Revision()),
   116  		CertDir: centralOpts.CertDir,
   117  		Timeout: centralOpts.Timeout,
   118  	}
   119  	dialOpts, err := xds.DialOptions(xdsOpts, istioNamespace, tokenServiceAccount, kubeClient)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	for _, pod := range pods {
   125  		fw, err := kubeClient.NewPortForwarder(pod.Name, pod.Namespace, "localhost", 0, centralOpts.XdsPodPort)
   126  		if err != nil {
   127  			return nil, err
   128  		}
   129  		err = fw.Start()
   130  		if err != nil {
   131  			return nil, err
   132  		}
   133  		defer fw.Close()
   134  		xdsOpts.Xds = fw.Address()
   135  		response, err := GetXdsResponse(dr, istioNamespace, tokenServiceAccount, xdsOpts, dialOpts)
   136  		if err != nil {
   137  			return nil, fmt.Errorf("could not get XDS from discovery pod %q: %v", pod.Name, err)
   138  		}
   139  		responses = append(responses, response)
   140  		if !all && len(responses) > 0 {
   141  			break
   142  		}
   143  	}
   144  	return responses, nil
   145  }
   146  
   147  // queryDebugSynczViaAgents sends a debug/syncz xDS request via Istio Agents.
   148  // By this way, even if istioctl cannot access a specific `istiod` instance directly,
   149  // `istioctl` can access the debug endpoint.
   150  // If `all` is true, `queryDebugSynczViaAgents` iterates all the pod having a proxy
   151  // except the pods of which status information is already queried.
   152  func queryDebugSynczViaAgents(all bool, dr *discovery.DiscoveryRequest, istioNamespace string, kubeClient kube.CLIClient,
   153  	centralOpts clioptions.CentralControlPlaneOptions, options Options,
   154  ) ([]*discovery.DiscoveryResponse, error) {
   155  	xdsOpts := clioptions.CentralControlPlaneOptions{
   156  		XDSSAN:  makeSan(istioNamespace, kubeClient.Revision()),
   157  		CertDir: centralOpts.CertDir,
   158  		Timeout: centralOpts.Timeout,
   159  	}
   160  	visited := make(map[string]bool)
   161  	queryToOnePod := func(pod *v1.Pod) (*discovery.DiscoveryResponse, error) {
   162  		fw, err := kubeClient.NewPortForwarder(pod.Name, pod.Namespace, "localhost", 0, 15004)
   163  		if err != nil {
   164  			return nil, err
   165  		}
   166  		err = fw.Start()
   167  		if err != nil {
   168  			return nil, err
   169  		}
   170  		defer fw.Close()
   171  		xdsOpts.Xds = fw.Address()
   172  		// Use plaintext.
   173  		response, err := xds.GetXdsResponse(dr, istioNamespace, tokenServiceAccount, xdsOpts, []grpc.DialOption{})
   174  		if err != nil {
   175  			return nil, fmt.Errorf("could not get XDS from the agent pod %q: %v", pod.Name, err)
   176  		}
   177  		for _, resource := range response.GetResources() {
   178  			switch resource.GetTypeUrl() {
   179  			case "type.googleapis.com/envoy.service.status.v3.ClientConfig":
   180  				clientConfig := xdsstatus.ClientConfig{}
   181  				err := resource.UnmarshalTo(&clientConfig)
   182  				if err != nil {
   183  					return nil, err
   184  				}
   185  				visited[clientConfig.Node.Id] = true
   186  			default:
   187  				// ignore unknown types.
   188  			}
   189  		}
   190  		return response, nil
   191  	}
   192  
   193  	responses := []*discovery.DiscoveryResponse{}
   194  	if all {
   195  		token := ""
   196  		touchedPods := 0
   197  
   198  	GetProxyLoop:
   199  		for {
   200  			list, err := kubeClient.GetProxyPods(context.TODO(), int64(kubeClientGetPodLimit), token)
   201  			if err != nil {
   202  				return nil, err
   203  			}
   204  			// Iterate all the pod.
   205  			for _, pod := range list.Items {
   206  				touchedPods++
   207  				if options.XdsViaAgentsLimit != 0 && touchedPods > options.XdsViaAgentsLimit {
   208  					fmt.Fprintf(options.MessageWriter, "Some proxies may be missing from the list"+
   209  						" because the number of visited pod hits the limit %d,"+
   210  						" which can be set by `--xds-via-agents-limit` flag.\n", options.XdsViaAgentsLimit)
   211  					break GetProxyLoop
   212  				}
   213  				namespacedName := pod.Name + "." + pod.Namespace
   214  				if visited[namespacedName] {
   215  					// If we already have information about the pod, skip it.
   216  					continue
   217  				}
   218  				resp, err := queryToOnePod(&pod)
   219  				if err != nil {
   220  					fmt.Fprintf(os.Stderr, "Skip the agent in Pod %s due to the error: %s\n", namespacedName, err.Error())
   221  					continue
   222  				}
   223  				responses = append(responses, resp)
   224  			}
   225  			token = list.ListMeta.GetContinue()
   226  			if token == "" {
   227  				break
   228  			}
   229  		}
   230  	} else {
   231  		// If there is a specific pod name in ResourceName, use the agent in the pod.
   232  		if len(dr.ResourceNames) != 1 {
   233  			return nil, fmt.Errorf("`ResourceNames` must have one element when `all` flag is turned on")
   234  		}
   235  		slice := strings.SplitN(dr.ResourceNames[0], ".", 2)
   236  		if len(slice) != 2 {
   237  			return nil, fmt.Errorf("invalid resource name format: %v", slice)
   238  		}
   239  		podName := slice[0]
   240  		ns := slice[1]
   241  		pod, err := kubeClient.Kube().CoreV1().Pods(ns).Get(context.TODO(), podName, metav1.GetOptions{})
   242  		if err != nil {
   243  			return nil, err
   244  		}
   245  		resp, err := queryToOnePod(pod)
   246  		if err != nil {
   247  			return nil, err
   248  		}
   249  		responses = append(responses, resp)
   250  		return responses, nil
   251  	}
   252  
   253  	return responses, nil
   254  }
   255  
   256  func mergeShards(responses map[string]*discovery.DiscoveryResponse) (*discovery.DiscoveryResponse, error) {
   257  	retval := discovery.DiscoveryResponse{}
   258  	if len(responses) == 0 {
   259  		return &retval, nil
   260  	}
   261  
   262  	for _, response := range responses {
   263  		// Combine all the shards as one, even if that means losing information about
   264  		// the control plane version from each shard.
   265  		retval.ControlPlane = response.ControlPlane
   266  		retval.Resources = append(retval.Resources, response.Resources...)
   267  	}
   268  
   269  	return &retval, nil
   270  }
   271  
   272  func makeSan(istioNamespace, revision string) string {
   273  	if revision == "" {
   274  		return fmt.Sprintf("istiod.%s.svc", istioNamespace)
   275  	}
   276  	return fmt.Sprintf("istiod-%s.%s.svc", revision, istioNamespace)
   277  }
   278  
   279  // AllRequestAndProcessXds returns all XDS responses from 1 central or 1..N K8s cluster-based XDS servers
   280  // nolint: lll
   281  func AllRequestAndProcessXds(dr *discovery.DiscoveryRequest, centralOpts clioptions.CentralControlPlaneOptions, istioNamespace string,
   282  	ns string, serviceAccount string, kubeClient kube.CLIClient, options Options,
   283  ) (map[string]*discovery.DiscoveryResponse, error) {
   284  	return MultiRequestAndProcessXds(true, dr, centralOpts, istioNamespace, ns, serviceAccount, kubeClient, options)
   285  }
   286  
   287  // FirstRequestAndProcessXds returns all XDS responses from 1 central or 1..N K8s cluster-based XDS servers,
   288  // stopping after the first response that returns any resources.
   289  // nolint: lll
   290  func FirstRequestAndProcessXds(dr *discovery.DiscoveryRequest, centralOpts clioptions.CentralControlPlaneOptions, istioNamespace string,
   291  	ns string, serviceAccount string, kubeClient kube.CLIClient, options Options,
   292  ) (map[string]*discovery.DiscoveryResponse, error) {
   293  	return MultiRequestAndProcessXds(false, dr, centralOpts, istioNamespace, ns, serviceAccount, kubeClient, options)
   294  }
   295  
   296  type xdsAddr struct {
   297  	gcpProject, host, istiod string
   298  }
   299  
   300  func getXdsAddressFromWebhooks(client kube.CLIClient) (*xdsAddr, error) {
   301  	webhooks, err := client.Kube().AdmissionregistrationV1().MutatingWebhookConfigurations().List(context.Background(), metav1.ListOptions{
   302  		LabelSelector: fmt.Sprintf("%s=%s,!istio.io/tag", label.IoIstioRev.Name, client.Revision()),
   303  	})
   304  	if err != nil {
   305  		return nil, err
   306  	}
   307  	for _, whc := range webhooks.Items {
   308  		for _, wh := range whc.Webhooks {
   309  			if wh.ClientConfig.URL != nil {
   310  				u, err := url.Parse(*wh.ClientConfig.URL)
   311  				if err != nil {
   312  					return nil, fmt.Errorf("parsing webhook URL: %w", err)
   313  				}
   314  				if isMCPAddr(u) {
   315  					return parseMCPAddr(u)
   316  				}
   317  				port := u.Port()
   318  				if port == "" {
   319  					port = "443" // default from Kubernetes
   320  				}
   321  				return &xdsAddr{host: net.JoinHostPort(u.Hostname(), port)}, nil
   322  			}
   323  		}
   324  	}
   325  	return nil, errors.New("xds address not found")
   326  }
   327  
   328  // nolint: lll
   329  func MultiRequestAndProcessXds(all bool, dr *discovery.DiscoveryRequest, centralOpts clioptions.CentralControlPlaneOptions, istioNamespace string,
   330  	ns string, serviceAccount string, kubeClient kube.CLIClient, options Options,
   331  ) (map[string]*discovery.DiscoveryResponse, error) {
   332  	// If Central Istiod case, just call it
   333  	if ns == "" {
   334  		ns = istioNamespace
   335  	}
   336  	if ns == istioNamespace {
   337  		serviceAccount = tokenServiceAccount
   338  	}
   339  	if centralOpts.Xds != "" {
   340  		dialOpts, err := xds.DialOptions(centralOpts, ns, serviceAccount, kubeClient)
   341  		if err != nil {
   342  			return nil, err
   343  		}
   344  		response, err := xds.GetXdsResponse(dr, ns, serviceAccount, centralOpts, dialOpts)
   345  		if err != nil {
   346  			return nil, err
   347  		}
   348  		return map[string]*discovery.DiscoveryResponse{
   349  			CpInfo(response).ID: response,
   350  		}, nil
   351  	}
   352  
   353  	var (
   354  		responses []*discovery.DiscoveryResponse
   355  		err       error
   356  	)
   357  
   358  	if options.XdsViaAgents {
   359  		responses, err = queryDebugSynczViaAgents(all, dr, istioNamespace, kubeClient, centralOpts, options)
   360  	} else {
   361  		// Self-administered case.  Find all Istiods in revision using K8s, port-forward and call each in turn
   362  		responses, err = queryEachShard(all, dr, istioNamespace, kubeClient, centralOpts)
   363  	}
   364  	if err != nil {
   365  		if _, ok := err.(ControlPlaneNotFoundError); ok {
   366  			// Attempt to get the XDS address from the webhook and try again
   367  			addr, err := getXdsAddressFromWebhooks(kubeClient)
   368  			if err == nil {
   369  				centralOpts.Xds = addr.host
   370  				centralOpts.GCPProject = addr.gcpProject
   371  				centralOpts.IstiodAddr = addr.istiod
   372  				dialOpts, err := xds.DialOptions(centralOpts, istioNamespace, tokenServiceAccount, kubeClient)
   373  				if err != nil {
   374  					return nil, err
   375  				}
   376  				response, err := xds.GetXdsResponse(dr, istioNamespace, tokenServiceAccount, centralOpts, dialOpts)
   377  				if err != nil {
   378  					return nil, err
   379  				}
   380  				return map[string]*discovery.DiscoveryResponse{
   381  					CpInfo(response).ID: response,
   382  				}, nil
   383  			}
   384  		}
   385  		return nil, err
   386  	}
   387  	return mapShards(responses)
   388  }
   389  
   390  func mapShards(responses []*discovery.DiscoveryResponse) (map[string]*discovery.DiscoveryResponse, error) {
   391  	retval := map[string]*discovery.DiscoveryResponse{}
   392  
   393  	for _, response := range responses {
   394  		retval[CpInfo(response).ID] = response
   395  	}
   396  
   397  	return retval, nil
   398  }
   399  
   400  // CpInfo returns the Istio control plane info from JSON-encoded XDS ControlPlane Identifier
   401  func CpInfo(xdsResponse *discovery.DiscoveryResponse) pilotxds.IstioControlPlaneInstance {
   402  	if xdsResponse.ControlPlane == nil {
   403  		return pilotxds.IstioControlPlaneInstance{
   404  			Component: "MISSING",
   405  			ID:        "MISSING",
   406  			Info: istioversion.BuildInfo{
   407  				Version: "MISSING CP ID",
   408  			},
   409  		}
   410  	}
   411  
   412  	cpID := pilotxds.IstioControlPlaneInstance{}
   413  	err := json.Unmarshal([]byte(xdsResponse.ControlPlane.Identifier), &cpID)
   414  	if err != nil {
   415  		return pilotxds.IstioControlPlaneInstance{
   416  			Component: "INVALID",
   417  			ID:        "INVALID",
   418  			Info: istioversion.BuildInfo{
   419  				Version: "INVALID CP ID",
   420  			},
   421  		}
   422  	}
   423  	return cpID
   424  }