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 }