diff --git a/csds-client/README.md b/csds-client/README.md
index 4157090..d01299e 100644
--- a/csds-client/README.md
+++ b/csds-client/README.md
@@ -1,7 +1,10 @@
# CSDS Client
[Client status discovery service (CSDS)](https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/status/v3/csds.proto) is a generic xDS API that can be used to get information about data plane clients from the control plane’s point of view. It is useful to enhance debuggability of the service mesh, where lots of xDS clients are connected to the control plane.
The CSDS client is developed as a generic tool that can be used/extended to work with different xDS control planes.
-For now, this initial version of this CSDS client only supports GCP's [Traffic Director](https://cloud.google.com/traffic-director).
+For now, this initial version of this CSDS client only supports:
+* GCP's [Traffic Director](https://cloud.google.com/traffic-director).
+* Envoy's [go-control-plane](https://github.com/envoyproxy/go-control-plane) based implementations.
+
Before you start, you'll need [Go](https://golang.org/) installed.
# Building
@@ -79,4 +82,4 @@ Client ID xDS stream type Config Status
)
OR
(Config has been saved to )
-```
\ No newline at end of file
+```
diff --git a/csds-client/client/client.go b/csds-client/client/client.go
index 1aec299..50d2cfe 100644
--- a/csds-client/client/client.go
+++ b/csds-client/client/client.go
@@ -4,21 +4,32 @@ import (
"time"
)
+// Platforms supported by client implementations
+// where `go` is an xDS implementation based on
+// https://github.com/envoyproxy/go-control-plane
+var (
+ SupportedPlatforms []string = []string{"gcp", "go"}
+)
+
// ClientOptions are options that are common to use in all the version implementations of client
// TODO: If ClientOptions will no longer be common to use in all the version, it will need to be
// implemented in version packages
type ClientOptions struct {
- Uri string
- Platform string
- AuthnMode string
- RequestFile string
- RequestYaml string
- Jwt string
- ConfigFile string
- MonitorInterval time.Duration
- Visualization bool
- FilterMode string
- FilterPattern string
+ Uri string
+ Authority string
+ Platform string
+ AuthnMode string
+ RequestFile string
+ RequestYaml string
+ Jwt string
+ ConfigFile string
+ MonitorInterval time.Duration
+ Visualization bool
+ FilterMode string
+ FilterPattern string
+ TLSCertFilepath string
+ TLSPrivateKeyFilepath string
+ TLSCACertsFilepath string
}
// Client implements CSDS Client of a particular version. Upon creation of the new client it is
diff --git a/csds-client/client/util/util.go b/csds-client/client/util/util.go
index d380703..71cae13 100644
--- a/csds-client/client/util/util.go
+++ b/csds-client/client/util/util.go
@@ -3,6 +3,7 @@ package util
import (
"bytes"
"context"
+ "crypto/tls"
"crypto/x509"
"encoding/json"
"envoy-tools/csds-client/client"
@@ -33,6 +34,7 @@ import (
envoy_extensions_accesslog_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/file/v3"
envoy_extensions_filters_http_cors_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cors/v3"
envoy_extensions_filters_http_fault_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/fault/v3"
+ envoy_extensions_filters_http_jwt_authn_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3"
envoy_extensions_filters_http_router_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3"
envoy_extensions_filters_network_http_connection_manager_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
"github.com/ghodss/yaml"
@@ -125,6 +127,10 @@ func (r *TypeResolver) FindMessageByURL(url string) (protoreflect.MessageType, e
case "type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog":
fileAccessLog := envoy_extensions_accesslog_v3.FileAccessLog{}
return fileAccessLog.ProtoReflect().Type(), nil
+ case "type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication":
+ jwtAuthn := envoy_extensions_filters_http_jwt_authn_v3.JwtAuthentication{}
+ return jwtAuthn.ProtoReflect().Type(), nil
+
default:
dummy := anypb.Any{}
return dummy.ProtoReflect().Type(), nil
@@ -423,6 +429,58 @@ func ConnToGCPWithAuto(uri string) (*grpc.ClientConn, error) {
return clientConn, nil
}
+// ConnToXDS connects to the the given gRPC target with the provided
+// DialOptions.
+func ConnToXDS(uri string, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
+ return grpc.Dial(uri, opts...)
+}
+
+// DialOptions parses client.ClientOptions and sets up grpc DialOption
+// to use when dialing a gRPC service.
+//
+// If no tls configuration was provided, it defaults to disabling gRPC transport
+// security.
+//
+// Otherwise, the tls configuration is used to build a TLS config to use on
+// the client connection.
+//
+// The optional HTTP/2 :authority header is set if `opts.Authority` is
+// provided.
+// The same value is also used in the ClientHello message for SNI, and to
+// verify the server name on the returned server certificate.
+func DialOptions(opts *client.ClientOptions) ([]grpc.DialOption, error) {
+ var dialOpts []grpc.DialOption
+ if opts.TLSCertFilepath != "" && opts.TLSPrivateKeyFilepath != "" {
+ clientCert, err := tls.LoadX509KeyPair(opts.TLSCertFilepath, opts.TLSPrivateKeyFilepath)
+ if err != nil {
+ return nil, fmt.Errorf("could not load client cert/key pair: %v", err)
+ }
+ tlsCfg := &tls.Config{
+ Certificates: []tls.Certificate{clientCert},
+ }
+ // Load root CAs to use when verifying server certificate.
+ if opts.TLSCACertsFilepath != "" {
+ caCertsData, err := ioutil.ReadFile(opts.TLSCACertsFilepath)
+ if err != nil {
+ return nil, fmt.Errorf("could not read CA certs file: %v", err)
+ }
+ caCertsPool := x509.NewCertPool()
+ caCertsPool.AppendCertsFromPEM(caCertsData)
+ tlsCfg.RootCAs = caCertsPool
+ if opts.Authority != "" {
+ tlsCfg.ServerName = opts.Authority
+ }
+ }
+ dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)))
+ } else {
+ dialOpts = append(dialOpts, grpc.WithInsecure())
+ if opts.Authority != "" {
+ dialOpts = append(dialOpts, grpc.WithAuthority(opts.Authority))
+ }
+ }
+ return dialOpts, nil
+}
+
// ParseYamlFileToMap parses yaml file to map
func ParseYamlFileToMap(path string) (map[string]interface{}, error) {
// parse yaml to json
diff --git a/csds-client/client/v2/client.go b/csds-client/client/v2/client.go
index fe9354b..81567a7 100644
--- a/csds-client/client/v2/client.go
+++ b/csds-client/client/v2/client.go
@@ -60,8 +60,9 @@ func (c *ClientV2) parseNodeMatcher() error {
return fmt.Errorf("missing field %v in NodeMatcher", key)
}
}
+ case "go":
default:
- return fmt.Errorf("%s platform is not supported, list of supported platforms: gcp", c.opts.Platform)
+ return fmt.Errorf("%s platform is not supported. supported platforms: %v", c.opts.Platform, client.SupportedPlatforms)
}
if c.opts.FilterMode != "" && c.opts.FilterMode != "prefix" && c.opts.FilterMode != "suffix" && c.opts.FilterMode != "regex" {
@@ -99,6 +100,13 @@ func (c *ClientV2) connWithAuth() error {
return err
}
return nil
+ case "go":
+ dialOpts, err := clientutil.DialOptions(&c.opts)
+ if err != nil {
+ return fmt.Errorf("could not build gRPC client dial options: %v", err)
+ }
+ c.clientConn, err = grpc.Dial(c.opts.Uri, dialOpts...)
+ return err
default:
return errors.New("auto authentication mode for this platform is not supported. Please use jwt_file instead")
}
diff --git a/csds-client/client/v3/client.go b/csds-client/client/v3/client.go
index 2641896..d9951c5 100644
--- a/csds-client/client/v3/client.go
+++ b/csds-client/client/v3/client.go
@@ -60,8 +60,9 @@ func (c *ClientV3) parseNodeMatcher() error {
return fmt.Errorf("missing field %v in NodeMatcher", key)
}
}
+ case "go":
default:
- return fmt.Errorf("%s platform is not supported, list of supported platforms: gcp", c.opts.Platform)
+ return fmt.Errorf("%s platform is not supported. supported platforms: %v", c.opts.Platform, client.SupportedPlatforms)
}
if c.opts.FilterMode != "" && c.opts.FilterMode != "prefix" && c.opts.FilterMode != "suffix" && c.opts.FilterMode != "regex" {
@@ -84,7 +85,7 @@ func (c *ClientV3) connWithAuth() error {
}
return nil
default:
- return fmt.Errorf("%s platform is not supported, list of supported platforms: gcp", c.opts.Platform)
+ return fmt.Errorf("%s platform is not supported for jwt authentication, list of supported platforms: gcp", c.opts.Platform)
}
case "auto":
@@ -99,6 +100,13 @@ func (c *ClientV3) connWithAuth() error {
return err
}
return nil
+ case "go":
+ dialOpts, err := clientutil.DialOptions(&c.opts)
+ if err != nil {
+ return fmt.Errorf("could not build gRPC client dial options: %v", err)
+ }
+ c.clientConn, err = grpc.Dial(c.opts.Uri, dialOpts...)
+ return err
default:
return errors.New("auto authentication mode for this platform is not supported. Please use jwt_file instead")
}
@@ -112,8 +120,8 @@ func New(option client.ClientOptions) (*ClientV3, error) {
c := &ClientV3{
opts: option,
}
- if c.opts.Platform != "gcp" {
- return nil, fmt.Errorf("%s platform is not supported, list of supported platforms: gcp", c.opts.Platform)
+ if c.opts.Platform != "gcp" && c.opts.Platform != "go" {
+ return nil, fmt.Errorf("%s platform is not supported, list of supported platforms: [gcp, go]", c.opts.Platform)
}
if err := c.parseNodeMatcher(); err != nil {
@@ -218,9 +226,8 @@ func printOutResponse(response *csdspb_v3.ClientStatusResponse, opts client.Clie
if response.GetConfig() == nil || len(response.GetConfig()) == 0 {
fmt.Printf("No xDS clients connected.\n")
return nil
- } else {
- fmt.Printf("%-50s %-30s %-30s \n", "Client ID", "xDS stream type", "Config Status")
}
+ fmt.Printf("%-50s %-30s %-30s \n", "Client ID", "xDS stream type", "Config Status")
var hasXdsConfig bool
diff --git a/csds-client/main.go b/csds-client/main.go
index 7b8d75d..2a69496 100644
--- a/csds-client/main.go
+++ b/csds-client/main.go
@@ -5,12 +5,14 @@ import (
client_v2 "envoy-tools/csds-client/client/v2"
client_v3 "envoy-tools/csds-client/client/v3"
"flag"
+ "fmt"
"log"
"time"
)
// flag vars
var uri string
+var authority string
var platform string
var authnMode string
var apiVersion string
@@ -22,10 +24,14 @@ var monitorInterval time.Duration
var visualization bool
var filterMode string
var filterPattern string
+var tlsCertFilepath string
+var tlsCACertsFilepath string
+var tlsPrivateKeyFilepath string
// const default values for flag vars
const (
uriDefault string = "trafficdirector.googleapis.com:443"
+ authorityDefault string = ""
platformDefault string = "gcp"
authnModeDefault string = "auto"
apiVersionDefault string = "v2"
@@ -37,12 +43,16 @@ const (
visualizationDefault bool = false
filterModeDefault string = ""
filterPatternDefault string = ""
+ TLSCertFilepath string = ""
+ TLSPrivateKeyFilepath string = ""
+ TLSCACertsFilepath string = ""
)
// init binds flags with variables
func init() {
flag.StringVar(&uri, "service_uri", uriDefault, "the uri of the service to connect to")
- flag.StringVar(&platform, "platform", platformDefault, "the platform (e.g. gcp, aws, ...)")
+ flag.StringVar(&authority, "authority", authorityDefault, "the :authority header to use when connecting to uri")
+ flag.StringVar(&platform, "platform", platformDefault, fmt.Sprintf("the target platform, one of %v", client.SupportedPlatforms))
flag.StringVar(&authnMode, "authn_mode", authnModeDefault, "the method to use for authentication (e.g. auto, jwt, ...)")
flag.StringVar(&apiVersion, "api_version", apiVersionDefault, "which xds api major version to use (e.g. v2, v3, ...)")
flag.StringVar(&requestFile, "request_file", requestFileDefault, "yaml file that defines the csds request")
@@ -53,23 +63,30 @@ func init() {
flag.BoolVar(&visualization, "visualization", visualizationDefault, "option to visualize the relationship between xDS")
flag.StringVar(&filterMode, "filter_mode", filterModeDefault, "the filter mode for the filter on xDS nodes to be returned (e.g. prefix, suffix, regex, ...)")
flag.StringVar(&filterPattern, "filter_pattern", filterPatternDefault, "the filter pattern for the filter on xDS nodes to be returned")
+ flag.StringVar(&tlsCertFilepath, "cert", tlsCertFilepath, "filepath to the client TLS certificate")
+ flag.StringVar(&tlsPrivateKeyFilepath, "key", tlsPrivateKeyFilepath, "filepath to the client TLS private key")
+ flag.StringVar(&tlsCACertsFilepath, "cacert", tlsCACertsFilepath, "filepath to the CAs certs used to verify the server's certificate")
}
func main() {
flag.Parse()
clientOpts := client.ClientOptions{
- Uri: uri,
- Platform: platform,
- AuthnMode: authnMode,
- RequestFile: requestFile,
- RequestYaml: requestYaml,
- Jwt: jwt,
- ConfigFile: configFile,
- MonitorInterval: monitorInterval,
- Visualization: visualization,
- FilterMode: filterMode,
- FilterPattern: filterPattern,
+ Uri: uri,
+ Authority: authority,
+ Platform: platform,
+ AuthnMode: authnMode,
+ RequestFile: requestFile,
+ RequestYaml: requestYaml,
+ Jwt: jwt,
+ ConfigFile: configFile,
+ MonitorInterval: monitorInterval,
+ Visualization: visualization,
+ FilterMode: filterMode,
+ FilterPattern: filterPattern,
+ TLSCertFilepath: tlsCertFilepath,
+ TLSCACertsFilepath: tlsCACertsFilepath,
+ TLSPrivateKeyFilepath: tlsPrivateKeyFilepath,
}
var c client.Client