istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/istio-agent/grpcxds/grpc_bootstrap.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  package grpcxds
    16  
    17  import (
    18  	"encoding/json"
    19  	"fmt"
    20  	"os"
    21  	"path"
    22  	"time"
    23  
    24  	core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    25  	"google.golang.org/protobuf/types/known/durationpb"
    26  	"google.golang.org/protobuf/types/known/structpb"
    27  
    28  	"istio.io/istio/pkg/file"
    29  	"istio.io/istio/pkg/log"
    30  	"istio.io/istio/pkg/model"
    31  	"istio.io/istio/pkg/util/protomarshal"
    32  )
    33  
    34  const (
    35  	ServerListenerNamePrefix = "xds.istio.io/grpc/lds/inbound/"
    36  	// ServerListenerNameTemplate for the name of the Listener resource to subscribe to for a gRPC
    37  	// server. If the token `%s` is present in the string, all instances of the
    38  	// token will be replaced with the server's listening "IP:port" (e.g.,
    39  	// "0.0.0.0:8080", "[::]:8080").
    40  	ServerListenerNameTemplate = ServerListenerNamePrefix + "%s"
    41  )
    42  
    43  // Bootstrap contains the general structure of what's expected by GRPC's XDS implementation.
    44  // See https://github.com/grpc/grpc-go/blob/master/xds/internal/xdsclient/bootstrap/bootstrap.go
    45  // TODO use structs from gRPC lib if created/exported
    46  type Bootstrap struct {
    47  	XDSServers                 []XdsServer                    `json:"xds_servers,omitempty"`
    48  	Node                       *core.Node                     `json:"node,omitempty"`
    49  	CertProviders              map[string]CertificateProvider `json:"certificate_providers,omitempty"`
    50  	ServerListenerNameTemplate string                         `json:"server_listener_resource_name_template,omitempty"`
    51  }
    52  
    53  type ChannelCreds struct {
    54  	Type   string `json:"type,omitempty"`
    55  	Config any    `json:"config,omitempty"`
    56  }
    57  
    58  type XdsServer struct {
    59  	ServerURI      string         `json:"server_uri,omitempty"`
    60  	ChannelCreds   []ChannelCreds `json:"channel_creds,omitempty"`
    61  	ServerFeatures []string       `json:"server_features,omitempty"`
    62  }
    63  
    64  type CertificateProvider struct {
    65  	PluginName string `json:"plugin_name,omitempty"`
    66  	Config     any    `json:"config,omitempty"`
    67  }
    68  
    69  func (cp *CertificateProvider) UnmarshalJSON(data []byte) error {
    70  	var dat map[string]*json.RawMessage
    71  	if err := json.Unmarshal(data, &dat); err != nil {
    72  		return err
    73  	}
    74  	*cp = CertificateProvider{}
    75  
    76  	if pluginNameVal, ok := dat["plugin_name"]; ok {
    77  		if err := json.Unmarshal(*pluginNameVal, &cp.PluginName); err != nil {
    78  			log.Warnf("failed parsing plugin_name in certificate_provider: %v", err)
    79  		}
    80  	} else {
    81  		log.Warnf("did not find plugin_name in certificate_provider")
    82  	}
    83  
    84  	if configVal, ok := dat["config"]; ok {
    85  		var err error
    86  		switch cp.PluginName {
    87  		case FileWatcherCertProviderName:
    88  			config := FileWatcherCertProviderConfig{}
    89  			err = json.Unmarshal(*configVal, &config)
    90  			cp.Config = config
    91  		default:
    92  			config := FileWatcherCertProviderConfig{}
    93  			err = json.Unmarshal(*configVal, &config)
    94  			cp.Config = config
    95  		}
    96  		if err != nil {
    97  			log.Warnf("failed parsing config in certificate_provider: %v", err)
    98  		}
    99  	} else {
   100  		log.Warnf("did not find config in certificate_provider")
   101  	}
   102  
   103  	return nil
   104  }
   105  
   106  const FileWatcherCertProviderName = "file_watcher"
   107  
   108  type FileWatcherCertProviderConfig struct {
   109  	CertificateFile   string          `json:"certificate_file,omitempty"`
   110  	PrivateKeyFile    string          `json:"private_key_file,omitempty"`
   111  	CACertificateFile string          `json:"ca_certificate_file,omitempty"`
   112  	RefreshDuration   json.RawMessage `json:"refresh_interval,omitempty"`
   113  }
   114  
   115  func (c *FileWatcherCertProviderConfig) FilePaths() []string {
   116  	return []string{c.CertificateFile, c.PrivateKeyFile, c.CACertificateFile}
   117  }
   118  
   119  // FileWatcherProvider returns the FileWatcherCertProviderConfig if one exists in CertProviders
   120  func (b *Bootstrap) FileWatcherProvider() *FileWatcherCertProviderConfig {
   121  	if b == nil || b.CertProviders == nil {
   122  		return nil
   123  	}
   124  	for _, provider := range b.CertProviders {
   125  		if provider.PluginName == FileWatcherCertProviderName {
   126  			cfg, ok := provider.Config.(FileWatcherCertProviderConfig)
   127  			if !ok {
   128  				return nil
   129  			}
   130  			return &cfg
   131  		}
   132  	}
   133  	return nil
   134  }
   135  
   136  // LoadBootstrap loads a Bootstrap from the given file path.
   137  func LoadBootstrap(file string) (*Bootstrap, error) {
   138  	data, err := os.ReadFile(file)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  	b := &Bootstrap{}
   143  	if err := json.Unmarshal(data, b); err != nil {
   144  		return nil, err
   145  	}
   146  	return b, err
   147  }
   148  
   149  type GenerateBootstrapOptions struct {
   150  	Node             *model.Node
   151  	XdsUdsPath       string
   152  	DiscoveryAddress string
   153  	CertDir          string
   154  }
   155  
   156  // GenerateBootstrap generates the bootstrap structure for gRPC XDS integration.
   157  func GenerateBootstrap(opts GenerateBootstrapOptions) (*Bootstrap, error) {
   158  	xdsMeta, err := extractMeta(opts.Node)
   159  	if err != nil {
   160  		return nil, fmt.Errorf("failed extracting xds metadata: %v", err)
   161  	}
   162  
   163  	// TODO direct to CP should use secure channel (most likely JWT + TLS, but possibly allow mTLS)
   164  	serverURI := opts.DiscoveryAddress
   165  	if opts.XdsUdsPath != "" {
   166  		serverURI = fmt.Sprintf("unix:///%s", opts.XdsUdsPath)
   167  	}
   168  
   169  	bootstrap := Bootstrap{
   170  		XDSServers: []XdsServer{{
   171  			ServerURI: serverURI,
   172  			// connect locally via agent
   173  			ChannelCreds:   []ChannelCreds{{Type: "insecure"}},
   174  			ServerFeatures: []string{"xds_v3"},
   175  		}},
   176  		Node: &core.Node{
   177  			Id:       opts.Node.ID,
   178  			Locality: opts.Node.Locality,
   179  			Metadata: xdsMeta,
   180  		},
   181  		ServerListenerNameTemplate: ServerListenerNameTemplate,
   182  	}
   183  
   184  	if opts.CertDir != "" {
   185  		// TODO use a more appropriate interval
   186  		refresh, err := protomarshal.Marshal(durationpb.New(15 * time.Minute))
   187  		if err != nil {
   188  			return nil, err
   189  		}
   190  
   191  		bootstrap.CertProviders = map[string]CertificateProvider{
   192  			"default": {
   193  				PluginName: "file_watcher",
   194  				Config: FileWatcherCertProviderConfig{
   195  					PrivateKeyFile:    path.Join(opts.CertDir, "key.pem"),
   196  					CertificateFile:   path.Join(opts.CertDir, "cert-chain.pem"),
   197  					CACertificateFile: path.Join(opts.CertDir, "root-cert.pem"),
   198  					RefreshDuration:   refresh,
   199  				},
   200  			},
   201  		}
   202  	}
   203  
   204  	return &bootstrap, err
   205  }
   206  
   207  func extractMeta(node *model.Node) (*structpb.Struct, error) {
   208  	bytes, err := json.Marshal(node.Metadata)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  	rawMeta := map[string]any{}
   213  	if err := json.Unmarshal(bytes, &rawMeta); err != nil {
   214  		return nil, err
   215  	}
   216  	xdsMeta, err := structpb.NewStruct(rawMeta)
   217  	if err != nil {
   218  		return nil, err
   219  	}
   220  	return xdsMeta, nil
   221  }
   222  
   223  // GenerateBootstrapFile generates and writes atomically as JSON to the given file path.
   224  func GenerateBootstrapFile(opts GenerateBootstrapOptions, path string) (*Bootstrap, error) {
   225  	bootstrap, err := GenerateBootstrap(opts)
   226  	if err != nil {
   227  		return nil, err
   228  	}
   229  	jsonData, err := json.MarshalIndent(bootstrap, "", "  ")
   230  	if err != nil {
   231  		return nil, err
   232  	}
   233  	if err := file.AtomicWrite(path, jsonData, os.FileMode(0o644)); err != nil {
   234  		return nil, fmt.Errorf("failed writing to %s: %v", path, err)
   235  	}
   236  	return bootstrap, nil
   237  }