istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/version/version.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 version
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"os"
    22  
    23  	core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    24  	discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    25  	statusv3 "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
    26  	"github.com/spf13/cobra"
    27  	"github.com/spf13/pflag"
    28  	"google.golang.org/protobuf/types/known/structpb"
    29  
    30  	"istio.io/istio/istioctl/pkg/cli"
    31  	"istio.io/istio/istioctl/pkg/clioptions"
    32  	"istio.io/istio/istioctl/pkg/multixds"
    33  	"istio.io/istio/operator/cmd/mesh"
    34  	"istio.io/istio/pilot/pkg/model"
    35  	"istio.io/istio/pilot/pkg/xds"
    36  	"istio.io/istio/pkg/proxy"
    37  	istioVersion "istio.io/istio/pkg/version"
    38  )
    39  
    40  func NewVersionCommand(ctx cli.Context) *cobra.Command {
    41  	profileCmd := mesh.ProfileCmd(ctx)
    42  	var opts clioptions.ControlPlaneOptions
    43  	versionCmd := istioVersion.CobraCommandWithOptions(istioVersion.CobraOptions{
    44  		GetRemoteVersion: getRemoteInfoWrapper(ctx, &profileCmd, &opts),
    45  		GetProxyVersions: getProxyInfoWrapper(ctx, &opts),
    46  	})
    47  	opts.AttachControlPlaneFlags(versionCmd)
    48  
    49  	versionCmd.Flags().VisitAll(func(flag *pflag.Flag) {
    50  		if flag.Name == "short" {
    51  			err := flag.Value.Set("true")
    52  			if err != nil {
    53  				fmt.Fprintf(os.Stdout, "set flag %q as true failed due to error %v", flag.Name, err)
    54  			}
    55  		}
    56  		if flag.Name == "remote" {
    57  			err := flag.Value.Set("true")
    58  			if err != nil {
    59  				fmt.Fprintf(os.Stdout, "set flag %q as true failed due to error %v", flag.Name, err)
    60  			}
    61  		}
    62  	})
    63  	return versionCmd
    64  }
    65  
    66  func getRemoteInfo(ctx cli.Context, opts clioptions.ControlPlaneOptions) (*istioVersion.MeshInfo, error) {
    67  	kubeClient, err := ctx.CLIClientWithRevision(opts.Revision)
    68  	if err != nil {
    69  		return nil, err
    70  	}
    71  
    72  	return kubeClient.GetIstioVersions(context.TODO(), ctx.IstioNamespace())
    73  }
    74  
    75  func getRemoteInfoWrapper(ctx cli.Context, pc **cobra.Command, opts *clioptions.ControlPlaneOptions) func() (*istioVersion.MeshInfo, error) {
    76  	return func() (*istioVersion.MeshInfo, error) {
    77  		remInfo, err := getRemoteInfo(ctx, *opts)
    78  		if err != nil {
    79  			fmt.Fprintf((*pc).OutOrStderr(), "%v\n", err)
    80  			// Return nil so that the client version is printed
    81  			return nil, nil
    82  		}
    83  		if remInfo == nil {
    84  			fmt.Fprintf((*pc).OutOrStderr(), "Istio is not present in the cluster with namespace %q\n", ctx.IstioNamespace())
    85  		}
    86  		return remInfo, err
    87  	}
    88  }
    89  
    90  func getProxyInfoWrapper(ctx cli.Context, opts *clioptions.ControlPlaneOptions) func() (*[]istioVersion.ProxyInfo, error) {
    91  	return func() (*[]istioVersion.ProxyInfo, error) {
    92  		client, err := ctx.CLIClientWithRevision(opts.Revision)
    93  		if err != nil {
    94  			return nil, err
    95  		}
    96  		return proxy.GetProxyInfo(client, ctx.IstioNamespace())
    97  	}
    98  }
    99  
   100  // XdsVersionCommand gets the Control Plane and Sidecar versions via XDS
   101  func XdsVersionCommand(ctx cli.Context) *cobra.Command {
   102  	var opts clioptions.ControlPlaneOptions
   103  	var centralOpts clioptions.CentralControlPlaneOptions
   104  	var xdsResponses *discovery.DiscoveryResponse
   105  	versionCmd := istioVersion.CobraCommandWithOptions(istioVersion.CobraOptions{
   106  		GetRemoteVersion: xdsRemoteVersionWrapper(ctx, &opts, &centralOpts, &xdsResponses),
   107  		GetProxyVersions: xdsProxyVersionWrapper(&xdsResponses),
   108  	})
   109  	opts.AttachControlPlaneFlags(versionCmd)
   110  	centralOpts.AttachControlPlaneFlags(versionCmd)
   111  	versionCmd.Args = func(c *cobra.Command, args []string) error {
   112  		if err := cobra.NoArgs(c, args); err != nil {
   113  			return err
   114  		}
   115  		if err := centralOpts.ValidateControlPlaneFlags(); err != nil {
   116  			return err
   117  		}
   118  		return nil
   119  	}
   120  	versionCmd.Example = `  # Retrieve version information directly from the control plane, using token security
   121    # (This is the usual way to get the control plane version with an out-of-cluster control plane.)
   122    istioctl x version --xds-address istio.cloudprovider.example.com:15012
   123  
   124    # Retrieve version information via Kubernetes config, using token security
   125    # (This is the usual way to get the control plane version with an in-cluster control plane.)
   126    istioctl x version
   127  
   128    # Retrieve version information directly from the control plane, using RSA certificate security
   129    # (Certificates must be obtained before this step.  The --cert-dir flag lets istioctl bypass the Kubernetes API server.)
   130    istioctl x version --xds-address istio.example.com:15012 --cert-dir ~/.istio-certs
   131  
   132    # Retrieve version information via XDS from specific control plane in multi-control plane in-cluster configuration
   133    # (Select a specific control plane in an in-cluster canary Istio configuration.)
   134    istioctl x version --xds-label istio.io/rev=default
   135  `
   136  
   137  	versionCmd.Flags().VisitAll(func(flag *pflag.Flag) {
   138  		if flag.Name == "short" {
   139  			err := flag.Value.Set("true")
   140  			if err != nil {
   141  				fmt.Fprintf(os.Stdout, "set flag %q as true failed due to error %v", flag.Name, err)
   142  			}
   143  		}
   144  		if flag.Name == "remote" {
   145  			err := flag.Value.Set("true")
   146  			if err != nil {
   147  				fmt.Fprintf(os.Stdout, "set flag %q as true failed due to error %v", flag.Name, err)
   148  			}
   149  		}
   150  	})
   151  	return versionCmd
   152  }
   153  
   154  // xdsRemoteVersionWrapper uses outXDS to share the XDS response with xdsProxyVersionWrapper.
   155  // (Screwy API on istioVersion.CobraCommandWithOptions)
   156  // nolint: lll
   157  func xdsRemoteVersionWrapper(ctx cli.Context, opts *clioptions.ControlPlaneOptions, centralOpts *clioptions.CentralControlPlaneOptions, outXDS **discovery.DiscoveryResponse) func() (*istioVersion.MeshInfo, error) {
   158  	return func() (*istioVersion.MeshInfo, error) {
   159  		xdsRequest := discovery.DiscoveryRequest{
   160  			TypeUrl: xds.TypeDebugSyncronization,
   161  		}
   162  		kubeClient, err := ctx.CLIClientWithRevision(opts.Revision)
   163  		if err != nil {
   164  			return nil, err
   165  		}
   166  		xdsResponse, err := multixds.RequestAndProcessXds(&xdsRequest, *centralOpts, ctx.IstioNamespace(), kubeClient)
   167  		if err != nil {
   168  			return nil, err
   169  		}
   170  		*outXDS = xdsResponse
   171  		if xdsResponse.ControlPlane == nil {
   172  			return &istioVersion.MeshInfo{
   173  				istioVersion.ServerInfo{
   174  					Component: "MISSING CP ID",
   175  					Info: istioVersion.BuildInfo{
   176  						Version: "MISSING CP ID",
   177  					},
   178  					Revision: "MISSING CP ID",
   179  				},
   180  			}, nil
   181  		}
   182  		cpID := xds.IstioControlPlaneInstance{}
   183  		err = json.Unmarshal([]byte(xdsResponse.ControlPlane.Identifier), &cpID)
   184  		if err != nil {
   185  			return nil, fmt.Errorf("could not parse CP Identifier: %w", err)
   186  		}
   187  		return &istioVersion.MeshInfo{
   188  			istioVersion.ServerInfo{
   189  				Component: cpID.Component,
   190  				Info:      cpID.Info,
   191  				Revision:  opts.Revision,
   192  			},
   193  		}, nil
   194  	}
   195  }
   196  
   197  func xdsProxyVersionWrapper(xdsResponse **discovery.DiscoveryResponse) func() (*[]istioVersion.ProxyInfo, error) {
   198  	return func() (*[]istioVersion.ProxyInfo, error) {
   199  		pi := []istioVersion.ProxyInfo{}
   200  		for _, resource := range (*xdsResponse).Resources {
   201  			switch resource.TypeUrl {
   202  			case "type.googleapis.com/envoy.config.core.v3.Node":
   203  				node := core.Node{}
   204  				err := resource.UnmarshalTo(&node)
   205  				if err != nil {
   206  					return nil, fmt.Errorf("could not unmarshal Node: %w", err)
   207  				}
   208  				meta, err := model.ParseMetadata(node.Metadata)
   209  				if err != nil || meta.ProxyConfig == nil {
   210  					// Skip non-sidecars (e.g. istioctl queries)
   211  					continue
   212  				}
   213  				pi = append(pi, istioVersion.ProxyInfo{
   214  					ID:           node.Id,
   215  					IstioVersion: getIstioVersionFromXdsMetadata(node.Metadata),
   216  				})
   217  			case "type.googleapis.com/envoy.service.status.v3.ClientConfig":
   218  				cc := statusv3.ClientConfig{}
   219  				err := resource.UnmarshalTo(&cc)
   220  				if err != nil {
   221  					return nil, fmt.Errorf("could not unmarshal Node: %w", err)
   222  				}
   223  				node := cc.Node
   224  				pi = append(pi, istioVersion.ProxyInfo{
   225  					ID:           node.Id,
   226  					IstioVersion: getIstioVersionFromXdsMetadata(node.Metadata),
   227  				})
   228  			default:
   229  				return nil, fmt.Errorf("unexpected resource type %q", resource.TypeUrl)
   230  			}
   231  		}
   232  		return &pi, nil
   233  	}
   234  }
   235  
   236  func getIstioVersionFromXdsMetadata(metadata *structpb.Struct) string {
   237  	meta, err := model.ParseMetadata(metadata)
   238  	if err != nil {
   239  		return "unknown sidecar version"
   240  	}
   241  	return meta.IstioVersion
   242  }