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 }