pkg/mesh/grpc_bootstrap.go (157 lines of code) (raw):

// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package mesh import ( "encoding/json" "fmt" "io/ioutil" "os" "path" "time" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/structpb" ) // WIP: copied from istio agent, to allow proxyless gRPC to start without a dependency to the full pilot-agent and // without the XDS proxy. This still depends on finishing up direct integration with the CA. // Note that gRPC does not support using custom SAN for the XDS server - MCP will work since it has real certs. // For in-cluster you need a proper gateway with real certs exposing Istiod. The east/west gateway (hgate) can handle // this, but with CertManager or some other tool creating the certs and with a DNS entry. A Serverless gateway can // also provide a similar domain+certificates. const ( ServerListenerNamePrefix = "xds.istio.io/grpc/lds/inbound/" // ServerListenerNameTemplate for the name of the Listener resource to subscribe to for a gRPC // server. If the token `%s` is present in the string, all instances of the // token will be replaced with the server's listening "IP:port" (e.g., // "0.0.0.0:8080", "[::]:8080"). ServerListenerNameTemplate = ServerListenerNamePrefix + "%s" ) // Bootstrap contains the general structure of what's expected by GRPC's XDS implementation. // See https://github.com/grpc/grpc-go/blob/master/xds/internal/xdsclient/bootstrap/bootstrap.go // TODO use structs from gRPC lib if created/exported type Bootstrap struct { XDSServers []XdsServer `json:"xds_servers,omitempty"` Node *Node `json:"node,omitempty"` CertProviders map[string]CertificateProvider `json:"certificate_providers,omitempty"` ServerListenerNameTemplate string `json:"server_listener_resource_name_template,omitempty"` } type ChannelCreds struct { Type string `json:"type,omitempty"` Config interface{} `json:"config,omitempty"` } type XdsServer struct { ServerURI string `json:"server_uri,omitempty"` ChannelCreds []ChannelCreds `json:"channel_creds,omitempty"` ServerFeatures []string `json:"server_features,omitempty"` } type CertificateProvider struct { PluginName string `json:"plugin_name,omitempty"` Config interface{} `json:"config,omitempty"` } const FileWatcherCertProviderName = "file_watcher" type FileWatcherCertProviderConfig struct { CertificateFile string `json:"certificate_file,omitempty"` PrivateKeyFile string `json:"private_key_file,omitempty"` CACertificateFile string `json:"ca_certificate_file,omitempty"` RefreshDuration json.RawMessage `json:"refresh_interval,omitempty"` } func (c *FileWatcherCertProviderConfig) FilePaths() []string { return []string{c.CertificateFile, c.PrivateKeyFile, c.CACertificateFile} } // FileWatcherProvider returns the FileWatcherCertProviderConfig if one exists in CertProviders func (b *Bootstrap) FileWatcherProvider() *FileWatcherCertProviderConfig { if b == nil || b.CertProviders == nil { return nil } for _, provider := range b.CertProviders { if provider.PluginName == FileWatcherCertProviderName { cfg, ok := provider.Config.(FileWatcherCertProviderConfig) if !ok { return nil } return &cfg } } return nil } // LoadBootstrap loads a Bootstrap from the given file path. func LoadBootstrap(file string) (*Bootstrap, error) { data, err := os.ReadFile(file) if err != nil { return nil, err } b := &Bootstrap{} if err := json.Unmarshal(data, b); err != nil { return nil, err } return b, err } // Duplicated from github.com/envoyproxy/go-control-plane/envoy/config/core/v3 // to avoid deps to large package. Only what we use. type Node struct { Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` Locality *Locality `protobuf:"bytes,4,opt,name=locality,proto3" json:"locality,omitempty"` Metadata *structpb.Struct `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"` } type GenerateBootstrapOptions struct { Node *Node XdsUdsPath string DiscoveryAddress string CertDir string } type Locality struct { // Region this :ref:`zone <envoy_api_field_config.core.v3.Locality.zone>` belongs to. Region string `protobuf:"bytes,1,opt,name=region,proto3" json:"region,omitempty"` // Defines the local service zone where Envoy is running. Though optional, it // should be set if discovery service routing is used and the discovery // service exposes :ref:`zone data <envoy_api_field_config.endpoint.v3.LocalityLbEndpoints.locality>`, // either in this message or via :option:`--service-zone`. The meaning of zone // is context dependent, e.g. `Availability Zone (AZ) // <https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html>`_ // on AWS, `Zone <https://cloud.google.com/compute/docs/regions-zones/>`_ on // GCP, etc. Zone string `protobuf:"bytes,2,opt,name=zone,proto3" json:"zone,omitempty"` // When used for locality of upstream hosts, this field further splits zone // into smaller chunks of sub-zones so they can be load balanced // independently. SubZone string `protobuf:"bytes,3,opt,name=sub_zone,json=subZone,proto3" json:"sub_zone,omitempty"` } // GenerateBootstrap generates the bootstrap structure for gRPC XDS integration. func GenerateBootstrap(opts GenerateBootstrapOptions, meta map[string]string) (*Bootstrap, error) { xdsMeta, err := extractMeta(meta) if err != nil { return nil, fmt.Errorf("failed extracting xds metadata: %v", err) } // TODO direct to CP should use secure channel (most likely JWT + TLS, but possibly allow mTLS) serverURI := opts.DiscoveryAddress if opts.XdsUdsPath != "" { serverURI = fmt.Sprintf("unix:///%s", opts.XdsUdsPath) } bootstrap := Bootstrap{ XDSServers: []XdsServer{{ ServerURI: serverURI, // connect locally via agent ChannelCreds: []ChannelCreds{{Type: "insecure"}}, ServerFeatures: []string{"xds_v3"}, }}, Node: &Node{ Id: opts.Node.Id, Locality: opts.Node.Locality, Metadata: xdsMeta, }, ServerListenerNameTemplate: ServerListenerNameTemplate, } if opts.CertDir != "" { // TODO use a more appropriate interval refresh, err := protojson.Marshal(durationpb.New(15 * time.Minute)) if err != nil { return nil, err } bootstrap.CertProviders = map[string]CertificateProvider{ "default": { PluginName: "file_watcher", Config: FileWatcherCertProviderConfig{ PrivateKeyFile: path.Join(opts.CertDir, "key.pem"), CertificateFile: path.Join(opts.CertDir, "cert-chain.pem"), CACertificateFile: path.Join(opts.CertDir, "root-cert.pem"), RefreshDuration: refresh, }, }, } } return &bootstrap, err } func extractMeta(meta map[string]string) (*structpb.Struct, error) { bytes, err := json.Marshal(meta) if err != nil { return nil, err } rawMeta := map[string]interface{}{} if err := json.Unmarshal(bytes, &rawMeta); err != nil { return nil, err } xdsMeta, err := structpb.NewStruct(rawMeta) if err != nil { return nil, err } return xdsMeta, nil } // GenerateBootstrapFile generates and writes atomically as JSON to the given file path. func GenerateBootstrapFile(opts GenerateBootstrapOptions, path string) (*Bootstrap, error) { bootstrap, err := GenerateBootstrap(opts, nil) if err != nil { return nil, err } jsonData, err := json.MarshalIndent(bootstrap, "", " ") if err != nil { return nil, err } if err := ioutil.WriteFile(path, jsonData, os.FileMode(0o644)); err != nil { return nil, fmt.Errorf("failed writing to %s: %v", path, err) } return bootstrap, nil }