istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/echo/cmd/client/main.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  // An example implementation of a client.
    16  
    17  package main
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"net/url"
    23  	"os"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/spf13/cobra"
    28  	// To install the xds resolvers and balancers.
    29  	_ "google.golang.org/grpc/xds"
    30  	wrappers "google.golang.org/protobuf/types/known/wrapperspb"
    31  
    32  	"istio.io/istio/pkg/cmd"
    33  	"istio.io/istio/pkg/log"
    34  	"istio.io/istio/pkg/test/echo/common"
    35  	"istio.io/istio/pkg/test/echo/proto"
    36  	"istio.io/istio/pkg/test/echo/server/forwarder"
    37  )
    38  
    39  var (
    40  	count                   int
    41  	timeout                 time.Duration
    42  	qps                     int
    43  	uds                     string
    44  	headers                 []string
    45  	msg                     string
    46  	expect                  string
    47  	expectSet               bool
    48  	method                  string
    49  	http2                   bool
    50  	http3                   bool
    51  	insecureSkipVerify      bool
    52  	alpn                    []string
    53  	serverName              string
    54  	serverFirst             bool
    55  	followRedirects         bool
    56  	newConnectionPerRequest bool
    57  	forceDNSLookup          bool
    58  
    59  	clientCert string
    60  	clientKey  string
    61  
    62  	caFile string
    63  
    64  	hboneAddress            string
    65  	hboneHeaders            []string
    66  	hboneClientCert         string
    67  	hboneClientKey          string
    68  	hboneCaFile             string
    69  	hboneInsecureSkipVerify bool
    70  
    71  	loggingOptions = log.DefaultOptions()
    72  
    73  	rootCmd = &cobra.Command{
    74  		Use:          "client",
    75  		Short:        "Istio test Echo client.",
    76  		SilenceUsage: true,
    77  		Long: `Istio test Echo client used for generating traffic between instances of the Echo service.
    78  For Kubernetes, this can be run from a source pod via "kubectl exec" with the desired flags.
    79  In general, Echo's gRPC interface (ForwardEcho) should be preferred. This client is only needed in cases
    80  where the network configuration doesn't support gRPC to the source pod.'
    81  `,
    82  		Args:              cobra.ExactArgs(1),
    83  		PersistentPreRunE: configureLogging,
    84  		Run: func(cmd *cobra.Command, args []string) {
    85  			expectSet = cmd.Flags().Changed("expect")
    86  			// Create a request from the flags.
    87  			request, err := getRequest(args[0])
    88  			if err != nil {
    89  				log.Fatal(err)
    90  			}
    91  
    92  			// Create a forwarder.
    93  			f := forwarder.New()
    94  			defer func() {
    95  				_ = f.Close()
    96  			}()
    97  
    98  			// Forward the requests.
    99  			response, err := f.ForwardEcho(context.Background(), &forwarder.Config{
   100  				Request: request,
   101  				UDS:     uds,
   102  			})
   103  			if err != nil {
   104  				log.Fatalf("Error %s\n", err) // nolint: revive
   105  				os.Exit(-1)
   106  			}
   107  
   108  			// Log the output to stdout.
   109  			for _, line := range response.Output {
   110  				fmt.Println(line)
   111  			}
   112  
   113  			log.Infof("All requests succeeded")
   114  		},
   115  	}
   116  )
   117  
   118  func configureLogging(_ *cobra.Command, _ []string) error {
   119  	if err := log.Configure(loggingOptions); err != nil {
   120  		return err
   121  	}
   122  	return nil
   123  }
   124  
   125  func init() {
   126  	rootCmd.PersistentFlags().IntVar(&count, "count", common.DefaultCount, "Number of times to make the request")
   127  	rootCmd.PersistentFlags().IntVar(&qps, "qps", 0, "Queries per second")
   128  	rootCmd.PersistentFlags().DurationVar(&timeout, "timeout", common.DefaultRequestTimeout, "Request timeout")
   129  	rootCmd.PersistentFlags().StringVar(&uds, "uds", "",
   130  		"Specify the Unix Domain Socket to connect to")
   131  	rootCmd.PersistentFlags().StringSliceVarP(&headers, "header", "H", headers,
   132  		"A list of http headers (use Host for authority) - 'name: value', following curl syntax")
   133  	rootCmd.PersistentFlags().StringVar(&caFile, "ca", "", "CA root cert file")
   134  	rootCmd.PersistentFlags().StringVar(&msg, "msg", "HelloWorld",
   135  		"message to send (for websockets)")
   136  	rootCmd.PersistentFlags().StringVar(&expect, "expect", "",
   137  		"message to expect (for tcp)")
   138  	rootCmd.PersistentFlags().StringVar(&method, "method", "", "method to use (for HTTP)")
   139  	rootCmd.PersistentFlags().BoolVar(&http2, "http2", false,
   140  		"send http requests as HTTP2 with prior knowledge")
   141  	rootCmd.PersistentFlags().BoolVar(&http3, "http3", false,
   142  		"send http requests as HTTP 3")
   143  	rootCmd.PersistentFlags().BoolVarP(&insecureSkipVerify, "insecure-skip-verify", "k", insecureSkipVerify,
   144  		"do not verify TLS")
   145  	rootCmd.PersistentFlags().BoolVar(&serverFirst, "server-first", false,
   146  		"Treat as a server first protocol; do not send request until magic string is received")
   147  	rootCmd.PersistentFlags().BoolVarP(&followRedirects, "follow-redirects", "L", false,
   148  		"If enabled, will follow 3xx redirects with the Location header")
   149  	rootCmd.PersistentFlags().BoolVar(&newConnectionPerRequest, "new-connection-per-request", false,
   150  		"If enabled, a new connection will be made to the server for each individual request. "+
   151  			"If false, an attempt will be made to re-use the connection for the life of the forward request. "+
   152  			"This is automatically set for DNS, TCP, TLS, and WebSocket protocols.")
   153  	rootCmd.PersistentFlags().BoolVar(&forceDNSLookup, "force-dns-lookup", false,
   154  		"If enabled, each request will force a DNS lookup. Only applies if new-connection-per-request is also enabled.")
   155  	rootCmd.PersistentFlags().StringVar(&clientCert, "client-cert", "", "client certificate file to use for request")
   156  	rootCmd.PersistentFlags().StringVar(&clientKey, "client-key", "", "client certificate key file to use for request")
   157  	rootCmd.PersistentFlags().StringSliceVarP(&alpn, "alpn", "", nil, "alpn to set")
   158  	rootCmd.PersistentFlags().StringVarP(&serverName, "server-name", "", serverName, "server name to set")
   159  
   160  	rootCmd.PersistentFlags().StringVar(&hboneAddress, "hbone", "", "address to send HBONE request to")
   161  	rootCmd.PersistentFlags().StringSliceVarP(&hboneHeaders, "hbone-header", "M", hboneHeaders,
   162  		"A list of http headers for HBONE connection (use Host for authority) - 'name: value', following curl syntax")
   163  	rootCmd.PersistentFlags().StringVar(&hboneCaFile, "hbone-ca", "", "CA root cert file used for the HBONE request")
   164  	rootCmd.PersistentFlags().StringVar(&hboneClientCert, "hbone-client-cert", "", "client certificate file used for the HBONE request")
   165  	rootCmd.PersistentFlags().StringVar(&hboneClientKey, "hbone-client-key", "", "client certificate key file used for the HBONE request")
   166  	rootCmd.PersistentFlags().BoolVar(&hboneInsecureSkipVerify, "hbone-insecure-skip-verify", hboneInsecureSkipVerify, "skip TLS verification of HBONE request")
   167  
   168  	loggingOptions.AttachCobraFlags(rootCmd)
   169  
   170  	cmd.AddFlags(rootCmd)
   171  }
   172  
   173  // Adds http scheme to and url if not set. This matches curl logic
   174  func defaultScheme(u string) string {
   175  	p, err := url.Parse(u)
   176  	if err != nil {
   177  		return u
   178  	}
   179  	if p.Scheme == "" {
   180  		return "http://" + u
   181  	}
   182  	return u
   183  }
   184  
   185  func getRequest(url string) (*proto.ForwardEchoRequest, error) {
   186  	request := &proto.ForwardEchoRequest{
   187  		Url:                     defaultScheme(url),
   188  		TimeoutMicros:           common.DurationToMicros(timeout),
   189  		Count:                   int32(count),
   190  		Qps:                     int32(qps),
   191  		Message:                 msg,
   192  		Http2:                   http2,
   193  		Http3:                   http3,
   194  		ServerFirst:             serverFirst,
   195  		FollowRedirects:         followRedirects,
   196  		Method:                  method,
   197  		ServerName:              serverName,
   198  		InsecureSkipVerify:      insecureSkipVerify,
   199  		NewConnectionPerRequest: newConnectionPerRequest,
   200  		ForceDNSLookup:          forceDNSLookup,
   201  	}
   202  	if len(hboneAddress) > 0 {
   203  		request.Hbone = &proto.HBONE{
   204  			Address:            hboneAddress,
   205  			CertFile:           hboneClientCert,
   206  			KeyFile:            hboneClientKey,
   207  			CaCertFile:         hboneCaFile,
   208  			InsecureSkipVerify: hboneInsecureSkipVerify,
   209  		}
   210  		for _, header := range hboneHeaders {
   211  			parts := strings.SplitN(header, ":", 2)
   212  			// require name:value format
   213  			if len(parts) != 2 {
   214  				return nil, fmt.Errorf("invalid header format: %q (want name:value)", header)
   215  			}
   216  
   217  			request.Hbone.Headers = append(request.Hbone.Headers, &proto.Header{
   218  				Key:   parts[0],
   219  				Value: strings.Trim(parts[1], " "),
   220  			})
   221  		}
   222  	}
   223  
   224  	if expectSet {
   225  		request.ExpectedResponse = &wrappers.StringValue{Value: expect}
   226  	}
   227  
   228  	if alpn != nil {
   229  		request.Alpn = &proto.Alpn{Value: alpn}
   230  	}
   231  
   232  	for _, header := range headers {
   233  		parts := strings.Split(header, ":")
   234  
   235  		// require name:value format
   236  		if len(parts) != 2 {
   237  			return nil, fmt.Errorf("invalid header format: %q (want name:value)", header)
   238  		}
   239  
   240  		request.Headers = append(request.Headers, &proto.Header{
   241  			Key:   parts[0],
   242  			Value: strings.Trim(parts[1], " "),
   243  		})
   244  	}
   245  
   246  	if clientCert != "" && clientKey != "" {
   247  		request.CertFile = clientCert
   248  		request.KeyFile = clientKey
   249  	}
   250  	if caFile != "" {
   251  		request.CaCertFile = caFile
   252  	}
   253  	return request, nil
   254  }
   255  
   256  func main() {
   257  	if err := rootCmd.Execute(); err != nil {
   258  		os.Exit(-1)
   259  	}
   260  }