github.com/projectcontour/contour@v1.28.2/cmd/contour/cli.go (about)

     1  // Copyright Project Contour Authors
     2  // Licensed under the Apache License, Version 2.0 (the "License");
     3  // you may not use this file except in compliance with the License.
     4  // You may obtain a copy of the License at
     5  //
     6  //     http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package main
    15  
    16  import (
    17  	"context"
    18  	"crypto/tls"
    19  	"crypto/x509"
    20  	"fmt"
    21  	"os"
    22  
    23  	"github.com/alecthomas/kingpin/v2"
    24  	corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    25  	envoy_service_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/service/cluster/v3"
    26  	envoy_discovery_v3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    27  	envoy_service_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/service/endpoint/v3"
    28  	envoy_service_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/service/listener/v3"
    29  	envoy_service_route_v3 "github.com/envoyproxy/go-control-plane/envoy/service/route/v3"
    30  	"github.com/sirupsen/logrus"
    31  	grpc_code "google.golang.org/genproto/googleapis/rpc/code"
    32  	"google.golang.org/genproto/googleapis/rpc/status"
    33  	"google.golang.org/grpc"
    34  	"google.golang.org/grpc/credentials"
    35  	"google.golang.org/grpc/credentials/insecure"
    36  	"google.golang.org/protobuf/encoding/protojson"
    37  )
    38  
    39  // registerCli registers the cli subcommand and flags
    40  // with the Application provided.
    41  func registerCli(app *kingpin.Application, log *logrus.Logger) (*kingpin.CmdClause, *Client) {
    42  	client := Client{Log: log}
    43  
    44  	cli := app.Command("cli", "A CLI client for the Contour Kubernetes ingress controller.")
    45  	cli.Flag("cafile", "CA bundle file for connecting to a TLS-secured Contour.").Envar("CLI_CAFILE").StringVar(&client.CAFile)
    46  	cli.Flag("cert-file", "Client certificate file for connecting to a TLS-secured Contour.").Envar("CLI_CERT_FILE").StringVar(&client.ClientCert)
    47  	cli.Flag("contour", "Contour host:port.").Default("127.0.0.1:8001").StringVar(&client.ContourAddr)
    48  	cli.Flag("delta", "Use incremental xDS.").BoolVar(&client.Delta)
    49  	cli.Flag("key-file", "Client key file for connecting to a TLS-secured Contour.").Envar("CLI_KEY_FILE").StringVar(&client.ClientKey)
    50  	cli.Flag("nack", "NACK all responses (for testing).").BoolVar(&client.Nack)
    51  	cli.Flag("node-id", "Node ID for the CLI client to use.").Envar("CLI_NODE_ID").Default("ContourCLI").StringVar(&client.NodeID)
    52  
    53  	return cli, &client
    54  }
    55  
    56  // Client holds the details for the cli client to connect to.
    57  // TODO(youngnick): Move NACK handling to a sentinel, either file or keystroke.
    58  type Client struct {
    59  	ContourAddr string
    60  	CAFile      string
    61  	ClientCert  string
    62  	ClientKey   string
    63  	Nack        bool
    64  	Delta       bool
    65  	NodeID      string
    66  	Log         *logrus.Logger
    67  }
    68  
    69  func (c *Client) dial() *grpc.ClientConn {
    70  	var options []grpc.DialOption
    71  
    72  	// Check the TLS setup
    73  	switch {
    74  	case c.CAFile != "" || c.ClientCert != "" || c.ClientKey != "":
    75  		// If one of the three TLS commands is not empty, they all must be not empty
    76  		if !(c.CAFile != "" && c.ClientCert != "" && c.ClientKey != "") {
    77  			kingpin.Fatalf("you must supply all three TLS parameters - --cafile, --cert-file, --key-file, or none of them")
    78  		}
    79  		// Load the client certificates from disk
    80  		certificate, err := tls.LoadX509KeyPair(c.ClientCert, c.ClientKey)
    81  		if err != nil {
    82  			c.Log.WithError(err).Fatal("failed to load certificates from disk")
    83  		}
    84  		// Create a certificate pool from the certificate authority
    85  		certPool := x509.NewCertPool()
    86  		ca, err := os.ReadFile(c.CAFile)
    87  		if err != nil {
    88  			c.Log.WithError(err).Fatal("failed to read CA cert")
    89  		}
    90  
    91  		// Append the certificates from the CA
    92  		if ok := certPool.AppendCertsFromPEM(ca); !ok {
    93  			// TODO(nyoung) OMG yuck, thanks for this, crypto/tls. Suggestions on alternates welcomed.
    94  			c.Log.Fatal("failed to append CA certs")
    95  		}
    96  
    97  		creds := credentials.NewTLS(&tls.Config{
    98  			// TODO(youngnick): Does this need to be defaulted with a cli flag to
    99  			// override?
   100  			// The ServerName here needs to be one of the SANs available in
   101  			// the serving cert used by contour serve.
   102  			ServerName:   "contour",
   103  			Certificates: []tls.Certificate{certificate},
   104  			RootCAs:      certPool,
   105  			MinVersion:   tls.VersionTLS12,
   106  		})
   107  		options = append(options, grpc.WithTransportCredentials(creds))
   108  	default:
   109  		options = append(options, grpc.WithTransportCredentials(insecure.NewCredentials()))
   110  	}
   111  
   112  	conn, err := grpc.Dial(c.ContourAddr, options...)
   113  	if err != nil {
   114  		c.Log.WithError(err).Fatal("failed connecting Contour Server")
   115  	}
   116  
   117  	return conn
   118  }
   119  
   120  // ClusterStream returns a stream of Clusters using the config in the Client.
   121  func (c *Client) ClusterStream() envoy_service_cluster_v3.ClusterDiscoveryService_StreamClustersClient {
   122  	stream, err := envoy_service_cluster_v3.NewClusterDiscoveryServiceClient(c.dial()).StreamClusters(context.Background())
   123  	if err != nil {
   124  		c.Log.WithError(err).Fatal("failed to fetch stream of Clusters")
   125  	}
   126  	return stream
   127  }
   128  
   129  // EndpointStream returns a stream of Endpoints using the config in the Client.
   130  func (c *Client) EndpointStream() envoy_service_endpoint_v3.EndpointDiscoveryService_StreamEndpointsClient {
   131  	stream, err := envoy_service_endpoint_v3.NewEndpointDiscoveryServiceClient(c.dial()).StreamEndpoints(context.Background())
   132  	if err != nil {
   133  		c.Log.WithError(err).Fatal("failed to fetch stream of Endpoints")
   134  	}
   135  	return stream
   136  }
   137  
   138  // ListenerStream returns a stream of Listeners using the config in the Client.
   139  func (c *Client) ListenerStream() envoy_service_listener_v3.ListenerDiscoveryService_StreamListenersClient {
   140  	stream, err := envoy_service_listener_v3.NewListenerDiscoveryServiceClient(c.dial()).StreamListeners(context.Background())
   141  	if err != nil {
   142  		c.Log.WithError(err).Fatal("failed to fetch stream of Listeners")
   143  	}
   144  	return stream
   145  }
   146  
   147  // RouteStream returns a stream of Routes using the config in the Client.
   148  func (c *Client) RouteStream() envoy_service_route_v3.RouteDiscoveryService_StreamRoutesClient {
   149  	stream, err := envoy_service_route_v3.NewRouteDiscoveryServiceClient(c.dial()).StreamRoutes(context.Background())
   150  	if err != nil {
   151  		c.Log.WithError(err).Fatal("failed to fetch stream of Routes")
   152  	}
   153  	return stream
   154  }
   155  
   156  type stream interface {
   157  	Send(*envoy_discovery_v3.DiscoveryRequest) error
   158  	Recv() (*envoy_discovery_v3.DiscoveryResponse, error)
   159  }
   160  
   161  func watchstream(log *logrus.Logger, st stream, typeURL string, resources []string, nack bool, nodeID string) {
   162  	m := protojson.MarshalOptions{
   163  		Multiline:     true,
   164  		Indent:        "  ",
   165  		UseProtoNames: true,
   166  	}
   167  
   168  	currentVersion := "0"
   169  
   170  	// Send the initial, non-ACK discovery request.
   171  	req := &envoy_discovery_v3.DiscoveryRequest{
   172  		TypeUrl:       typeURL,
   173  		ResourceNames: resources,
   174  		VersionInfo:   currentVersion,
   175  		Node: &corev3.Node{
   176  			Id: nodeID,
   177  		},
   178  	}
   179  	log.WithField("currentVersion", currentVersion).Info("Sending discover request")
   180  	fmt.Println(m.Format(req))
   181  	err := st.Send(req)
   182  	if err != nil {
   183  		log.WithError(err).Fatal("failed to send Discover Request")
   184  	}
   185  
   186  	for {
   187  
   188  		// Wait until we receive a response to our request.
   189  		resp, err := st.Recv()
   190  		if err != nil {
   191  			log.WithError(err).Fatal("failed to receive response for Discover Request")
   192  		}
   193  		log.WithField("currentVersion", currentVersion).
   194  			WithField("resp_version_info", resp.VersionInfo).
   195  			WithField("nonce", resp.Nonce).
   196  			Info("Received Discovery Response")
   197  
   198  		fmt.Println(m.Format(resp))
   199  		if err != nil {
   200  			log.WithError(err).Fatal("failed to marshal Discovery Response")
   201  		}
   202  
   203  		currentVersion = resp.VersionInfo
   204  
   205  		if nack {
   206  			// We'll NACK the response we just got.
   207  			// The ResponseNonce field is what makes it an ACK,
   208  			// and the VersionInfo field must match the one in the response we
   209  			// just got, or else the watch won't happen properly.
   210  			// The ErrorDetail field being populated is what makes this a NACK
   211  			// instead of an ACK.
   212  			nackReq := &envoy_discovery_v3.DiscoveryRequest{
   213  				TypeUrl:       typeURL,
   214  				ResponseNonce: resp.Nonce,
   215  				VersionInfo:   resp.VersionInfo,
   216  				ErrorDetail: &status.Status{
   217  					Code:    int32(grpc_code.Code_INTERNAL),
   218  					Message: "Told to create a NACK for testing",
   219  				},
   220  				Node: &corev3.Node{
   221  					Id: nodeID,
   222  				},
   223  			}
   224  			log.WithField("response_nonce", resp.Nonce).
   225  				WithField("version_info", resp.VersionInfo).
   226  				WithField("currentVersion", currentVersion).
   227  				Info("Sending NACK discover request")
   228  
   229  			fmt.Println(m.Format(nackReq))
   230  			err := st.Send(nackReq)
   231  			if err != nil {
   232  				log.WithError(err).Fatal("failed to send NACK Discover Request")
   233  			}
   234  
   235  		} else {
   236  			// We'll ACK our request.
   237  			// The ResponseNonce field is what makes it an ACK,
   238  			// and the VersionInfo field must match the one in the response we
   239  			// just got, or else the watch won't happen properly.
   240  			ackReq := &envoy_discovery_v3.DiscoveryRequest{
   241  				TypeUrl:       typeURL,
   242  				ResponseNonce: resp.Nonce,
   243  				VersionInfo:   resp.VersionInfo,
   244  				Node: &corev3.Node{
   245  					Id: nodeID,
   246  				},
   247  			}
   248  			log.WithField("response_nonce", resp.Nonce).
   249  				WithField("version_info", resp.VersionInfo).
   250  				WithField("currentVersion", currentVersion).
   251  				Info("Sending ACK discover request")
   252  			fmt.Println(m.Format(ackReq))
   253  			err := st.Send(ackReq)
   254  			if err != nil {
   255  				log.WithError(err).Fatal("failed to send ACK Discover Request")
   256  			}
   257  
   258  		}
   259  	}
   260  }
   261  
   262  // ClusterStream returns a stream of Clusters using the config in the Client.
   263  func (c *Client) DeltaClusterStream() envoy_service_cluster_v3.ClusterDiscoveryService_DeltaClustersClient {
   264  	stream, err := envoy_service_cluster_v3.NewClusterDiscoveryServiceClient(c.dial()).DeltaClusters(context.Background())
   265  	if err != nil {
   266  		c.Log.WithError(err).Fatal("failed to fetch incremental stream of Clusters")
   267  	}
   268  	return stream
   269  }
   270  
   271  // EndpointStream returns a stream of Endpoints using the config in the Client.
   272  func (c *Client) DeltaEndpointStream() envoy_service_endpoint_v3.EndpointDiscoveryService_DeltaEndpointsClient {
   273  	stream, err := envoy_service_endpoint_v3.NewEndpointDiscoveryServiceClient(c.dial()).DeltaEndpoints(context.Background())
   274  	if err != nil {
   275  		c.Log.WithError(err).Fatal("failed to fetch incremental stream of Endpoints")
   276  	}
   277  	return stream
   278  }
   279  
   280  // ListenerStream returns a stream of Listeners using the config in the Client.
   281  func (c *Client) DeltaListenerStream() envoy_service_listener_v3.ListenerDiscoveryService_DeltaListenersClient {
   282  	stream, err := envoy_service_listener_v3.NewListenerDiscoveryServiceClient(c.dial()).DeltaListeners(context.Background())
   283  	if err != nil {
   284  		c.Log.WithError(err).Fatal("failed to fetch incremental stream of Listeners")
   285  	}
   286  	return stream
   287  }
   288  
   289  // RouteStream returns a stream of Routes using the config in the Client.
   290  func (c *Client) DeltaRouteStream() envoy_service_route_v3.RouteDiscoveryService_DeltaRoutesClient {
   291  	stream, err := envoy_service_route_v3.NewRouteDiscoveryServiceClient(c.dial()).DeltaRoutes(context.Background())
   292  	if err != nil {
   293  		c.Log.WithError(err).Fatal("failed to fetch incremental stream of Routes")
   294  	}
   295  	return stream
   296  }
   297  
   298  type deltaStream interface {
   299  	Send(*envoy_discovery_v3.DeltaDiscoveryRequest) error
   300  	Recv() (*envoy_discovery_v3.DeltaDiscoveryResponse, error)
   301  }
   302  
   303  func watchDeltaStream(log *logrus.Logger, st deltaStream, typeURL string, resources []string, nack bool, nodeID string) {
   304  	m := protojson.MarshalOptions{
   305  		Multiline:     true,
   306  		Indent:        "  ",
   307  		UseProtoNames: true,
   308  	}
   309  
   310  	currentVersion := "0"
   311  
   312  	// Send the initial, non-ACK discovery request.
   313  	req := &envoy_discovery_v3.DeltaDiscoveryRequest{
   314  		TypeUrl:                typeURL,
   315  		ResourceNamesSubscribe: resources,
   316  		Node: &corev3.Node{
   317  			Id: nodeID,
   318  		},
   319  	}
   320  	log.WithField("currentVersion", currentVersion).Info("Sending incremental discover request")
   321  	fmt.Println(m.Format(req))
   322  	err := st.Send(req)
   323  	if err != nil {
   324  		log.WithError(err).Fatal("failed to send incremental Discover Request")
   325  	}
   326  
   327  	for {
   328  
   329  		// Wait until we receive a response to our request.
   330  		resp, err := st.Recv()
   331  		if err != nil {
   332  			log.WithError(err).Fatal("failed to receive response for incremental Discover Request")
   333  		}
   334  		log.WithField("currentVersion", currentVersion).
   335  			WithField("resp_system_version_info", resp.SystemVersionInfo).
   336  			WithField("nonce", resp.Nonce).
   337  			Info("Received Discovery Response")
   338  
   339  		fmt.Println(m.Format(resp))
   340  		if err != nil {
   341  			log.WithError(err).Fatal("failed to marshal incremental Discovery Response")
   342  		}
   343  
   344  		currentVersion = resp.SystemVersionInfo
   345  
   346  		if nack {
   347  			// We'll NACK the response we just got.
   348  			// The ResponseNonce field is what makes it an ACK.
   349  			// The ErrorDetail field being populated is what makes this a NACK
   350  			// instead of an ACK.
   351  			nackReq := &envoy_discovery_v3.DeltaDiscoveryRequest{
   352  				TypeUrl:       typeURL,
   353  				ResponseNonce: resp.Nonce,
   354  				ErrorDetail: &status.Status{
   355  					Code:    int32(grpc_code.Code_INTERNAL),
   356  					Message: "Told to create a NACK for testing",
   357  				},
   358  				Node: &corev3.Node{
   359  					Id: nodeID,
   360  				},
   361  			}
   362  			log.WithField("response_nonce", resp.Nonce).
   363  				WithField("version_info", resp.SystemVersionInfo).
   364  				WithField("currentVersion", currentVersion).
   365  				Info("Sending incremental NACK discover request")
   366  
   367  			fmt.Println(m.Format(nackReq))
   368  			err := st.Send(nackReq)
   369  			if err != nil {
   370  				log.WithError(err).Fatal("failed to send NACK Discover Request")
   371  			}
   372  
   373  		} else {
   374  			// We'll ACK our request.
   375  			// The ResponseNonce field is what makes it an ACK.
   376  			ackReq := &envoy_discovery_v3.DeltaDiscoveryRequest{
   377  				TypeUrl:       typeURL,
   378  				ResponseNonce: resp.Nonce,
   379  				Node: &corev3.Node{
   380  					Id: nodeID,
   381  				},
   382  			}
   383  			log.WithField("response_nonce", resp.Nonce).
   384  				WithField("version_info", resp.SystemVersionInfo).
   385  				WithField("currentVersion", currentVersion).
   386  				Info("Sending incremental ACK discover request")
   387  			fmt.Println(m.Format(ackReq))
   388  			err := st.Send(ackReq)
   389  			if err != nil {
   390  				log.WithError(err).Fatal("failed to send ACK incremental Discover Request")
   391  			}
   392  
   393  		}
   394  	}
   395  }